@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,82 @@
1
+ /**
2
+ * What a merge of two effort stores does with each table: the registry
3
+ * every table of the SQLite store has an entry in.
4
+ *
5
+ * ## Scope
6
+ *
7
+ * A `merged` table travels: a merge unions its rows from both stores,
8
+ * one copy of each identity. Each carries `origin_store` and
9
+ * `origin_seq`, since migration `row-origins` or, for a table created
10
+ * after it, from its own migration, with the partial unique index
11
+ * `<table>_by_origin` over the pair, and is one of the
12
+ * `ORIGIN_TABLES` (`origins.ts`). A `local` table never leaves its
13
+ * machine: the migration log, the store's identity row, the ids it
14
+ * replaced and the merge trail describe this one file, and a merge neither reads them from the
15
+ * other store nor writes them from it.
16
+ *
17
+ * ## Identity
18
+ *
19
+ * The columns that make two rows of a merged table the same row, for a
20
+ * row whose origin is NULL, as every row written before `row-origins`
21
+ * or by an older runtime is. A UUID `id` where the table has one, the
22
+ * session id for sessions and dispatches, the sha for commits, and the
23
+ * natural key the table's own UNIQUE constraint or index names for the
24
+ * rest. Two NULLs in an identity column count as equal, as the
25
+ * `ifnull` of `skill_invocations_by_use` counts them.
26
+ *
27
+ * ## Edited fields
28
+ *
29
+ * The store is almost append-only. A column a production statement
30
+ * changes after the insert is an edited field, and the merge has to know
31
+ * which of two stores' values to keep. There are two today.
32
+ * `findings.tracker_ref`, written by `writeTrackerRef`
33
+ * (`tracker-refs.ts`) once a bug is filed, is {@link SET_ONCE}: a
34
+ * filled value beats NULL, and two different filled values mean one
35
+ * finding was filed twice, which the merge keeps in `merge_conflicts`.
36
+ * A reference that supersedes another is a row of its own, never a new
37
+ * value of this field; the `findings` entry says how.
38
+ * `commits.row_json` is {@link RECOMPUTED}: its `minutesSincePrevious`
39
+ * is rewritten by the merge itself (`merge-commit-gaps.ts`) for each
40
+ * commit brought in and the commit after it in time, so two rows of one
41
+ * commit that differ only in that key are the same row.
42
+ *
43
+ * ## What holds the registry to the schema
44
+ *
45
+ * `merge-rules.sweep.test.ts` reads a store brought through every migration
46
+ * and fails on a table with no entry, an entry with no table, a merged
47
+ * table without both origin columns and their partial unique index, an
48
+ * identity or edited column the table lacks, and a production edit of a
49
+ * merged table's column that has no rule here. A migration that adds a
50
+ * table therefore adds its entry in the same commit.
51
+ */
52
+ /** Whether a merge unions a table's rows or leaves the table on its machine. */
53
+ export type MergeScope = 'merged' | 'local';
54
+ /**
55
+ * A filled value beats NULL, and two different filled values are both
56
+ * kept, the incoming one as a conflict.
57
+ */
58
+ export declare const SET_ONCE = "set-once";
59
+ /**
60
+ * The merge recomputes the field from the rows it holds, so two stores'
61
+ * values of it are never compared: for `commits.row_json`, its
62
+ * `minutesSincePrevious` key.
63
+ */
64
+ export declare const RECOMPUTED = "recomputed";
65
+ /** How a merge settles an edited field whose two stores disagree. */
66
+ export type EditRule = typeof SET_ONCE | typeof RECOMPUTED;
67
+ /** A table a merge unions. */
68
+ export interface MergedTableRule {
69
+ readonly scope: 'merged';
70
+ /** The columns two rows with a NULL origin are matched by. */
71
+ readonly identity: readonly string[];
72
+ /** Every column a production statement changes after the insert, with its rule. */
73
+ readonly edited: Readonly<Record<string, EditRule>>;
74
+ }
75
+ /** A table that never leaves its machine. */
76
+ export interface LocalTableRule {
77
+ readonly scope: 'local';
78
+ }
79
+ /** One table's entry in {@link MERGE_RULES}. */
80
+ export type MergeRule = MergedTableRule | LocalTableRule;
81
+ /** Every table of the SQLite store, by name, and what a merge does with it. */
82
+ export declare const MERGE_RULES: Readonly<Record<string, MergeRule>>;
@@ -0,0 +1,211 @@
1
+ /**
2
+ * The merge `rafa effort merge` runs: another device's store file
3
+ * unioned into this store, built beside it, checked, and swapped in
4
+ * behind a whole-file backup.
5
+ *
6
+ * ## What it refuses
7
+ *
8
+ * Every refusal comes before anything is built or copied, so both files
9
+ * keep their bytes. In the order they are asked:
10
+ *
11
+ * - A project whose `store` is `ndjson`: its sessions and commits are
12
+ * NDJSON, which carries no origin, so a merge needs the SQLite
13
+ * backend. The text names {@link MOVE_TO_SQLITE}. The backend is the
14
+ * caller's resolved config, never read here.
15
+ * - A store file absent on either side, and a journal or write-ahead
16
+ * log beside either one (`refuseInFlight`, `rebuild-aside.ts`).
17
+ * - A damaged other file: one SQLite cannot open as a database, or
18
+ * whose `integrity_check` answers anything but `ok`. A plain file
19
+ * copy taken during a write holds a half-written page; a copy made
20
+ * by `rafa effort copy` is consistent.
21
+ * - A store on either side that `planSchema` (`schema-plan.ts`) will
22
+ * not let this rafa write, with the plan's own text.
23
+ * - Another project's store: both stores name a project, and their
24
+ * root commits differ. The project of each is the one its
25
+ * `store_meta` row records; this store, when it has not been
26
+ * minted, takes the one git reads in its directory. A side that
27
+ * names none, such as a store only an older runtime wrote, cannot
28
+ * be told apart and is merged, and the `merges` row then names no
29
+ * other store.
30
+ *
31
+ * Two more are asked only of a swap, since a dry run reads the stores
32
+ * and deletes what it builds, as `effort migrate`'s is:
33
+ *
34
+ * - From a development build, over a store it does not own, with
35
+ * `refuseUnownedDevelopmentWrite`'s text (`development-build.ts`).
36
+ * What it names is what the swap records: the migrations this store
37
+ * has pending, then `merges`, the table the merge writes its row to.
38
+ * - While a loop record under the store's project reads `running` or
39
+ * `paused` with its pid alive (`liveLoop`, `migrate.ts`): renaming
40
+ * the store under a loop loses what the loop writes. The record
41
+ * whose session id `sessionId` names is passed: it is the run
42
+ * pulling at the end of its own task, between task sessions, so
43
+ * nothing of its own writes into the store under the swap. It is
44
+ * matched by session id, never by pid, so a second live record
45
+ * sharing that pid is still refused.
46
+ *
47
+ * ## The other store
48
+ *
49
+ * It is only ever read. It is opened `{ readonly: true }` when it holds
50
+ * every migration this rafa knows. When it lacks some, it is written
51
+ * out with `VACUUM INTO` (`vacuumInto`, `copy.ts`) to a directory of
52
+ * its own under the temporary directory, that copy is brought forward
53
+ * (`bringForward`, `builtAside`), and the copy is what the union reads;
54
+ * the directory is removed once the merge ends, refused or not. A store
55
+ * logging a migration this rafa does not know adds columns the union
56
+ * refuses (`UnionSchemaMismatch`, `merge-union.ts`), and that refusal
57
+ * is passed on.
58
+ *
59
+ * ## The build
60
+ *
61
+ * The parallel file, `<store>.merge-<stamp>`, is this store written out
62
+ * by `vacuumInto` and brought forward there with `builtAside`, each new
63
+ * log row naming this runtime (`appliedByName`). In one transaction on
64
+ * it, the union (`unionStores`) inserts every unmatched row of each
65
+ * merged table, the conflict step (`settleMatches`,
66
+ * `merge-conflicts.ts`) settles each matched pair and records in
67
+ * `merge_conflicts` what no rule settles, the commit gaps of each commit
68
+ * brought in and of the commit after it are recomputed
69
+ * (`recomputeCommitGaps`, `merge-commit-gaps.ts`), and one `merges` row
70
+ * records the merge: its id, the other store's id, when, and the rows
71
+ * added, skipped and in conflict over every merged table.
72
+ *
73
+ * The live store is then attached, and every table both files hold,
74
+ * the log apart, must hold its live row count plus the rows the merge
75
+ * added to it (`checkedCounts`): the rows added to each merged table,
76
+ * one row in `merges`, and one in `merge_conflicts` per conflict. A
77
+ * merged table's fills and gap rewrites change no count. SQLite's
78
+ * `integrity_check` must answer `ok`, and `planSchema` must find the
79
+ * file one this rafa uses as it is (`refuseUnusable`, `fix-schema.ts`).
80
+ *
81
+ * Under `dryRun` the parallel file is then deleted, and the answer says
82
+ * what the merge would have done. Otherwise the parallel file is
83
+ * swapped in (`swapIn`, `rebuild-aside.ts`): this store is written out
84
+ * to `<store>.before-merge-<stamp>.bak` with `VACUUM INTO`, its identity
85
+ * is carried onto the parallel file (`carryStoreIdentity`,
86
+ * `store-meta.ts`), and the parallel file is renamed into its place.
87
+ *
88
+ * ## The store keeps its id
89
+ *
90
+ * `store_meta` is `local` in `MERGE_RULES`, so the parallel file holds
91
+ * this store's own row and never the other store's. The carry gives it
92
+ * the device, inode and generation the rename brings to this store's
93
+ * path, so the merged store keeps this store's origin, and the rows it
94
+ * writes next carry the same store id. A store whose next write would
95
+ * have minted anyway, being a copy already, is carried nothing and
96
+ * mints on its first write after the swap.
97
+ *
98
+ * The backup holds every row the original held, and restoring it is
99
+ * renaming it back. It is a copy with an inode of its own, so once
100
+ * renamed back over the store its first writing open mints a new
101
+ * origin, as any restored copy must (`store-identity.ts`).
102
+ *
103
+ * ## What the counts leave out
104
+ *
105
+ * An unmatched incoming row the union could not insert, because it
106
+ * collides with a row here on another UNIQUE key, is answered per table
107
+ * as `collided` and kept in the other file only: it is neither added,
108
+ * skipped nor recorded, since `merge_conflicts` names a local row and
109
+ * the union does not know which one it collided with.
110
+ *
111
+ * It opens both stores itself rather than through `withSqliteStore`, so
112
+ * it runs the test guard (`guardTestProcess`, `location.ts`) itself,
113
+ * over both paths, before it reads anything.
114
+ */
115
+ import type { DevelopmentProbe } from './development-build.js';
116
+ import type { SqliteMigration } from './migrations.js';
117
+ import type { ProjectIdentity } from './store-identity.js';
118
+ import type { StoreBackend } from '../../core/config/sections.js';
119
+ import { RebuildRefusal } from './rebuild-aside.js';
120
+ /** The command that moves an NDJSON project's rows into the SQLite store. */
121
+ export declare const MOVE_TO_SQLITE = "rafa effort move --to=sqlite";
122
+ /** Why a merge was refused before anything was built. */
123
+ export type MergeRefusalReason =
124
+ /** The project's `store` is `ndjson`. */
125
+ 'ndjson'
126
+ /** A store file is absent. */
127
+ | 'missing'
128
+ /** The other file is no SQLite database, or fails `integrity_check`. */
129
+ | 'damaged'
130
+ /** A store's schema is one this rafa will not write. */
131
+ | 'schema'
132
+ /** The two stores name different projects. */
133
+ | 'other-project'
134
+ /** A loop records to this store. */
135
+ | 'live-loop';
136
+ /** A merge refused before either store was changed. */
137
+ export declare class MergeRefusal extends RebuildRefusal {
138
+ readonly reason: MergeRefusalReason;
139
+ constructor(reason: MergeRefusalReason, message: string);
140
+ }
141
+ /** What the run did. */
142
+ export type MergeStatus =
143
+ /** Under `dryRun`: the merged store was built, checked and deleted. */
144
+ 'would-merge'
145
+ /** The merged store was swapped in and the original kept as a backup. */
146
+ | 'merged';
147
+ /** What the merge did with one merged table's incoming rows. */
148
+ export interface TableMerge {
149
+ readonly table: string;
150
+ /** Rows inserted with a new local `seq`. */
151
+ readonly added: number;
152
+ /** Matched rows left with equal content, fills included. */
153
+ readonly skipped: number;
154
+ /** `merge_conflicts` rows recorded. */
155
+ readonly inConflict: number;
156
+ /** Set-once fields this store took from the other. */
157
+ readonly filled: number;
158
+ /** Unmatched rows another UNIQUE key refused; see the module note. */
159
+ readonly collided: number;
160
+ }
161
+ /** The outcome of one run. */
162
+ export interface MergeResult {
163
+ /** This store's file. */
164
+ readonly path: string;
165
+ /** The other store's file. */
166
+ readonly otherPath: string;
167
+ readonly status: MergeStatus;
168
+ /** The `merges.id` the merge recorded. */
169
+ readonly mergeId: string;
170
+ /** The other store's id, or null when it was never minted. */
171
+ readonly otherStore: string | null;
172
+ /** What the other store's copy was brought forward through; empty when it was read as it is. */
173
+ readonly otherBroughtForward: readonly string[];
174
+ /** One entry per merged table, in `MERGE_RULES` order. */
175
+ readonly tables: readonly TableMerge[];
176
+ readonly rowsAdded: number;
177
+ readonly rowsSkipped: number;
178
+ readonly rowsInConflict: number;
179
+ /** Commit rows whose gap was rewritten. */
180
+ readonly gapsRewritten: number;
181
+ /** Where the original went, for `merged` only. */
182
+ readonly backupPath: string | null;
183
+ }
184
+ /** What one run is handed; the {@link DevelopmentProbe} fields say which build runs and which stores it owns. */
185
+ export interface MergeOptions extends DevelopmentProbe {
186
+ /** This store's file. */
187
+ readonly path: string;
188
+ /** The other store's file, only ever read. */
189
+ readonly otherPath: string;
190
+ /** The project's resolved `store` backend. */
191
+ readonly backend: StoreBackend;
192
+ /** Build and check the merged store, then delete it instead of swapping it in. */
193
+ readonly dryRun: boolean;
194
+ /** Names the parallel and backup files, so two runs never collide. */
195
+ readonly stamp: string;
196
+ /** The catalogue both stores are brought to: this build's unless a test passes another. */
197
+ readonly migrations?: readonly SqliteMigration[];
198
+ /** The clock `merged_at` and new log rows read. */
199
+ readonly now?: () => Date;
200
+ /** A new merge id; `crypto.randomUUID` when absent. */
201
+ readonly newMergeId?: () => string;
202
+ /** The project of the repository holding `dir`, for an unminted store; `readProjectIdentity` when absent. */
203
+ readonly readProject?: (dir: string) => ProjectIdentity;
204
+ /**
205
+ * The session id of the loop run merging, whose own live record the
206
+ * live-loop guard passes; absent or null for a caller that is no run.
207
+ */
208
+ readonly sessionId?: string | null;
209
+ }
210
+ /** Merges the store at `options.otherPath` into the one at `options.path`. See the module note. */
211
+ export declare function mergeStore(options: MergeOptions): MergeResult;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The union at the heart of a merge: every row of each `merged` table in
3
+ * another store that matches no row here is inserted here, so the two
4
+ * stores' rows end up in this one, one copy of each.
5
+ *
6
+ * ## Matching
7
+ *
8
+ * An incoming row is looked up here in two steps, and the first to find
9
+ * a row wins:
10
+ *
11
+ * 1. **The origin pair.** An incoming row whose `origin_store` and
12
+ * `origin_seq` are both set matches the row here holding the same
13
+ * pair. The pair is unique wherever it is set (`<table>_by_origin`),
14
+ * so at most one row answers.
15
+ * 2. **Identity.** Otherwise the row matches the row here holding the
16
+ * same values in the table's identity columns from `MERGE_RULES`
17
+ * (`merge-rules.ts`): `session_id` for sessions and dispatches, `sha`
18
+ * for commits, `id` for the UUID tables, `(pr, head_sha, read_at)`
19
+ * for `plan_ci`. Two NULLs count as equal, as `IS` compares them.
20
+ *
21
+ * Step 2 is what a row with a NULL origin, on either side, is matched
22
+ * by. It also runs when both rows carry a pair and the pairs differ,
23
+ * because every identity is a UNIQUE key of its table: a commit two
24
+ * devices collected from one pushed history holds one sha under two
25
+ * origins, and the table could not hold both. Such a match, and one on
26
+ * a pair whose two rows differ, is what the merge's conflict step reads
27
+ * the {@link UnionMatch} list for; the union itself compares no content.
28
+ *
29
+ * ## Inserting
30
+ *
31
+ * An unmatched row is inserted with a new local `seq`, named in the
32
+ * insert as `COALESCE(MAX(seq), 0) + 1` for the reason `origins.ts`
33
+ * gives, and every other column as the other store holds it: its origin
34
+ * pair unchanged, NULL staying NULL. Rows go in in the other store's
35
+ * `seq` order, and each lookup reads the rows inserted before it, so two
36
+ * rows of one identity in the other store land as one.
37
+ *
38
+ * An unmatched row can still collide with a row here on one of the
39
+ * table's other UNIQUE keys, such as `findings_by_artifact`, when one
40
+ * report was stored under two UUIDs. Nothing in `MERGE_RULES` says which
41
+ * row that is, so the row is not inserted and is returned as a
42
+ * {@link UnionCollision}; the union goes on with the next row.
43
+ *
44
+ * ## What it refuses
45
+ *
46
+ * Both stores must hold the same columns in every merged table, which
47
+ * the merge sees to by bringing both to the same migrations first. A
48
+ * column one side lacks would be dropped from every row copied or read
49
+ * as NULL, so the union checks every table before it inserts anything
50
+ * and throws {@link UnionSchemaMismatch} naming the columns. The inserts
51
+ * run in one transaction on `local`; `other` is only read.
52
+ */
53
+ import type { Database } from 'bun:sqlite';
54
+ /** Which of the two lookups found an incoming row's match. */
55
+ export type MatchedBy = 'origin' | 'identity';
56
+ /** An incoming row that matched a row already here, which the union left alone. */
57
+ export interface UnionMatch {
58
+ /** The local row's `seq`. */
59
+ readonly localSeq: number;
60
+ /** The incoming row's `seq` in the other store. */
61
+ readonly incomingSeq: number;
62
+ readonly by: MatchedBy;
63
+ }
64
+ /** An unmatched incoming row the table refused on another of its UNIQUE keys. */
65
+ export interface UnionCollision {
66
+ /** The incoming row's `seq` in the other store. */
67
+ readonly incomingSeq: number;
68
+ /** SQLite's own message, naming the key. */
69
+ readonly reason: string;
70
+ }
71
+ /** What the union did with one merged table. */
72
+ export interface TableUnion {
73
+ readonly table: string;
74
+ /** The local `seq` of each row inserted, in insert order. */
75
+ readonly added: readonly number[];
76
+ readonly matched: readonly UnionMatch[];
77
+ readonly collided: readonly UnionCollision[];
78
+ }
79
+ /** The two stores disagree on the columns of a merged table. */
80
+ export declare class UnionSchemaMismatch extends Error {
81
+ constructor(message: string);
82
+ }
83
+ /** The columns of `table` other than `seq`, in table order. */
84
+ export declare function carriedColumns(db: Database, table: string): string[];
85
+ /**
86
+ * Inserts into `local` every row of each merged table of `other` that
87
+ * matches no row of `local`, as the module note says, and answers what
88
+ * it did with each table, in `MERGE_RULES` order. Throws
89
+ * {@link UnionSchemaMismatch}, having inserted nothing, when the two
90
+ * stores hold different columns in any merged table.
91
+ */
92
+ export declare function unionStores(local: Database, other: Database): TableUnion[];
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The forward move `rafa effort migrate` runs: a SQLite store's pending
3
+ * migrations, breaking ones included, applied to a copy built beside it,
4
+ * checked, and swapped in behind a whole-file backup.
5
+ *
6
+ * ## What it applies
7
+ *
8
+ * The decision is `planSchema`'s (`schema-plan.ts`), asked for a write
9
+ * by the `migrate` caller, the one caller a pending breaking migration
10
+ * does not refuse. The store's `user_version` and migration log are read
11
+ * on a read-only connection, so a store with nothing to write keeps its
12
+ * bytes.
13
+ *
14
+ * - A store with nothing to adopt or apply is `current`, and left alone.
15
+ * - A store `planSchema` refuses is refused with the plan's own text,
16
+ * which ends with the command to run next. `breaking-pending` never
17
+ * comes back, since this is the caller it names.
18
+ * - Otherwise the store is brought forward aside: adopted when it has
19
+ * no log, then every pending migration applied in catalogue order.
20
+ *
21
+ * ## The build
22
+ *
23
+ * The parallel file, `<store>.migrate-<stamp>`, is the live store written
24
+ * out by `VACUUM INTO` on a read-only connection (`vacuumInto`,
25
+ * `copy.ts`), which keeps `user_version` and the log. It is brought
26
+ * forward there by the log-aware apply (`bringForward`,
27
+ * `bring-forward.ts`) as the `migrate` caller, each row's `applied_by`
28
+ * naming this runtime (`appliedByName`, `development-build.ts`). The live
29
+ * store is then attached, and the row count of every table both files
30
+ * hold, the log apart, must equal the live one (`checkedCounts`): a
31
+ * breaking table rebuild that lost a row is refused there. SQLite's
32
+ * `integrity_check` must answer `ok`, and `planSchema` must find the file
33
+ * one this rafa uses as it is (`refuseUnusable`, `fix-schema.ts`).
34
+ *
35
+ * Under `dryRun` the parallel file is then deleted. Otherwise the live
36
+ * store is written out to `<store>.before-<id>-<stamp>.bak` with
37
+ * `VACUUM INTO`, its identity is carried onto the parallel file
38
+ * (`carryStoreIdentity`, `store-meta.ts`), and the parallel file is
39
+ * renamed into its place. `<id>` is the first migration applied, or
40
+ * `schema_migrations` when the run only adopts the store.
41
+ *
42
+ * The migrated store keeps the live store's origin, so the rows it
43
+ * writes next carry the same store id; a store whose next write would
44
+ * have minted anyway is carried nothing and mints after the swap. The
45
+ * backup holds every row the original held, and restoring it is
46
+ * renaming it back. It is a copy with an inode of its own, so once
47
+ * renamed back over the store its first writing open mints a new
48
+ * origin (`store-identity.ts`).
49
+ *
50
+ * Those steps, from the in-flight refusal to the swap and the removal of
51
+ * the parallel file on any failure, are `rebuild-aside.ts`'s, the same
52
+ * convention `fix-schema.ts` runs with its own file names.
53
+ *
54
+ * ## When the swap is refused
55
+ *
56
+ * Both refusals come before anything is built, and leave the directory
57
+ * as it was. A dry run is asked neither, since it only reads the store
58
+ * and deletes what it writes.
59
+ *
60
+ * - From a development build, unless the store is one it owns: under
61
+ * the temporary directory or `RAFA_EFFORT_DIR`, which is where a copy
62
+ * `rafa effort copy` made sits. The question and its text are
63
+ * `refuseUnownedDevelopmentWrite`'s, the one every open asks.
64
+ * - While a loop record under the store's project root reads `running`
65
+ * or `paused` with its pid alive (`readSessions`,
66
+ * `packages/rafa/src/core/loop/sessions.ts`): renaming the store under a loop loses
67
+ * what it writes. The spec's text names the first breaking migration:
68
+ * `REFUSED — migration <id> breaks older runtimes, and loop <sessionId>
69
+ * (pid <pid>, plan <stub>) is running on this store. Nothing was
70
+ * migrated. Finish or stop that loop, then run it again.` With only
71
+ * additive migrations pending it names the loop and the lost writes
72
+ * instead. A store that is not a project's own `.rafa/effort/` file,
73
+ * a copy, is named by no loop record, since `loop start` refuses to
74
+ * run with `RAFA_EFFORT_DIR` set.
75
+ *
76
+ * It opens the store itself rather than through `withSqliteStore`, so it
77
+ * runs the test guard (`guardTestProcess`, `location.ts`) itself, before
78
+ * it reads or makes anything.
79
+ */
80
+ import type { DevelopmentProbe } from './development-build.js';
81
+ import type { SqliteMigration } from './migrations.js';
82
+ import type { TableRows } from './rebuild-aside.js';
83
+ import type { LoggedMigration } from './schema-plan.js';
84
+ import type { SessionRecord } from '../../core/loop/index.js';
85
+ /** What the run found, and what it did about it. */
86
+ export type MigrateStatus =
87
+ /** No store file: nothing to migrate, and none is created. */
88
+ 'missing'
89
+ /** Nothing to adopt or apply: nothing to migrate. */
90
+ | 'current'
91
+ /** Under `dryRun`: the migrated store was built, checked and deleted. */
92
+ | 'would-migrate'
93
+ /** The migrated store was swapped in and the original kept as a backup. */
94
+ | 'migrated';
95
+ /** The outcome of one run. */
96
+ export interface MigrateResult {
97
+ /** The live store's file. */
98
+ readonly path: string;
99
+ readonly status: MigrateStatus;
100
+ /** The live store's `user_version`, or null when there is no file. */
101
+ readonly storeVersion: number | null;
102
+ /** Whether the live store held a migration log. */
103
+ readonly logged: boolean;
104
+ /** What the run records: `schema_migrations` first when it adopts, then each pending id. */
105
+ readonly applying: readonly string[];
106
+ /** The pending ids that break older runtimes, in catalogue order. */
107
+ readonly breaking: readonly string[];
108
+ /** Logged migrations this rafa does not know; each is one this rafa uses the store through. */
109
+ readonly unknown: readonly LoggedMigration[];
110
+ /** Who the new log rows name as applying them. */
111
+ readonly appliedBy: string;
112
+ /** The `user_version` the migrated store holds, or null when nothing was built. */
113
+ readonly userVersion: number | null;
114
+ /** Every table both files hold, with the rows each holds; empty when nothing was built. */
115
+ readonly counted: readonly TableRows[];
116
+ /** Where the original went, for `migrated` only. */
117
+ readonly backupPath: string | null;
118
+ }
119
+ /** What one run is handed; the {@link DevelopmentProbe} fields say which build runs and which stores it owns. */
120
+ export interface MigrateOptions extends DevelopmentProbe {
121
+ /** The live store's file. */
122
+ readonly path: string;
123
+ /** Build and check the migrated store, then delete it instead of swapping it in. */
124
+ readonly dryRun: boolean;
125
+ /** Names the parallel and backup files, so two runs never collide. */
126
+ readonly stamp: string;
127
+ /** The catalogue to migrate to: this build's unless a test passes a synthetic tail. */
128
+ readonly migrations?: readonly SqliteMigration[];
129
+ /** The clock the new log rows' `applied_at` is read from. */
130
+ readonly now?: () => Date;
131
+ }
132
+ /** A run refused before the live store was changed: the build-aside steps' refusal. */
133
+ export { RebuildRefusal as MigrateRefusal } from './rebuild-aside.js';
134
+ /**
135
+ * The first loop record under the store's project root that reads
136
+ * `running` or `paused`, if any, passing the record whose session id is
137
+ * `ownSessionId`: the run asking, which a pull at the end of its own task
138
+ * names (`merge-store.ts`). A record is passed by its session id alone,
139
+ * never by its pid, so another live record sharing the caller's pid is
140
+ * still answered. Records that cannot be read refuse, the text ending
141
+ * with `nothingDone`, the caller's sentence for what it left undone; the
142
+ * merge asks this too, and only the merge names a session id.
143
+ */
144
+ export declare function liveLoop(path: string, options: DevelopmentProbe, nothingDone?: string, ownSessionId?: string | null): SessionRecord | undefined;
145
+ /** Migrates the store at `options.path`. See the module note. */
146
+ export declare function migrateStore(options: MigrateOptions): MigrateResult;
@@ -0,0 +1,92 @@
1
+ /** What an older runtime can no longer do once a migration has run. */
2
+ export type MigrationBreak = 'readers' | 'writers';
3
+ /** Why a breaking migration breaks, and what prepared the way for it. */
4
+ export interface MigrationContract {
5
+ /**
6
+ * The id of the additive migration that shipped the new shape in an
7
+ * earlier release, or null when no expand step was needed.
8
+ */
9
+ readonly expand: string | null;
10
+ /** Why the change cannot be additive. */
11
+ readonly why: string;
12
+ }
13
+ /** A migration as rafa decides about it, whatever backend runs it. */
14
+ export interface MigrationSpec {
15
+ /** Kebab-case and unique for all time: the key a store records it by. */
16
+ readonly id: string;
17
+ /** `[]` for an additive migration; otherwise what it breaks. */
18
+ readonly breaks: readonly MigrationBreak[];
19
+ /** Present exactly when `breaks` is not empty. */
20
+ readonly contract?: MigrationContract;
21
+ }
22
+ /** The one migration type that carries SQL. */
23
+ export interface SqliteMigration extends MigrationSpec {
24
+ /** DDL only, frozen once shipped; `migrations.lock.json` holds its sha256. */
25
+ readonly sql: string;
26
+ }
27
+ /**
28
+ * The `user_version` a store holds while every migration it has applied
29
+ * is additive: the length of the history 0.23.0 to 0.24.1 hold, and so
30
+ * the last version those releases accept.
31
+ */
32
+ export declare const LEGACY_GATE_OPEN = 13;
33
+ /**
34
+ * The `user_version` a store holds once a breaking migration is applied:
35
+ * past any version a pre-log release counts to, so every such release
36
+ * refuses the store.
37
+ */
38
+ export declare const LEGACY_GATE_CLOSED = 1000;
39
+ /**
40
+ * The schema's history, by name. This module's note says why an entry
41
+ * is frozen, what its `breaks` declares, and who still reads the first
42
+ * thirteen by position.
43
+ */
44
+ export declare const SQLITE_MIGRATIONS: readonly SqliteMigration[];
45
+ /**
46
+ * The length of the history. `migrateSchema` in `sqlite.ts`, which
47
+ * tests plant pre-log stores with, brings a store to this
48
+ * `user_version` and refuses one past it, as the pre-log releases do.
49
+ */
50
+ export declare const SQLITE_SCHEMA_VERSION: number;
51
+ /**
52
+ * The `user_version` a store is given when it holds `applied`. The
53
+ * value is {@link LEGACY_GATE_OPEN} when every entry declares no
54
+ * `breaks`, and {@link LEGACY_GATE_CLOSED} otherwise. Any word counts
55
+ * as a break, including one this rafa does not know, because a pre-log
56
+ * release can use the store only when nothing in it breaks.
57
+ */
58
+ export declare function legacyGate(applied: readonly {
59
+ readonly breaks: readonly string[];
60
+ }[]): number;
61
+ /** The sha256 of a migration's SQL, as lowercase hex over its UTF-8 bytes. */
62
+ export declare function migrationChecksum(migration: Pick<SqliteMigration, 'sql'>): string;
63
+ /** The lock file's shape: each migration's sha256, keyed by its id. */
64
+ export type MigrationLock = Readonly<Record<string, string>>;
65
+ /** One way the migrations and their lock disagree. */
66
+ export type LockMismatch =
67
+ /** The entry's SQL hashes to something other than its lock line. */
68
+ {
69
+ readonly kind: 'edited';
70
+ readonly id: string;
71
+ readonly locked: string;
72
+ readonly computed: string;
73
+ }
74
+ /** The entry has no lock line. */
75
+ | {
76
+ readonly kind: 'unlocked';
77
+ readonly id: string;
78
+ readonly computed: string;
79
+ }
80
+ /** The lock line has no entry, so a migration was removed or renamed. */
81
+ | {
82
+ readonly kind: 'missing';
83
+ readonly id: string;
84
+ readonly locked: string;
85
+ };
86
+ /**
87
+ * Every way `migrations` and `lock` disagree. Entries come first, in
88
+ * array order, followed by the lock lines that no entry matches, in
89
+ * lock order. An empty answer means the lock holds exactly these
90
+ * entries, each with the checksum of its SQL.
91
+ */
92
+ export declare function lockMismatches(migrations: readonly SqliteMigration[], lock: MigrationLock): readonly LockMismatch[];