@open-tomato/rafa 0.45.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 (140) hide show
  1. package/README.md +255 -23
  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 +101 -18
  14. package/dist/cli.js +74726 -62960
  15. package/dist/config/entry.d.ts +119 -0
  16. package/dist/config/index.d.ts +36 -0
  17. package/dist/config/index.js +51 -0
  18. package/dist/config/shape.d.ts +232 -0
  19. package/dist/config-registry.d.ts +127 -0
  20. package/dist/config-schema-board-project.d.ts +157 -0
  21. package/dist/config-schema-bugs.d.ts +71 -0
  22. package/dist/config-schema-errors.d.ts +54 -0
  23. package/dist/config-schema-features.d.ts +64 -0
  24. package/dist/config-schema-hub.d.ts +150 -0
  25. package/dist/config-schema-issues.d.ts +95 -0
  26. package/dist/config-schema-logger.d.ts +91 -0
  27. package/dist/config-schema-loop-continue.d.ts +122 -0
  28. package/dist/config-schema-loop-retries.d.ts +94 -0
  29. package/dist/config-schema-release.d.ts +307 -0
  30. package/dist/config-schema-tests.d.ts +170 -0
  31. package/dist/config-schema-triage.d.ts +142 -0
  32. package/dist/config-schema-wrap-up.d.ts +96 -0
  33. package/dist/core/command/command.d.ts +228 -0
  34. package/dist/core/command/confirm.d.ts +67 -0
  35. package/dist/core/command/index.d.ts +112 -0
  36. package/dist/core/command/lazy.d.ts +54 -0
  37. package/dist/core/command/registry-types.d.ts +96 -0
  38. package/dist/core/command/running.d.ts +96 -0
  39. package/dist/core/command/spends.d.ts +39 -0
  40. package/dist/core/command/types.d.ts +92 -0
  41. package/dist/core/command/version.d.ts +4 -0
  42. package/dist/core/config/branch-template.d.ts +20 -0
  43. package/dist/core/config/config-items.d.ts +86 -0
  44. package/dist/core/config/config-load.d.ts +47 -0
  45. package/dist/core/config/config-origins.d.ts +57 -0
  46. package/dist/core/config/config-readers.d.ts +167 -0
  47. package/dist/core/config/config-schema.d.ts +207 -0
  48. package/dist/core/config/config-sections.d.ts +493 -0
  49. package/dist/core/config/config.d.ts +320 -0
  50. package/dist/core/config/core-section.d.ts +39 -0
  51. package/dist/core/config/dangerous.d.ts +58 -0
  52. package/dist/core/config/default-routing.d.ts +16 -0
  53. package/dist/core/config/index.d.ts +141 -0
  54. package/dist/core/config/scope.d.ts +42 -0
  55. package/dist/core/config/section.d.ts +206 -0
  56. package/dist/core/config/sections.d.ts +45 -0
  57. package/dist/core/config/standard-schema.d.ts +27 -0
  58. package/dist/core/git/commit.d.ts +232 -0
  59. package/dist/core/git/gh-runner.d.ts +24 -0
  60. package/dist/core/git/git.d.ts +22 -0
  61. package/dist/core/git/index.d.ts +85 -0
  62. package/dist/core/loop/claude.d.ts +388 -0
  63. package/dist/core/loop/hop-record.d.ts +52 -0
  64. package/dist/core/loop/index.d.ts +179 -0
  65. package/dist/core/loop/session-env.d.ts +20 -0
  66. package/dist/core/loop/session-record-parse.d.ts +63 -0
  67. package/dist/core/loop/sessions.d.ts +408 -0
  68. package/dist/core/loop/suite-failure.d.ts +25 -0
  69. package/dist/core/output/active.d.ts +100 -0
  70. package/dist/core/output/stream.d.ts +20 -0
  71. package/dist/core/output/text.d.ts +55 -0
  72. package/dist/core/plan/block-reader.d.ts +46 -0
  73. package/dist/core/plan/blocks.d.ts +139 -0
  74. package/dist/core/plan/declaration.d.ts +366 -0
  75. package/dist/core/plan/plan-stamp.d.ts +75 -0
  76. package/dist/core/runtime/identity.d.ts +21 -0
  77. package/dist/core/runtime/index.d.ts +46 -0
  78. package/dist/effort/store/bring-forward.d.ts +131 -0
  79. package/dist/effort/store/check-rows.d.ts +31 -0
  80. package/dist/effort/store/commit-stats.d.ts +42 -0
  81. package/dist/effort/store/copy.d.ts +35 -0
  82. package/dist/effort/store/development-build.d.ts +105 -0
  83. package/dist/effort/store/effort-levels.d.ts +13 -0
  84. package/dist/effort/store/findings.d.ts +214 -0
  85. package/dist/effort/store/fix-schema.d.ts +184 -0
  86. package/dist/effort/store/index.js +10 -7
  87. package/dist/effort/store/location.d.ts +44 -0
  88. package/dist/effort/store/merge-commit-gaps.d.ts +68 -0
  89. package/dist/effort/store/merge-conflicts.d.ts +91 -0
  90. package/dist/effort/store/merge-rules.d.ts +82 -0
  91. package/dist/effort/store/merge-store.d.ts +211 -0
  92. package/dist/effort/store/merge-union.d.ts +92 -0
  93. package/dist/effort/store/migrate.d.ts +146 -0
  94. package/dist/effort/store/migrations.d.ts +92 -0
  95. package/dist/effort/store/ndjson-files.d.ts +114 -0
  96. package/dist/effort/store/origins.d.ts +116 -0
  97. package/dist/effort/store/plan-stubs.d.ts +79 -0
  98. package/dist/effort/store/rebuild-aside.d.ts +172 -0
  99. package/dist/effort/store/schema-plan.d.ts +102 -0
  100. package/dist/effort/store/session-row-types.d.ts +64 -0
  101. package/dist/effort/store/settings.d.ts +13 -0
  102. package/dist/effort/store/sqlite.d.ts +340 -0
  103. package/dist/effort/store/store-generation.d.ts +17 -0
  104. package/dist/effort/store/store-identity.d.ts +220 -0
  105. package/dist/effort/store/store-meta.d.ts +305 -0
  106. package/dist/effort/store/store-replaced.d.ts +72 -0
  107. package/dist/effort/store/types.d.ts +223 -0
  108. package/dist/errors/codes.d.ts +56 -0
  109. package/dist/errors/rafa-codes.d.ts +36 -0
  110. package/dist/index-2a7nb3an.js +125 -0
  111. package/dist/{index-r28fzsvc.js → index-39kbcw68.js} +350 -165
  112. package/dist/{index-nv34ptht.js → index-77ajfffn.js} +14 -9
  113. package/dist/index-cy98y87p.js +2597 -0
  114. package/dist/index-gqz4jn6j.js +13447 -0
  115. package/dist/index-gvek4r8m.js +710 -0
  116. package/dist/index-j4k2vdz3.js +1125 -0
  117. package/dist/index-jh1r4kcn.js +1067 -0
  118. package/dist/{index-mgh2s9bj.js → index-ykggr043.js} +58 -455
  119. package/dist/index-z1w83f81.js +4 -0
  120. package/dist/index.js +14715 -21516
  121. package/dist/learning/index.js +5 -6
  122. package/dist/plan/index.js +12 -11
  123. package/dist/plan-prompt.md +3 -1
  124. package/dist/ports/index.d.ts +732 -0
  125. package/dist/ports/learning.d.ts +101 -0
  126. package/dist/ports/log-level.d.ts +13 -0
  127. package/dist/ports/spec-review.d.ts +66 -0
  128. package/dist/pr/checks.d.ts +133 -0
  129. package/dist/pr/types.d.ts +422 -0
  130. package/dist/ready-4f8edezt.js +2016 -0
  131. package/dist/release-templates/release-bot-fragment.yml +153 -0
  132. package/dist/release-templates/release-publish.yml +110 -0
  133. package/dist/release-templates/release-settle.yml +122 -0
  134. package/dist/release-templates/release-tag.yml +102 -0
  135. package/dist/report/parse.d.ts +415 -0
  136. package/dist/schema/project-id.d.ts +79 -0
  137. package/dist/schema/tiers.d.ts +107 -0
  138. package/package.json +41 -3
  139. package/dist/index-hf2k497d.js +0 -16
  140. package/dist/index-nsr78ct2.js +0 -8219
@@ -0,0 +1,340 @@
1
+ /**
2
+ * The effort store's SQLite backend: the port over `bun:sqlite`, with
3
+ * every kind in one file.
4
+ *
5
+ * ## Layout
6
+ *
7
+ * One file, `effort.sqlite`, in the directory the NDJSON backend's
8
+ * files live in, which `effortStoreDir` (`location.ts`) spells once for
9
+ * both: `.rafa/effort/` (`EFFORT_STORE_DIR` in `ndjson-files.ts`),
10
+ * ignored by the project's `.gitignore` entry for `.rafa/`, or the
11
+ * directory `RAFA_EFFORT_DIR` names, which moves both backends together.
12
+ * The two backends can therefore hold a store side by side, and
13
+ * neither is committed unless the project sets `tracking.all`.
14
+ *
15
+ * One table per kind, named for the kind and keyed by the key the
16
+ * NDJSON rows are deduplicated by. Each table has these five columns,
17
+ * and `sessions` a sixth:
18
+ *
19
+ * - `seq`, the order the row was appended in.
20
+ * - The kind's key, filled from {@link EFFORT_KEY_PROJECTIONS}:
21
+ * `session_id` for `sessions` and `sha` for `commits`, the names the
22
+ * reference schema gives the same two keys. It is `NOT NULL` and
23
+ * `UNIQUE` and refuses an empty string, so no stored row lacks one.
24
+ * - `row_json`, the row itself.
25
+ * - `origin_store` and `origin_seq`, the store's origin and the row's
26
+ * own `seq`, which the append stamps as every production insert
27
+ * does (`origins.ts`), NULL in both on a store with no origin.
28
+ * - `worktree`, on `sessions` alone, the row's `worktree` field
29
+ * repeated from `row_json` (migration `session-worktree`), NULL for
30
+ * a session of the main checkout and for a row without the field.
31
+ *
32
+ * The row is stored whole, as the JSON text the NDJSON backend writes
33
+ * as a line, and read back through `JSON.parse` as that backend reads
34
+ * its lines. Both backends make the same pair of calls on the same row,
35
+ * so a row read from one is byte-identical to its counterpart in the
36
+ * other by construction, which is what the parity test holds them to.
37
+ * The row is not spread over a column per field. A session row carries
38
+ * open-keyed histograms (records by type, branch, entrypoint, effort
39
+ * and model) that no fixed column set can hold, and a row rebuilt from
40
+ * columns would have to reproduce the collector's field order to come
41
+ * out byte-identical. The key is the one field SQLite has to see,
42
+ * because the uniqueness constraint on it is what deduplicates.
43
+ * `worktree` is repeated beside it only so a query can read it; `read`
44
+ * still answers the row from `row_json`, so both backends' rows stay
45
+ * byte-identical.
46
+ *
47
+ * `seq` is an `INTEGER PRIMARY KEY`, SQLite's alias for the rowid, so
48
+ * the append order is a column the schema declares rather than an
49
+ * implicit one. That is a declaration, not a repair: measured on SQLite
50
+ * 3.51.0, a `VACUUM` left implicit rowids where they were as well.
51
+ *
52
+ * The file holds seventeen tables that are not kinds. Six are filled from
53
+ * task reports: `findings`, `blockers`, `out_of_scope_bugs` and
54
+ * `changes`, one per list a report carries, `report_absences`, one row
55
+ * per task session whose output held no report to read, and
56
+ * `task_reports`, one row per task session whose output carried one,
57
+ * holding its status. Four are filled from no report: `preflight`, one
58
+ * row per item a run's preflight checked, `dispatches`, one row per task
59
+ * session the loop spawned, `skill_invocations`, one row per skill a
60
+ * session invoked, and `plan_ci`, one row per reading of a plan's CI.
61
+ * An eleventh, `stale_docs`, is filled from task reports as the first
62
+ * six are, one row per documentation line a report named as stale;
63
+ * `stale-docs.ts` writes it and holds its row type. A twelfth,
64
+ * `bug_routes`, holds one row per `out_of_scope_bugs` entry the triage
65
+ * step routed; `bug-routes.ts` writes it and holds its row type. The
66
+ * thirteenth is `schema_migrations`, the migration log, which only
67
+ * `bring-forward.ts` writes; the section on bringing a store forward
68
+ * says what it holds. The last four are the store's own identity and
69
+ * merge trail: `store_meta`, `store_replaced`, `merges` and
70
+ * `merge_conflicts`. The port's row map names none of them, and
71
+ * nothing in this module reads or writes them. `findings.ts` writes the
72
+ * first, `triage.ts` the next two, `changes.ts` the fourth,
73
+ * `absences.ts` the fifth, `reports.ts` the sixth, `preflight.ts` the
74
+ * seventh, `dispatches.ts` the eighth, `skill-invocations.ts` the ninth
75
+ * and `plan-ci.ts` the tenth. `tracker-refs.ts` writes the first as
76
+ * well, setting a filed issue's reference on a row and inserting the
77
+ * row when there is none. `findings.ts`, `triage.ts`, `changes.ts`,
78
+ * `stale-docs.ts`, `preflight.ts` and `skill-invocations.ts` write through
79
+ * {@link writeSqliteStore}, as an append does, so a write left with
80
+ * nothing to insert still meets the schema check. `absences.ts` always
81
+ * has its one row and opens {@link withSqliteStore} directly, as
82
+ * `tracker-refs.ts` does with the one row it places. `reports.ts`
83
+ * always has its one row too, and passes {@link writeSqliteStore} a
84
+ * count of one, which opens the store as that direct call does, and so
85
+ * do `dispatches.ts` and `plan-ci.ts`. Each table is opened, brought
86
+ * forward and closed as every kind's table is. Each writer says why its
87
+ * tables have a column per field and their own deduplication keys,
88
+ * where a kind has `row_json` and one key.
89
+ *
90
+ * ## The port's rules, as they come out here
91
+ *
92
+ * - Append-only. `append` inserts, and nothing here updates or
93
+ * deletes a row.
94
+ * - `keys` is read back from the rows. It answers each stored row's
95
+ * key column, which is the column the uniqueness constraint dedupes
96
+ * by, so the set a collector skips by and the set an append dedupes
97
+ * by are one set. The key field inside `row_json` is read by
98
+ * neither.
99
+ * - Duplicate suppression covers the batch as well as the disk. The
100
+ * batch is inserted under `ON CONFLICT (<key>) DO NOTHING`, so a key
101
+ * repeated within it conflicts with the row its first occurrence
102
+ * inserted, and the first occurrence stays, as in the NDJSON file.
103
+ * That form rather than `INSERT OR IGNORE`, because measured,
104
+ * `OR IGNORE` also swallows a `NOT NULL` violation as a skipped row,
105
+ * where the upsert form absorbs the key conflict and throws on
106
+ * anything else.
107
+ * - The batch is checked before the store is opened. Every key is
108
+ * projected and every row serialised first, so a refused batch, or
109
+ * a row `JSON.stringify` throws on, leaves the file byte-identical,
110
+ * and on a store that does not exist yet leaves no file and no
111
+ * directory. A batch that passes is inserted in one transaction,
112
+ * and lands whole or not at all.
113
+ * - Writing nothing writes NOTHING, and still checks the schema. An
114
+ * empty batch on a store that does not exist opens nothing. On a
115
+ * store that exists it opens the store, through
116
+ * {@link writeSqliteStore}, so an empty append is brought forward,
117
+ * or refused, as a batch with rows is, and inserts nothing. An
118
+ * empty append, or one whose every row is already held, leaves a
119
+ * current store's bytes identical, measured, where an append that
120
+ * adds a row changes them.
121
+ * - Absence is the first-run case. `read`, `keys` and an empty append
122
+ * answer empty for a store with no file, without opening one, and
123
+ * only an append with a row to add opens the store with SQLite's
124
+ * create flag. Measured, an open without that flag refuses a missing
125
+ * file with `SQLITE_CANTOPEN` and creates nothing.
126
+ * - A store that exists and cannot be read throws. Measured, a
127
+ * directory at the path refuses to open (`SQLITE_CANTOPEN`), and a
128
+ * file that is not a database refuses its first statement
129
+ * (`SQLITE_NOTADB`). A zero-byte file is an empty database to
130
+ * SQLite, and is what a run killed between creating the file and
131
+ * its first commit leaves, so it reads as an empty store and is
132
+ * given the schema.
133
+ *
134
+ * ## What does not carry over from the NDJSON backend
135
+ *
136
+ * There is no partial last line to tolerate. SQLite commits a
137
+ * transaction whole or not at all (measured, a throw inside one rolls
138
+ * back its rows and its schema changes alike), so a run killed
139
+ * mid-append leaves no half-row. Nor can any append write a `row_json`
140
+ * that is not a JSON object, so `read` throws on one rather than
141
+ * skipping it. The NDJSON reader tolerates a malformed line because of
142
+ * a failure mode this store does not have; carried over, the tolerance
143
+ * would only hide a row written from outside.
144
+ *
145
+ * A key holding a lone UTF-16 surrogate REFUSES its batch here, where
146
+ * the NDJSON backend would store it. `JSON.stringify` escapes a lone
147
+ * surrogate, so the row would survive in `row_json`, but the key column
148
+ * is bound as UTF-8 text, and bun converts a lone surrogate lossily.
149
+ * Measured: `ab\uD800cd` comes back as `ab\u{10063}d`, the very text a
150
+ * genuine astral character binds to, so two distinct keys would share
151
+ * one row and the second would be dropped as a duplicate. A key the
152
+ * store cannot deduplicate by refuses its batch, which is the port's
153
+ * rule for a keyless row. The collectors' keys are log file basenames
154
+ * and hex shas, so this guards against a key they are not expected to
155
+ * produce.
156
+ *
157
+ * The journal stays SQLite's default rollback journal, measured as
158
+ * `delete` on a fresh store, with nothing left beside the file once a
159
+ * call returns. The reference switches to the write-ahead log. Under
160
+ * WAL a committed row can sit in the `-wal` file rather than in the
161
+ * store, so the store file's bytes would stop being the store, and
162
+ * comparing them, which is how a refused batch is shown to have written
163
+ * nothing, would stop meaning anything. Measured on macOS, where bun
164
+ * uses Apple's SQLite, a WAL store also leaves its `-wal` and `-shm`
165
+ * files behind after a close.
166
+ *
167
+ * Every call opens the store, does its work and closes it before
168
+ * returning, because the port has no `close`. Measured, thirty
169
+ * open-append-close cycles left no descriptor on the file open, where
170
+ * one open connection holds one. Every open sets `PRAGMA busy_timeout`
171
+ * to `effort.busyTimeoutMs`, read off `activeStoreSettings`
172
+ * (`settings.ts`) at each open and five seconds by default, so a call
173
+ * that finds another process holding the write lock waits up to that
174
+ * long for its turn and throws `SQLITE_BUSY` past it. Measured without
175
+ * it, an open that found a migration pending while another process held
176
+ * the lock threw `SQLITE_BUSY` at once.
177
+ *
178
+ * ## Bringing a store forward
179
+ *
180
+ * The schema's history, `SQLITE_MIGRATIONS`, lives in `./migrations.ts`,
181
+ * whose module note says what each named entry declares. It and its
182
+ * length, `SQLITE_SCHEMA_VERSION`, are re-exported here. Every call
183
+ * opens the store through {@link withSqliteStore}, which hands it to
184
+ * `bringForward` (`bring-forward.ts`) before any read or write. Each
185
+ * caller states its access: `keys`, `read` and every reader outside the
186
+ * port open for a `read`, and every writer, an empty write included,
187
+ * for a `write`. `planSchema` (`schema-plan.ts`) decides from the
188
+ * store's migration log, and `bringForward`'s module note says what is
189
+ * written and when. As it comes out here:
190
+ *
191
+ * - A store with nothing to adopt or apply is used as it is. A read
192
+ * of one writes nothing and takes no lock, so a read-only call
193
+ * leaves a current store's bytes identical.
194
+ * - A store a release before the log wrote, at a `user_version` from
195
+ * 1 to 13 with no `schema_migrations`, is adopted on its first open,
196
+ * a read included, since adoption counts as a write. A fresh or
197
+ * zero-byte file is given every migration.
198
+ * - A development build (`bun src/rafa.ts`, a checkout's
199
+ * `dist/cli.js`, `bun test`) adopts or applies only over a store
200
+ * under the temporary directory or `RAFA_EFFORT_DIR`. Over any
201
+ * other it throws `DevelopmentBuildRefusedError`
202
+ * (`development-build.ts`) before any write, naming a live loop
203
+ * recorded on the store, while a store with nothing pending is used
204
+ * as it is.
205
+ * - Whatever is pending is applied under `BEGIN IMMEDIATE`, after a
206
+ * second plan under that lock. Measured in `sqlite.test.ts`: two
207
+ * processes that open one fresh store at once both wait for the
208
+ * lock, one applies every migration and the other finds nothing
209
+ * pending and writes no row.
210
+ * - A store `planSchema` refuses throws `SchemaRefusedError`, whose
211
+ * message ends with the one command to run next, and no byte is
212
+ * written. A store logging a migration this rafa does not know,
213
+ * which breaks only older writers, is read and refused a write.
214
+ * - Once the store is brought forward, a write open mints its origin
215
+ * into `store_meta` when it holds none or is a copy
216
+ * (`store-meta.ts`). A read open never touches that row, so a read
217
+ * of a current store still writes nothing, minted or not.
218
+ *
219
+ * {@link migrateSchema} counts the history by position and keeps no
220
+ * log. No open and no command goes through it; `fix-schema.ts` builds
221
+ * its rebuild through `bringForward`, and tests plant pre-log stores
222
+ * with it.
223
+ */
224
+ import type { SqliteMigration } from './migrations.js';
225
+ import type { StoreAccess } from './schema-plan.js';
226
+ import type { StoreIdentitySeams } from './store-meta.js';
227
+ import type { EffortRowKind, EffortStore } from './types.js';
228
+ import { Database } from 'bun:sqlite';
229
+ export { SQLITE_MIGRATIONS, SQLITE_SCHEMA_VERSION } from './migrations.js';
230
+ /** The store's file name, inside the directory `effortStoreDir` answers. */
231
+ export declare const SQLITE_STORE_FILE_NAME = "effort.sqlite";
232
+ /**
233
+ * The file the SQLite store lives in under one repo root, whether or
234
+ * not it exists yet: in `RAFA_EFFORT_DIR` when it is set, and under
235
+ * `<root>/.rafa/effort` otherwise (`location.ts`). Every kind's table,
236
+ * and every table the report writers fill, sit in it.
237
+ */
238
+ export declare function sqliteStorePath(repoRoot: string): string;
239
+ /**
240
+ * A lone UTF-16 surrogate: a high half with no low half after it, or a
241
+ * low half with no high half before it. There is no `u` flag, so the
242
+ * pattern reads code units, which is the level a lone half exists at.
243
+ * Nor is there a `g` flag, so `test` keeps no state between calls.
244
+ *
245
+ * Exported for the report writers, whose text lands in this file too.
246
+ */
247
+ export declare const LONE_SURROGATE: RegExp;
248
+ /**
249
+ * The pre-log way of bringing a database forward, which no open and no
250
+ * command takes: it writes no migration log, which is what lets a test
251
+ * plant a store as a release before the log left it.
252
+ *
253
+ * Brings a database's schema up to the last version a migrations array
254
+ * holds: the store's own history unless another is passed, which is
255
+ * what lets the version arithmetic be driven with a history of any
256
+ * length. `path` is what a refusal names the database as.
257
+ *
258
+ * Every pending migration runs in one `BEGIN IMMEDIATE` transaction,
259
+ * and the version is read again once the write lock is held, since
260
+ * another process may have migrated the store in the meantime. A
261
+ * failure anywhere leaves the database at the version it was found at,
262
+ * with nothing half-applied. A database already at the last version
263
+ * takes no lock and is not written to.
264
+ *
265
+ * Two databases are refused, and neither is written to:
266
+ *
267
+ * - One already past the array's last version. A newer rafa wrote
268
+ * it, and this one can neither vouch for its schema nor write rows
269
+ * the newer one expects.
270
+ * - One a migration fails on, such as a version-0 file that already
271
+ * holds a table named for a kind. The migrations create their
272
+ * tables without the `IF NOT EXISTS` the reference uses, so a table
273
+ * nobody migrated is refused rather than adopted with a layout
274
+ * nobody chose. Measured, the refusal leaves the file
275
+ * byte-identical.
276
+ */
277
+ export declare function migrateSchema(db: Database, path: string, migrations?: readonly SqliteMigration[]): void;
278
+ /**
279
+ * Refuses a test process opening a store outside the temporary
280
+ * directory before anything is made (`guardTestProcess`, `location.ts`),
281
+ * then opens the store at `path`, sets its busy timeout to the active
282
+ * `effort.busyTimeoutMs` (`settings.ts`), brings its schema
283
+ * forward through `bringForward` for an open with `access`, settles its
284
+ * identity through `settleStoreIdentity` (`store-meta.ts`), hands it to
285
+ * `use`, and closes it whatever `use` did. A refused store throws
286
+ * `SchemaRefusedError` before `use` runs. Only a caller with a row to
287
+ * add passes `create`, and only then is the directory made.
288
+ *
289
+ * `access` is `write` for every caller that may write, whether or not
290
+ * it ends up writing, and `read` for one that only reads. A read can
291
+ * still write the store, when it adopts one or applies what is pending;
292
+ * the module note says when. Only a write mints: it records a new
293
+ * origin in `store_meta` when the store holds none or is a copy, and a
294
+ * read never reads or writes that row. `identity` is what the mint
295
+ * reads the host, the project and the clock through; every caller but
296
+ * a test leaves it out.
297
+ *
298
+ * Exported so the tables outside the port are opened, brought forward
299
+ * and closed exactly as every kind's is. A write that can be left with
300
+ * nothing to insert goes through {@link writeSqliteStore} instead.
301
+ */
302
+ export declare function withSqliteStore<T>(path: string, access: StoreAccess, create: boolean, use: (db: Database) => T, identity?: StoreIdentitySeams): T;
303
+ /**
304
+ * The one path every write that can be left with nothing to insert
305
+ * takes, whatever its batch size, so no writer can skip the schema
306
+ * check by having nothing to insert.
307
+ *
308
+ * With rows to insert, it opens the store, creating the file and its
309
+ * directory when absent, and answers what `insert` answers. With none,
310
+ * it still opens a store that exists for a write, so `bringForward`
311
+ * brings it forward or refuses it as it would for a write with rows,
312
+ * and it answers `empty` without calling `insert`. Only an empty write on a
313
+ * store that does not exist opens nothing, creating no file and no
314
+ * directory: the branch `read` and `keys` take on an absent store.
315
+ *
316
+ * `rowCount` counts what is left to insert once the caller has checked
317
+ * its batch. The check comes before this call, so a refused batch opens
318
+ * nothing.
319
+ */
320
+ export declare function writeSqliteStore<T>(path: string, rowCount: number, empty: T, insert: (db: Database) => T): T;
321
+ /**
322
+ * The SQLite backend's surface: the port, plus the one reading the
323
+ * NDJSON backend also makes, so a caller holding either can find the
324
+ * file a kind lives in.
325
+ */
326
+ export interface SqliteEffortStore extends EffortStore {
327
+ /**
328
+ * The file one kind lives in, whether or not it exists yet. For this
329
+ * backend it is the same file for every kind.
330
+ */
331
+ path: (kind: EffortRowKind) => string;
332
+ }
333
+ /**
334
+ * Opens the SQLite store under one repo root.
335
+ *
336
+ * Opening touches nothing on disk. Each call opens the file afresh,
337
+ * brings its schema forward and closes it before returning, which is
338
+ * why the port has no `close` for this backend to need.
339
+ */
340
+ export declare function openSqliteStore(repoRoot: string): SqliteEffortStore;
@@ -0,0 +1,17 @@
1
+ /** What follows the store file's name in its side record's name. */
2
+ export declare const STORE_GENERATION_SUFFIX = ".generation";
3
+ /** The side record's path for the store file at `storePath`: the same path with `.generation` appended. */
4
+ export declare function storeGenerationPath(storePath: string): string;
5
+ /**
6
+ * The generation in the side record of the store file at `storePath`,
7
+ * trimmed, or null when the record is absent or empty. Throws the
8
+ * filesystem's error for any other failure to read it.
9
+ */
10
+ export declare function readStoreGeneration(storePath: string): string | null;
11
+ /**
12
+ * Writes `generation` to the side record of the store file at
13
+ * `storePath` through a temporary file and a rename. Throws, with the
14
+ * temporary file removed, when the generation is refused or the write
15
+ * or rename fails.
16
+ */
17
+ export declare function writeStoreGeneration(storePath: string, generation: string): void;
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Which store a row came from: the facts a writing open compares to
3
+ * tell whether this file is still the store its origin was minted for,
4
+ * the generation that tells it whether this is the file last written,
5
+ * and the decision whether to mint a new one.
6
+ *
7
+ * ## The origin is the store, never the machine
8
+ *
9
+ * A row's origin pair is `(origin_store, origin_seq)`. Two stores that
10
+ * stamp one `origin_store` write colliding pairs as soon as both count
11
+ * past the rows they share, so a store that is a copy of another must
12
+ * stamp a new one. The rule every check here follows: **minting a new
13
+ * origin is always safe, and missing a copy is not.** An extra origin
14
+ * only adds a store id; a missed copy lets two stores write the same
15
+ * pairs. So each doubt resolves towards minting.
16
+ *
17
+ * ## The three facts
18
+ *
19
+ * `store_meta` records three facts when the origin is minted, and
20
+ * {@link decideStoreIdentity} mints again when a writing open finds any
21
+ * one of them different:
22
+ *
23
+ * - The **host id** ({@link readHostId}). A store opened on another
24
+ * machine is a copy.
25
+ * - The store file's **absolute path**, read through its real path
26
+ * ({@link readStoreFileFacts}), so a symlinked spelling of the same
27
+ * file is the same path. A store copied to another path is a copy.
28
+ * - Its **file identity**, device and inode. A `.bak` renamed back
29
+ * over the store has the path and the host and a new inode, so it
30
+ * is caught; the host id alone would miss it.
31
+ *
32
+ * A store renamed away and back to its own path keeps all three, and
33
+ * its origin. A store `rafa effort copy` builds is a new file and gets
34
+ * a new origin, which is harmless. The file `fix-schema`, `migrate` and
35
+ * `rafa effort merge` swap in is new as well, but the swap carries the
36
+ * store's identity onto it before the rename (`carryStoreIdentity`,
37
+ * `store-meta.ts`), so it keeps the origin. The backup the swap writes
38
+ * is the copy: it has an inode of its own and names the original's, so
39
+ * renamed back over the store it mints.
40
+ *
41
+ * Two copies these facts cannot see, measured on 2026-09-29 on tmpfs:
42
+ * a `.bak` restored with `cp` over the existing file is written into
43
+ * the old inode, so all three facts match; and a disk cloned whole
44
+ * matches all three as well. The generation below catches the first
45
+ * once the store was written after the `.bak` was taken. The merge
46
+ * catches what is left afterwards, since one origin pair holding two
47
+ * contents can only come from a missed copy.
48
+ *
49
+ * ## The generation
50
+ *
51
+ * `store_meta.generation` holds a generation, and the side record
52
+ * beside the store's real path (`store-generation.ts`) holds one too.
53
+ * A copy of the store file carries the row and leaves the side record
54
+ * behind, so {@link observeStore} reads the side record with the three
55
+ * facts, and {@link decideStoreIdentity} mints with the reason
56
+ * `generation` when the row holds a generation and the side record
57
+ * holds another or none. That holds whatever the inode, so it reaches
58
+ * a file the three facts call the same: a row one generation behind
59
+ * its side record mints with the host, path, device and inode all
60
+ * unchanged.
61
+ *
62
+ * A row whose generation is NULL was written by a runtime that did not
63
+ * know the column, or minted before any open wrote one. Its generation
64
+ * proves nothing, so the three facts alone judge it, and a side record
65
+ * beside it changes nothing. This module only decides: writing a new
66
+ * generation to the row and the side record is the caller's.
67
+ *
68
+ * ## The host id is a keyed hash
69
+ *
70
+ * `/etc/machine-id` on Linux, the `IOPlatformUUID` that
71
+ * `ioreg -rd1 -c IOPlatformExpertDevice` prints on macOS, and
72
+ * `os.hostname()` anywhere else or when the platform's own source says
73
+ * nothing. `machine-id(5)` asks that the id never be used directly, only
74
+ * through a keyed hash under an application-specific key, and a store
75
+ * travels to other devices, so the host id is {@link hostIdFrom}: an
76
+ * HMAC-SHA256 of the source name and the value. The hash keeps the
77
+ * sources apart, so a hostname can never equal a machine id.
78
+ *
79
+ * ## The project identity
80
+ *
81
+ * {@link readProjectIdentity} reads the root commit and the `origin`
82
+ * remote, which a merge compares to refuse two stores of different
83
+ * projects. The remote is stored normalised (`normalizeRemote`), which
84
+ * strips any credentials a URL carries and makes the spellings two
85
+ * checkouts use for one repository equal. Neither is a copy fact: a
86
+ * remote added later is the same store.
87
+ *
88
+ * ## A read decides nothing
89
+ *
90
+ * An open that only reads never mints, and never asks for the facts:
91
+ * `decideStoreIdentity` takes them as a function it calls on a write
92
+ * alone, so a read spawns no `ioreg`, stats nothing and writes nothing.
93
+ */
94
+ import type { StoreAccess } from './schema-plan.js';
95
+ import type { GitRunner } from '../../core/git/index.js';
96
+ /** Which source a host id was derived from. */
97
+ export type HostIdSource = 'machine-id' | 'platform-uuid' | 'hostname';
98
+ /** What {@link readHostId} reads the machine through. */
99
+ export interface HostIdSeams {
100
+ /** `process.platform`'s spelling. `os.platform()` when absent. */
101
+ readonly platform?: string;
102
+ /** The Linux machine id, or null when there is none. */
103
+ readonly readMachineId?: () => string | null;
104
+ /** The macOS platform UUID, or null when there is none. */
105
+ readonly readPlatformUuid?: () => string | null;
106
+ /** The hostname, the fallback. `os.hostname()` when absent. */
107
+ readonly readHostname?: () => string;
108
+ }
109
+ /**
110
+ * The host id stored for `value` read from `source`: the hex
111
+ * HMAC-SHA256 of `<source>:<value>` under this module's key. Never the
112
+ * value itself; see the module note.
113
+ */
114
+ export declare function hostIdFrom(source: HostIdSource, value: string): string;
115
+ /**
116
+ * The `IOPlatformUUID` value in `ioreg -rd1 -c IOPlatformExpertDevice`
117
+ * output, or null when no line carries one.
118
+ */
119
+ export declare function parseIoregPlatformUuid(output: string): string | null;
120
+ /**
121
+ * This machine's host id: from the machine id on Linux, the platform
122
+ * UUID on macOS, the hostname otherwise or when that source answers
123
+ * nothing. Tests pass every seam and never read the real machine.
124
+ */
125
+ export declare function readHostId(seams?: HostIdSeams): string;
126
+ /** Where the store file is and which file it is. */
127
+ export interface StoreFileFacts {
128
+ /** The file's real path: absolute, with every symlink resolved. */
129
+ readonly storePath: string;
130
+ /** The device the file is on. A bigint, so no inode is rounded. */
131
+ readonly fileDev: bigint;
132
+ /** The file's inode on that device. */
133
+ readonly fileIno: bigint;
134
+ }
135
+ /**
136
+ * The absolute path and file identity of the store file at `path`.
137
+ * Throws the filesystem's own error, which names the path, when the
138
+ * file does not exist.
139
+ */
140
+ export declare function readStoreFileFacts(path: string): StoreFileFacts;
141
+ /** What a writing open observes: the three facts and the side record's generation. */
142
+ export interface StoreIdentityFacts extends StoreFileFacts {
143
+ /** {@link readHostId}'s answer. */
144
+ readonly hostId: string;
145
+ /** The generation in the side record beside the store's real path, or null when it has none. */
146
+ readonly generation: string | null;
147
+ }
148
+ /**
149
+ * The facts of the store file at `path`, with the host id `readHost`
150
+ * answers ({@link readHostId} when absent) and the generation the side
151
+ * record beside its real path holds.
152
+ */
153
+ export declare function observeStore(path: string, readHost?: () => string): StoreIdentityFacts;
154
+ /**
155
+ * The identity a store recorded when its origin was minted. The device
156
+ * and inode may come back from SQLite as numbers.
157
+ */
158
+ export interface RecordedIdentity {
159
+ /** The origin the store stamps on the rows it writes. */
160
+ readonly storeId: string;
161
+ /** The host id it was minted on. */
162
+ readonly hostId: string;
163
+ /** The absolute path it was minted at. */
164
+ readonly storePath: string;
165
+ /** The device its file was on. */
166
+ readonly fileDev: bigint | number;
167
+ /** Its file's inode. */
168
+ readonly fileIno: bigint | number;
169
+ /**
170
+ * The generation last written into the row, or null for a row written
171
+ * by a runtime before the `generation` column, which only the three
172
+ * facts judge.
173
+ */
174
+ readonly generation: string | null;
175
+ }
176
+ /**
177
+ * Why a writing open mints: the store holds no identity yet, the host,
178
+ * the path or the file (device or inode) differs from the one recorded,
179
+ * or the row holds a generation the side record does not.
180
+ */
181
+ export type MintReason = 'unminted' | 'host' | 'path' | 'file' | 'generation';
182
+ /** What a store open does about its identity. */
183
+ export type IdentityDecision =
184
+ /** A read: nothing is minted, compared or observed. */
185
+ {
186
+ readonly action: 'none';
187
+ }
188
+ /** A write to the store the origin was minted for: stamp `storeId`. */
189
+ | {
190
+ readonly action: 'keep';
191
+ readonly storeId: string;
192
+ }
193
+ /** A write that must mint a new origin and record `facts`. */
194
+ | {
195
+ readonly action: 'mint';
196
+ readonly reasons: readonly MintReason[];
197
+ readonly facts: StoreIdentityFacts;
198
+ };
199
+ /**
200
+ * What an open with `access` does about the store's identity, given the
201
+ * identity it `recorded` (null when it holds none). A read answers
202
+ * `none` without calling `observe`. A write observes the store once and
203
+ * mints when nothing is recorded, any of the three facts moved, or the
204
+ * row holds a generation the side record does not; it keeps the
205
+ * recorded origin otherwise.
206
+ */
207
+ export declare function decideStoreIdentity(access: StoreAccess, recorded: RecordedIdentity | null, observe: () => StoreIdentityFacts): IdentityDecision;
208
+ /** The project a store belongs to, as a merge compares it. */
209
+ export interface ProjectIdentity {
210
+ /** The smallest root commit of `HEAD`, or null outside a repository with commits. */
211
+ readonly rootCommit: string | null;
212
+ /** `origin`'s URL, normalised and without credentials, or null when there is none. */
213
+ readonly remote: string | null;
214
+ }
215
+ /**
216
+ * The project identity of the repository holding `dir`, read through
217
+ * `git` (a runner in `dir` when absent). Never throws: a part git
218
+ * cannot read is null.
219
+ */
220
+ export declare function readProjectIdentity(dir: string, git?: GitRunner): ProjectIdentity;