@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,305 @@
1
+ /**
2
+ * The store's own identity row: reading what `store_meta` records,
3
+ * minting a new origin into it on a writing open that needs one,
4
+ * settling on that mint which ids `store_replaced` holds, and rotating
5
+ * the store's generation on every writing open and on every rebuild
6
+ * that keeps the store's origin.
7
+ *
8
+ * ## What a writing open writes
9
+ *
10
+ * `withSqliteStore` (`sqlite.ts`) calls {@link settleStoreIdentity}
11
+ * after `bringForward`, so the table and its `generation` column exist,
12
+ * and before any caller reads or writes. `decideStoreIdentity`
13
+ * (`store-identity.ts`) makes the decision: a `read` open answers
14
+ * `none` and this module neither reads the row, observes the file nor
15
+ * touches the side record, so a read writes nothing. A `write` open
16
+ * reads the row, observes the host, the real path, the file's device
17
+ * and inode and the side record's generation, and mints when the row is
18
+ * absent, any of the three facts moved, or the row holds a generation
19
+ * the side record does not. A row's generation is read as NULL when its
20
+ * store has no `generation` column yet, and a NULL one is judged by the
21
+ * three facts alone.
22
+ *
23
+ * Every writing open then takes the write lock with `BEGIN IMMEDIATE`,
24
+ * reads the row and decides again under it, since another process may
25
+ * have minted or rotated the same file while this one waited, and
26
+ * writes a new random generation (`newGeneration`, a UUID) in that
27
+ * transaction: on a keep, into `store_meta.generation`; on a mint, into
28
+ * the new row (`id = 1`) with the new origin. Either way it then writes
29
+ * the same generation to the side record (`store-generation.ts`),
30
+ * `<real path of the store file>.generation` beside it, and commits. So
31
+ * a store written without a generation keeps its origin on its first
32
+ * write and holds one after it, two processes that open one unminted
33
+ * store at once record one origin, and a copy of the file, which
34
+ * carries the row and leaves the side record behind, mints on its first
35
+ * write once the original has been written since the copy was taken.
36
+ *
37
+ * The row is written first and the side record second, both before the
38
+ * commit, because a writer that waits for the lock decides only once it
39
+ * holds it and so always sees the two agree. A side record that fails
40
+ * to write throws and rolls the row back. A crash after the side
41
+ * record's rename and before the commit leaves the side record one
42
+ * rotation ahead of the row; the next writing open mints once, which is
43
+ * safe, and keeps on the write after. A side record restored from
44
+ * before the last write is one rotation behind and mints the same way.
45
+ *
46
+ * What the generation does not catch: a restore of the store's whole
47
+ * directory, the store file and its side record together, brings back a
48
+ * row and a side record that agree. When the file comes back at its old
49
+ * inode, as a `cp` over the existing file writes it, all three facts
50
+ * match as well and nothing mints; the merge's collision check is what
51
+ * catches that copy afterwards.
52
+ *
53
+ * ## The project a mint records
54
+ *
55
+ * `store_meta.project_root_commit` is `NOT NULL`. The project is read
56
+ * through git in the store file's directory (`readProjectIdentity`), so
57
+ * `<root>/.rafa/effort/` and a copy under `<root>/.rafa/scratch/` both
58
+ * name the project they sit in. When git finds no root commit there, a
59
+ * mint takes the project the row already records, since a copy moved
60
+ * outside its checkout is still that project's store. When neither
61
+ * names one, as for a store under `tmpdir()` outside any repository
62
+ * with commits, nothing is written and the outcome is `no-project`: the
63
+ * store stays unminted, no generation is written to the row or the side
64
+ * record, and the next writing open asks again. That costs the git
65
+ * reads on each such open and loses no origin, since an unminted store
66
+ * stamps none. Git is only asked on a mint, never on a write that keeps
67
+ * the origin, and before the lock is taken: under it, git is asked only
68
+ * for a store that came due a mint while this open waited.
69
+ *
70
+ * ## The ids a mint replaces
71
+ *
72
+ * `store_meta` holds one row, so a mint writes the new origin over the
73
+ * old one. A claim names the id of the store that made it, and a store
74
+ * file given a new device or inode under its own host and path is still
75
+ * this device's store. So a mint settles `store_replaced`
76
+ * (`store-replaced.ts`) by its reasons, in the transaction that writes
77
+ * the new row:
78
+ *
79
+ * - `file`, `generation`, or both: the host and the path were kept.
80
+ * One host and one path hold one store file at a time, so this is
81
+ * the device's own store, moved or rolled back. The outgoing id is
82
+ * recorded with the time the new row takes as `minted_at`, read from
83
+ * the clock once, and every id recorded before it stays. `generation`
84
+ * alone is what a crash between the side record's rename and the
85
+ * commit leaves, and both together what a backup renamed back over
86
+ * the store after a write leaves.
87
+ * - `host` or `path` among them, whatever else moved: the file is a
88
+ * copy, and a copy carries the original's table inside it. The table
89
+ * is emptied and the outgoing id is not recorded, so a copy inherits
90
+ * no claim.
91
+ * - `unminted`: no id is outgoing, and nothing is written.
92
+ *
93
+ * A write that keeps the origin, and a mint that knows no project and
94
+ * writes nothing, leave the table as it was. The record and the row
95
+ * commit together: a side record that fails to write rolls both back.
96
+ *
97
+ * `withSqliteStore` settles after `bringForward`, which has applied
98
+ * `store-replaced` by then, so the table is there on every writing open.
99
+ * A mint that keeps the host and the path over a store without it
100
+ * throws SQLite's `no such table: store_replaced` and writes nothing,
101
+ * since an id that cannot be kept is not reported as kept. A mint an
102
+ * older runtime made recorded nothing, and neither did any mint from
103
+ * before the table: those ids are in no store.
104
+ *
105
+ * ## Device and inode as SQLite integers
106
+ *
107
+ * Both are unsigned 64-bit values, and SQLite's INTEGER is signed.
108
+ * Measured with Bun 1.4.2: binding `2n ** 63n` raises nothing and
109
+ * stores `-9223372036854775808`, and a plain read would answer a
110
+ * rounded number. So {@link toStoredInteger} writes the two's
111
+ * complement explicitly, the row is read through `CAST(… AS TEXT)`,
112
+ * and {@link fromStoredInteger} turns it back unsigned. Every value
113
+ * below 2^63 is written and read as itself.
114
+ *
115
+ * ## Carrying the identity onto a file that replaces the store
116
+ *
117
+ * A rebuild beside the store (`rebuild-aside.ts`) builds a parallel file
118
+ * that holds the store's own `store_meta` row, since `store_meta` is
119
+ * `local` in `MERGE_RULES`, and then renames it over the store. The
120
+ * renamed file has the store's path and host and the parallel file's
121
+ * inode, so its next writing open would mint although it is the same
122
+ * store. {@link carryStoreIdentity} runs before that rename. It reads
123
+ * the live file's row and asks `decideStoreIdentity` what a write to the
124
+ * live file would do. Only on a keep does it write the parallel file's
125
+ * own device and inode, and a new generation (`newGeneration`), into the
126
+ * parallel file's row, and it answers that generation in `carried`.
127
+ * `swapIn` then writes it to the live store's side record immediately
128
+ * before the rename. After the rename the parallel file sits at the live
129
+ * path beside that side record, so all three facts and the generation
130
+ * agree and the next writing open keeps the origin.
131
+ *
132
+ * The generation rotates on the carry, rather than staying the one the
133
+ * live side record held, so that nothing the rebuild leaves behind
134
+ * matches the side record afterwards. The backup and the replaced live
135
+ * file both hold the old generation in their rows: either renamed back
136
+ * over the store mints on its first write, even when it lands on an
137
+ * inode number the host reused. A rename that fails after the side
138
+ * record was written leaves the live file holding the old generation
139
+ * beside the new one, so it mints once on its next write, the safe
140
+ * side, and keeps on the write after. A parallel file whose
141
+ * `store_meta` has no `generation` column, as for a store written
142
+ * before `store-meta-generation`, is given its device and inode only;
143
+ * `carried` then holds a null generation and nothing is written to the
144
+ * side record, and the renamed file is judged by the three facts alone.
145
+ *
146
+ * Every other answer carries nothing, so the renamed file mints on its
147
+ * next write, which is the safe side. That covers a live file with no
148
+ * `store_meta` table or no row (an unminted store has no origin to
149
+ * carry), a live file that would mint itself (another host, path,
150
+ * device or inode, or a row generation its side record does not hold),
151
+ * and a parallel file whose row is not the live store's. On each of
152
+ * them the parallel file's row is still given a fresh random generation
153
+ * (`newGeneration`), where it has a row and a `generation` column, and
154
+ * the answer reports it in `spoiled`; nothing else in the row changes
155
+ * and nothing is written to the side record. Without it, the renamed
156
+ * file would hold the generation the side record holds, since the
157
+ * parallel file is built from the live one, and would be judged by the
158
+ * three facts alone; a file that lands at the path on the inode number
159
+ * its row names, which a host that reuses a freed number can give it,
160
+ * would then keep an origin that was due a mint. With the fresh
161
+ * generation the side record does not hold it, and the renamed file
162
+ * mints however the inode numbers fall. The row is never given a null
163
+ * generation, which would be judged by the three facts alone as well. A
164
+ * parallel file with no row or no `generation` column is left
165
+ * untouched, and `spoiled` is null. A write to the
166
+ * live store between the side record's write and the rename rotates the
167
+ * side record past the generation carried, so the renamed file mints as
168
+ * well. A write between the carry and the side record's write is
169
+ * overwritten in the side record by the generation carried, so the
170
+ * renamed file keeps the origin; the rows such a write adds were never
171
+ * in the parallel file, as for any write after the rebuild began.
172
+ */
173
+ import type { StoreAccess } from './schema-plan.js';
174
+ import type { MintReason, ProjectIdentity, RecordedIdentity } from './store-identity.js';
175
+ import { Database } from 'bun:sqlite';
176
+ /**
177
+ * What a store open reads the machine and the clock through. Every
178
+ * field falls back to the real one; tests pass the host id and the
179
+ * project and never read this machine's.
180
+ */
181
+ export interface StoreIdentitySeams {
182
+ /** The host id. `readHostId` when absent. */
183
+ readonly readHostId?: () => string;
184
+ /** The project of the repository holding `dir`. `readProjectIdentity` when absent. */
185
+ readonly readProject?: (dir: string) => ProjectIdentity;
186
+ /** A new store id. `crypto.randomUUID` when absent. */
187
+ readonly newStoreId?: () => string;
188
+ /** A new generation. `crypto.randomUUID` when absent. */
189
+ readonly newGeneration?: () => string;
190
+ /** The time a mint records, in the row and for the id it replaces. `new Date()` when absent. */
191
+ readonly now?: () => Date;
192
+ }
193
+ /** The whole `store_meta` row. */
194
+ export interface StoreMeta extends RecordedIdentity {
195
+ /** The project's smallest root commit. */
196
+ readonly projectRootCommit: string;
197
+ /** Its normalised `origin` remote, or null when it had none. */
198
+ readonly projectRemote: string | null;
199
+ /** The device the file was on, unsigned. */
200
+ readonly fileDev: bigint;
201
+ /** The file's inode, unsigned. */
202
+ readonly fileIno: bigint;
203
+ /** When the origin was minted, as an ISO string. */
204
+ readonly mintedAt: string;
205
+ }
206
+ /** What {@link settleStoreIdentity} did. */
207
+ export type IdentityOutcome =
208
+ /** A read: nothing was read, observed or written. */
209
+ {
210
+ readonly action: 'none';
211
+ }
212
+ /** A write to the store its origin was minted for: only a new generation written. */
213
+ | {
214
+ readonly action: 'keep';
215
+ readonly storeId: string;
216
+ }
217
+ /** A write that recorded the new origin `storeId` and a new generation, and settled `store_replaced`. */
218
+ | {
219
+ readonly action: 'mint';
220
+ readonly storeId: string;
221
+ readonly reasons: readonly MintReason[];
222
+ }
223
+ /** A write that was due a mint and wrote nothing, knowing no project. */
224
+ | {
225
+ readonly action: 'no-project';
226
+ readonly reasons: readonly MintReason[];
227
+ };
228
+ /** An unsigned 64-bit value as the signed INTEGER SQLite stores. */
229
+ export declare function toStoredInteger(value: bigint): bigint;
230
+ /** The unsigned value {@link toStoredInteger} stored as `text`. */
231
+ export declare function fromStoredInteger(text: string): bigint;
232
+ /**
233
+ * The `store_meta` row of the store `db` holds, or null when it holds
234
+ * none. The generation is null for a row whose store has no
235
+ * `generation` column yet, as for one written before it.
236
+ */
237
+ export declare function readStoreMeta(db: Database): StoreMeta | null;
238
+ /**
239
+ * Settles the identity of the store at `path`, open as `db`, for an open
240
+ * with `access`: nothing on a read; on a write, under the write lock,
241
+ * the recorded origin kept for the same file or a new one minted for an
242
+ * unminted store or a copy, and a new generation written to the row and
243
+ * the side record either way. A mint over a recorded origin also
244
+ * settles `store_replaced`: the outgoing id recorded when the host and
245
+ * the path were kept, the table emptied when either moved. See the
246
+ * module note.
247
+ */
248
+ export declare function settleStoreIdentity(db: Database, path: string, access: StoreAccess, seams?: StoreIdentitySeams): IdentityOutcome;
249
+ /**
250
+ * What {@link carryStoreIdentity} did. Every answer but `carried` reports
251
+ * in `spoiled` the fresh generation it wrote into the parallel file's
252
+ * row, the only thing it wrote there, or null when that file holds no
253
+ * `store_meta` row with a `generation` column and nothing was written.
254
+ */
255
+ export type CarryOutcome =
256
+ /** The live file holds no `store_meta` table. */
257
+ {
258
+ readonly action: 'no-table';
259
+ readonly spoiled: string | null;
260
+ }
261
+ /** The live file's `store_meta` holds no row: an unminted store. */
262
+ | {
263
+ readonly action: 'no-row';
264
+ readonly spoiled: string | null;
265
+ }
266
+ /** A write to the live file would mint, for `reasons`. */
267
+ | {
268
+ readonly action: 'mint';
269
+ readonly reasons: readonly MintReason[];
270
+ readonly spoiled: string | null;
271
+ }
272
+ /** The parallel file holds no `store_meta` row of the live store's id. */
273
+ | {
274
+ readonly action: 'other-row';
275
+ readonly spoiled: string | null;
276
+ }
277
+ /**
278
+ * The parallel file's row now records its own device and inode and the
279
+ * new `generation`, which the side record has yet to be given; null when
280
+ * its `store_meta` has no `generation` column, so none was written.
281
+ */
282
+ | {
283
+ readonly action: 'carried';
284
+ readonly storeId: string;
285
+ readonly generation: string | null;
286
+ };
287
+ /**
288
+ * Carries the identity of the live store at `livePath` onto the file at
289
+ * `parallelPath` that is about to be renamed over it. When
290
+ * `decideStoreIdentity('write', …)` answers keep for the live file and
291
+ * the parallel file holds a row of the live store's id, the parallel
292
+ * file's own device and inode and a new generation from
293
+ * `seams.newGeneration` (`crypto.randomUUID` when absent) are written
294
+ * into that row, and its store id, path, host and every other column are
295
+ * kept. The answer `carried` holds that generation, which the caller
296
+ * writes to the live store's side record immediately before the rename
297
+ * (`swapIn`, `rebuild-aside.ts`); this function writes no side record.
298
+ * On every other answer a new generation from the same seam is written
299
+ * into the parallel file's row, where it has one with a `generation`
300
+ * column, and nothing else, and the answer reports it in `spoiled`. The
301
+ * host id is read through `seams.readHostId` (`readHostId` when absent).
302
+ * Throws the filesystem's own error, which names the path, when either
303
+ * file does not exist. See the module note.
304
+ */
305
+ export declare function carryStoreIdentity(livePath: string, parallelPath: string, seams?: StoreIdentitySeams): CarryOutcome;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The ids a store carried before its present one: reading the table
3
+ * `store_replaced`, recording one id into it with the time it was
4
+ * replaced, and emptying it.
5
+ *
6
+ * ## What the table holds
7
+ *
8
+ * Migration `store-replaced` creates `store_replaced`, one row per id:
9
+ * `store_id`, its primary key, and `replaced_at`, an ISO string. Like
10
+ * `store_meta` it is SQLite-only, outside the port's row map, and
11
+ * `local` in `MERGE_RULES`, so it never leaves the machine. A store
12
+ * file found under the host and path its `store_meta` row names is this
13
+ * device's own store, moved or rolled back, and a claim made under an
14
+ * id held here is still this device's.
15
+ *
16
+ * ## What this module does not decide
17
+ *
18
+ * It reads and writes rows and nothing else. Which mint records the
19
+ * outgoing id and which one empties the table is the caller's decision,
20
+ * as is the transaction either write runs in: every function takes the
21
+ * open handle and begins none of its own, so a write made inside the
22
+ * caller's transaction commits or rolls back with it. No function opens
23
+ * a store; a caller passes the handle `withSqliteStore` (`sqlite.ts`)
24
+ * gave it.
25
+ *
26
+ * ## A store without the table
27
+ *
28
+ * A store an older runtime left has no `store_replaced`.
29
+ * {@link readReplacedStoreIds}
30
+ * answers none for it, the same answer as for an empty table, and
31
+ * creates nothing. {@link clearReplacedStoreIds} writes nothing to it,
32
+ * since a table that does not exist already holds no id.
33
+ * {@link recordReplacedStoreId} is the one that throws there, with
34
+ * SQLite's own `no such table: store_replaced`: an id that cannot be
35
+ * kept is not reported as kept.
36
+ *
37
+ * ## An id recorded twice
38
+ *
39
+ * The id is the primary key. Recording one the table already holds
40
+ * adds no row and keeps the time first recorded, because that is when
41
+ * the store stopped carrying the id.
42
+ */
43
+ import type { Database } from 'bun:sqlite';
44
+ /** One id a store carried before its present one. */
45
+ export interface ReplacedStoreId {
46
+ /** The id the store carried. */
47
+ readonly storeId: string;
48
+ /** When the store stopped carrying it, as an ISO string. */
49
+ readonly replacedAt: string;
50
+ }
51
+ /**
52
+ * The ids the store `db` holds replaced, each with the time it was
53
+ * replaced, the earliest first and by id within one instant. Answers
54
+ * none for a store without the table and for one with it empty, and
55
+ * writes nothing either way.
56
+ */
57
+ export declare function readReplacedStoreIds(db: Database): readonly ReplacedStoreId[];
58
+ /**
59
+ * Records that the store `db` holds stopped carrying `storeId` at
60
+ * `replacedAt`, written as an ISO string. An id the table already holds
61
+ * keeps its first time and adds no row. Throws, writing nothing, when
62
+ * the id is empty or the time is no valid date, and throws SQLite's own
63
+ * error when the store has no `store_replaced` table. Begins no
64
+ * transaction; see the module note.
65
+ */
66
+ export declare function recordReplacedStoreId(db: Database, storeId: string, replacedAt: Date): void;
67
+ /**
68
+ * Removes every row of `store_replaced` from the store `db` holds and
69
+ * keeps the table. Writes nothing to a store without the table, and
70
+ * does not create it. Begins no transaction; see the module note.
71
+ */
72
+ export declare function clearReplacedStoreIds(db: Database): void;
@@ -0,0 +1,223 @@
1
+ /**
2
+ * The effort store's port: the contract both backends implement, and
3
+ * the one value they share.
4
+ *
5
+ * The sibling's store was bound to one file and one key projection per
6
+ * instance. This port is parameterised by row kind instead, per the
7
+ * spec's signature, so one store holds every kind and each call names
8
+ * the kind it acts on. Left there, that would reopen the failure the
9
+ * binding existed to prevent: a collector skipping by one projection of
10
+ * a row while the append dedupes by another, so work is read again or
11
+ * rows are duplicated, and both look like a store that is working. So
12
+ * the projection is not the caller's to pass. No method takes one;
13
+ * {@link EFFORT_KEY_PROJECTIONS} is a closed record a backend reads for
14
+ * `keys` and `append` alike.
15
+ *
16
+ * ## What every backend owes
17
+ *
18
+ * These are the sibling's NDJSON store's rules, carried over unchanged
19
+ * in behaviour, and the SQLite backend owes them too, so that nothing a
20
+ * caller typed against {@link EffortStore} relies on holds for one
21
+ * backend and not the other.
22
+ *
23
+ * - Append-only. Nothing a store holds is rewritten or removed.
24
+ * - `keys` is read back from the rows, never from a sidecar index,
25
+ * which would be a second authority free to disagree with them. A
26
+ * stored row with no key contributes nothing, rather than a
27
+ * placeholder every other keyless row would collide with.
28
+ * - `append` adds only the rows whose key is not already held,
29
+ * whether it is held in storage or earlier in the same batch.
30
+ * - A row with no key REFUSES the whole batch: the append throws
31
+ * before anything is written. Such a row could never be
32
+ * deduplicated, so appending it would duplicate it on every run.
33
+ * - Writing nothing writes NOTHING. An append left with no row to add
34
+ * creates no file and no directory, and `read` and `keys` on a
35
+ * store that does not exist answer empty and create nothing either:
36
+ * absence is the first-run case. Only absence is absorbed. A store
37
+ * that exists and cannot be read throws, so it cannot pass for an
38
+ * empty one and have everything appended again.
39
+ *
40
+ * ## What the port leaves out
41
+ *
42
+ * Readings only one storage format can make, such as the NDJSON line
43
+ * arithmetic, its unparsed-line count, or the newline it writes ahead
44
+ * of a partial last line, are not the port's. A backend reports those
45
+ * on a wider type of its own; {@link AppendResult} and `read` carry
46
+ * only what both backends can answer.
47
+ *
48
+ * There is no `close`, because the spec's port has no lifecycle method.
49
+ * A backend acquires and releases what it needs within each call; a
50
+ * handle held across calls would have no owner to release it.
51
+ *
52
+ * The port is synchronous because both backends can be. The sibling's
53
+ * NDJSON store reads and appends through `node:fs`'s synchronous calls,
54
+ * and `bun:sqlite`'s `run` and `all` return plain values rather than
55
+ * promises, measured on bun 1.3.14.
56
+ */
57
+ import type { CommitStats } from './commit-stats.js';
58
+ import type { PlanStubMatch } from './plan-stubs.js';
59
+ import type { SessionKind, SessionStats } from './session-row-types.js';
60
+ /**
61
+ * Where a session ran, as its row records it.
62
+ *
63
+ * `local` is the only member in this phase, and the collector writes it
64
+ * on every row. The spec reserves `remote` for phase 6, the phase that
65
+ * brings the remote transport, and the member joins this union then and
66
+ * not before: a union holding it now would let a row of this phase
67
+ * claim a mode no session of this phase can have.
68
+ */
69
+ export type SessionMode = 'local';
70
+ /**
71
+ * One stats row widened with what attribution answered and with the
72
+ * mode the session ran in — the row the collector's session half writes.
73
+ *
74
+ * Declared here rather than beside the collector that builds it, so the
75
+ * port depends only on the modules the row is made of and never on its
76
+ * own caller. `effort/collect.ts` re-exports it.
77
+ *
78
+ * Attribution reaches the row field by field, and one of its answers
79
+ * stays off it: `issueIdentifierSource`, where the identifier was read.
80
+ * The reconciled schema names the identifier alone.
81
+ *
82
+ * {@link SessionEffortRow.mode} and {@link SessionEffortRow.issueIdentifier}
83
+ * are the reconciled schema's widening, and a row collected before it
84
+ * carries neither. An append-only store never rewrites such a row, and
85
+ * the sibling's own store is made of them: measured on 2026-09-11, not
86
+ * one of its session rows carries either field. The row type is the
87
+ * port's assertion over a row read back, as it is for every other
88
+ * field, so a reader of that store meets the two fields ABSENT, which
89
+ * is neither `local` nor null.
90
+ */
91
+ export interface SessionEffortRow extends SessionStats {
92
+ /** What the session was dispatched to do. */
93
+ kind: SessionKind;
94
+ /** Where the session ran: `local`, on every row of this phase. */
95
+ mode: SessionMode;
96
+ /** The modal branch of the session's records, or null. */
97
+ branch: string | null;
98
+ /** Records carrying it, and how many distinct branches were seen. */
99
+ branchRecordCount: number;
100
+ distinctBranchCount: number;
101
+ /** The branch split; the stub is not yet a plan stub. */
102
+ branchType: string | null;
103
+ branchStub: string | null;
104
+ /** The resolved plan, or null — never the branch stub as a fallback. */
105
+ planStub: string | null;
106
+ planStubMatch: PlanStubMatch;
107
+ /**
108
+ * The issue the session resolved to, upper-cased, or null — never the
109
+ * branch stub as a fallback. Read from the resolved plan stub, else
110
+ * from the whole branch name, by `effort/attribution.ts`.
111
+ */
112
+ issueIdentifier: string | null;
113
+ /** The dispatched task sentence, for a task session only. */
114
+ taskText: string | null;
115
+ /** Index of the enqueue among parsed records, or null if none. */
116
+ enqueueRecordIndex: number | null;
117
+ /** File size at collection; short of the file means a frozen row. */
118
+ sizeBytes: number;
119
+ /** File mtime at collection, ISO 8601. */
120
+ modifiedAt: string;
121
+ /**
122
+ * The worktree the session ran in, as an absolute path, or null for a
123
+ * session of the main checkout. A row collected before the field
124
+ * carries it ABSENT, as the module note says of `mode`; the SQLite
125
+ * backend's `worktree` column (migration `session-worktree`) reads
126
+ * NULL for both.
127
+ */
128
+ worktree: string | null;
129
+ }
130
+ /**
131
+ * One commit's row: the commit parser's answer, stored as parsed.
132
+ *
133
+ * An alias rather than a widening. The commit half appends exactly
134
+ * what `effort/commits.ts` answers, so the row is declared there.
135
+ */
136
+ export type CommitEffortRow = CommitStats;
137
+ /**
138
+ * The row each kind stores — the closed set every per-kind record in
139
+ * this module is typed against.
140
+ *
141
+ * A type alias rather than an interface, on purpose. An interface can
142
+ * be widened from outside this file by declaration merging and a type
143
+ * alias cannot: measured, a module augmentation re-declaring an alias
144
+ * is refused as a duplicate identifier, where the same merge into an
145
+ * interface compiles silently. A kind therefore enters the store here
146
+ * or nowhere, and entering it here is a compile error until
147
+ * {@link EFFORT_KEY_PROJECTIONS} names its key.
148
+ */
149
+ export type EffortRowByKind = {
150
+ sessions: SessionEffortRow;
151
+ commits: CommitEffortRow;
152
+ };
153
+ /** The row kinds a store holds. */
154
+ export type EffortRowKind = keyof EffortRowByKind;
155
+ /** The row one kind stores. */
156
+ export type EffortRow<K extends EffortRowKind> = EffortRowByKind[K];
157
+ /** What one append did. */
158
+ export interface AppendResult {
159
+ /**
160
+ * The file the append targeted, whether or not anything was written
161
+ * to it: the kind's own file, or one shared by every kind, as the
162
+ * backend lays its store out.
163
+ */
164
+ path: string;
165
+ /** Rows appended. Zero writes nothing and creates nothing. */
166
+ appended: number;
167
+ /**
168
+ * Rows whose key was already held, in storage or earlier in the
169
+ * batch. A batch that was not refused has no third outcome, so
170
+ * `appended + skipped` is its length.
171
+ */
172
+ skipped: number;
173
+ }
174
+ /**
175
+ * An effort store holding every row kind, under the rules in the
176
+ * module note.
177
+ *
178
+ * Property signatures rather than method signatures, as the sibling's
179
+ * store interface spelled its own. TypeScript compares a method's
180
+ * parameters bivariantly and a function-typed property's strictly, so
181
+ * a backend whose `keys` accepted only one of the kinds compiles
182
+ * against a method signature and is refused against this one.
183
+ */
184
+ export interface EffortStore {
185
+ /**
186
+ * Appends the rows of one kind the store does not already hold.
187
+ *
188
+ * Throws, having written nothing, when any row of the batch carries
189
+ * no key under {@link EFFORT_KEY_PROJECTIONS}.
190
+ */
191
+ append: <K extends EffortRowKind>(kind: K, rows: readonly EffortRow<K>[]) => AppendResult;
192
+ /**
193
+ * The keys one kind already holds, which is what a collector skips.
194
+ * Read back from the rows, into a fresh set the caller owns.
195
+ */
196
+ keys: (kind: EffortRowKind) => Set<string>;
197
+ /** Every row one kind holds, in the order they were appended. */
198
+ read: <K extends EffortRowKind>(kind: K) => EffortRow<K>[];
199
+ }
200
+ /** Each kind's key projection, typed against that kind's own row. */
201
+ export type EffortKeyProjections = {
202
+ readonly [K in EffortRowKind]: (row: EffortRow<K>) => string | null;
203
+ };
204
+ /**
205
+ * The key each kind is deduplicated by: `sessions` by session id,
206
+ * `commits` by sha.
207
+ *
208
+ * Closed over {@link EffortRowByKind}. A kind added there without a key
209
+ * here is a compile error, and so is a projection reading a field its
210
+ * kind's row does not carry. A backend answers `keys` and dedupes an
211
+ * `append` through this one record, with no projection of its own to
212
+ * substitute, and that is what keeps the set a collector skips by and
213
+ * the set an append dedupes by the same set.
214
+ *
215
+ * A projection answers null for a row that carries no key: the field
216
+ * absent, not a string, or empty. That puts the keyless rule a batch
217
+ * is refused by in one place too. The check is made on the value
218
+ * rather than trusted to the row's type, because a row read back from
219
+ * storage is only as typed as the file it came from. Only the empty
220
+ * string is empty — a key is not trimmed — which is the sibling store's
221
+ * rule, so the two agree on every stored row's key.
222
+ */
223
+ export declare const EFFORT_KEY_PROJECTIONS: EffortKeyProjections;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Cause codes: the `<family>:<leaf>` names of known error contexts, and
3
+ * the checks every list of them passes (#949).
4
+ *
5
+ * A code marks which gate tripped: the family names the gate or tool
6
+ * (`git`, `tsc`, `fs`), the leaf the condition. Two bugs sharing a code
7
+ * may still fail in different things, so a code raises a match score and
8
+ * never decides it.
9
+ *
10
+ * Every family holds `<family>:new-context` without declaring it, for a
11
+ * cause no leaf fits yet; `unknown:new-context` is the last resort when
12
+ * even the family is unknown. Both reserved words are refused as declared
13
+ * leaves, and `unknown` as a declared family, so a listed code always
14
+ * names a real context. `context/error-codes.md` is the page for the list.
15
+ */
16
+ /** How loud an error of the code is. */
17
+ export type ErrorLevel = 'error' | 'warn';
18
+ /** Every level, in the order a refusal names them. */
19
+ export declare const ERROR_LEVELS: readonly ErrorLevel[];
20
+ /** One known error context. */
21
+ export interface ErrorCodeEntry {
22
+ /** `<family>:<leaf>`, both kebab-case. */
23
+ readonly code: string;
24
+ /** One line: what the context is. */
25
+ readonly description: string;
26
+ /** The recommended next action. */
27
+ readonly hint: string;
28
+ /** How loud an error of the code is. */
29
+ readonly level: ErrorLevel;
30
+ /**
31
+ * The version or issue that added the code. rafa's own list always
32
+ * names it ({@link defineErrorCodes} refuses an entry without one); a
33
+ * project's entry may leave it out.
34
+ */
35
+ readonly since?: string;
36
+ /** The spelling an older per-module union gives the same context. */
37
+ readonly legacy?: string;
38
+ }
39
+ /** A declarable code: kebab family, colon, kebab leaf. */
40
+ export declare const CODE_PATTERN: RegExp;
41
+ /** The leaf every family holds for a cause no leaf fits yet. */
42
+ export declare const NEW_CONTEXT_LEAF = "new-context";
43
+ /** The family of the last resort, and a leaf no entry may take. */
44
+ export declare const UNKNOWN_FAMILY = "unknown";
45
+ /** The part of `code` before its colon. */
46
+ export declare function familyOf(code: string): string;
47
+ /** Why `code` cannot be declared, or null when it can. */
48
+ export declare function codeProblem(code: string): string | null;
49
+ /**
50
+ * `entries`, frozen, once each passes the shape check and no code is
51
+ * declared twice. Throws a `TypeError` naming the first fault: the list
52
+ * is source, so a fault is a build error, never a reading to recover.
53
+ */
54
+ export declare function defineErrorCodes(entries: readonly ErrorCodeEntry[]): readonly ErrorCodeEntry[];
55
+ /** True for a declared code, a declared family's new-context, or `unknown:new-context`. */
56
+ export declare function isKnownCode(code: string, entries: readonly ErrorCodeEntry[]): boolean;