@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.
- package/README.md +255 -23
- package/dist/PROMPT.md +18 -1
- package/dist/adapters/logger/settings.d.ts +132 -0
- package/dist/bugs/closest.d.ts +19 -0
- package/dist/bugs/fenced.d.ts +8 -0
- package/dist/bugs/places.d.ts +20 -0
- package/dist/bugs/record.d.ts +145 -0
- package/dist/bugs/title.d.ts +83 -0
- package/dist/bundled/agents/doc-updater.md +17 -3
- package/dist/bundled/agents/loop-implementer.md +14 -2
- package/dist/bundled/operators/skills/rafa-stretch-pit-stop/SKILL.md +10 -3
- package/dist/bundled/operators/skills/rafa-stretch-sweep/SKILL.md +5 -0
- package/dist/bundled/skills/dev-planner/SKILL.md +101 -18
- package/dist/cli.js +74726 -62960
- package/dist/config/entry.d.ts +119 -0
- package/dist/config/index.d.ts +36 -0
- package/dist/config/index.js +51 -0
- package/dist/config/shape.d.ts +232 -0
- package/dist/config-registry.d.ts +127 -0
- package/dist/config-schema-board-project.d.ts +157 -0
- package/dist/config-schema-bugs.d.ts +71 -0
- package/dist/config-schema-errors.d.ts +54 -0
- package/dist/config-schema-features.d.ts +64 -0
- package/dist/config-schema-hub.d.ts +150 -0
- package/dist/config-schema-issues.d.ts +95 -0
- package/dist/config-schema-logger.d.ts +91 -0
- package/dist/config-schema-loop-continue.d.ts +122 -0
- package/dist/config-schema-loop-retries.d.ts +94 -0
- package/dist/config-schema-release.d.ts +307 -0
- package/dist/config-schema-tests.d.ts +170 -0
- package/dist/config-schema-triage.d.ts +142 -0
- package/dist/config-schema-wrap-up.d.ts +96 -0
- package/dist/core/command/command.d.ts +228 -0
- package/dist/core/command/confirm.d.ts +67 -0
- package/dist/core/command/index.d.ts +112 -0
- package/dist/core/command/lazy.d.ts +54 -0
- package/dist/core/command/registry-types.d.ts +96 -0
- package/dist/core/command/running.d.ts +96 -0
- package/dist/core/command/spends.d.ts +39 -0
- package/dist/core/command/types.d.ts +92 -0
- package/dist/core/command/version.d.ts +4 -0
- package/dist/core/config/branch-template.d.ts +20 -0
- package/dist/core/config/config-items.d.ts +86 -0
- package/dist/core/config/config-load.d.ts +47 -0
- package/dist/core/config/config-origins.d.ts +57 -0
- package/dist/core/config/config-readers.d.ts +167 -0
- package/dist/core/config/config-schema.d.ts +207 -0
- package/dist/core/config/config-sections.d.ts +493 -0
- package/dist/core/config/config.d.ts +320 -0
- package/dist/core/config/core-section.d.ts +39 -0
- package/dist/core/config/dangerous.d.ts +58 -0
- package/dist/core/config/default-routing.d.ts +16 -0
- package/dist/core/config/index.d.ts +141 -0
- package/dist/core/config/scope.d.ts +42 -0
- package/dist/core/config/section.d.ts +206 -0
- package/dist/core/config/sections.d.ts +45 -0
- package/dist/core/config/standard-schema.d.ts +27 -0
- package/dist/core/git/commit.d.ts +232 -0
- package/dist/core/git/gh-runner.d.ts +24 -0
- package/dist/core/git/git.d.ts +22 -0
- package/dist/core/git/index.d.ts +85 -0
- package/dist/core/loop/claude.d.ts +388 -0
- package/dist/core/loop/hop-record.d.ts +52 -0
- package/dist/core/loop/index.d.ts +179 -0
- package/dist/core/loop/session-env.d.ts +20 -0
- package/dist/core/loop/session-record-parse.d.ts +63 -0
- package/dist/core/loop/sessions.d.ts +408 -0
- package/dist/core/loop/suite-failure.d.ts +25 -0
- package/dist/core/output/active.d.ts +100 -0
- package/dist/core/output/stream.d.ts +20 -0
- package/dist/core/output/text.d.ts +55 -0
- package/dist/core/plan/block-reader.d.ts +46 -0
- package/dist/core/plan/blocks.d.ts +139 -0
- package/dist/core/plan/declaration.d.ts +366 -0
- package/dist/core/plan/plan-stamp.d.ts +75 -0
- package/dist/core/runtime/identity.d.ts +21 -0
- package/dist/core/runtime/index.d.ts +46 -0
- package/dist/effort/store/bring-forward.d.ts +131 -0
- package/dist/effort/store/check-rows.d.ts +31 -0
- package/dist/effort/store/commit-stats.d.ts +42 -0
- package/dist/effort/store/copy.d.ts +35 -0
- package/dist/effort/store/development-build.d.ts +105 -0
- package/dist/effort/store/effort-levels.d.ts +13 -0
- package/dist/effort/store/findings.d.ts +214 -0
- package/dist/effort/store/fix-schema.d.ts +184 -0
- package/dist/effort/store/index.js +10 -7
- package/dist/effort/store/location.d.ts +44 -0
- package/dist/effort/store/merge-commit-gaps.d.ts +68 -0
- package/dist/effort/store/merge-conflicts.d.ts +91 -0
- package/dist/effort/store/merge-rules.d.ts +82 -0
- package/dist/effort/store/merge-store.d.ts +211 -0
- package/dist/effort/store/merge-union.d.ts +92 -0
- package/dist/effort/store/migrate.d.ts +146 -0
- package/dist/effort/store/migrations.d.ts +92 -0
- package/dist/effort/store/ndjson-files.d.ts +114 -0
- package/dist/effort/store/origins.d.ts +116 -0
- package/dist/effort/store/plan-stubs.d.ts +79 -0
- package/dist/effort/store/rebuild-aside.d.ts +172 -0
- package/dist/effort/store/schema-plan.d.ts +102 -0
- package/dist/effort/store/session-row-types.d.ts +64 -0
- package/dist/effort/store/settings.d.ts +13 -0
- package/dist/effort/store/sqlite.d.ts +340 -0
- package/dist/effort/store/store-generation.d.ts +17 -0
- package/dist/effort/store/store-identity.d.ts +220 -0
- package/dist/effort/store/store-meta.d.ts +305 -0
- package/dist/effort/store/store-replaced.d.ts +72 -0
- package/dist/effort/store/types.d.ts +223 -0
- package/dist/errors/codes.d.ts +56 -0
- package/dist/errors/rafa-codes.d.ts +36 -0
- package/dist/index-2a7nb3an.js +125 -0
- package/dist/{index-r28fzsvc.js → index-39kbcw68.js} +350 -165
- package/dist/{index-nv34ptht.js → index-77ajfffn.js} +14 -9
- package/dist/index-cy98y87p.js +2597 -0
- package/dist/index-gqz4jn6j.js +13447 -0
- package/dist/index-gvek4r8m.js +710 -0
- package/dist/index-j4k2vdz3.js +1125 -0
- package/dist/index-jh1r4kcn.js +1067 -0
- package/dist/{index-mgh2s9bj.js → index-ykggr043.js} +58 -455
- package/dist/index-z1w83f81.js +4 -0
- package/dist/index.js +14715 -21516
- package/dist/learning/index.js +5 -6
- package/dist/plan/index.js +12 -11
- package/dist/plan-prompt.md +3 -1
- package/dist/ports/index.d.ts +732 -0
- package/dist/ports/learning.d.ts +101 -0
- package/dist/ports/log-level.d.ts +13 -0
- package/dist/ports/spec-review.d.ts +66 -0
- package/dist/pr/checks.d.ts +133 -0
- package/dist/pr/types.d.ts +422 -0
- package/dist/ready-4f8edezt.js +2016 -0
- package/dist/release-templates/release-bot-fragment.yml +153 -0
- package/dist/release-templates/release-publish.yml +110 -0
- package/dist/release-templates/release-settle.yml +122 -0
- package/dist/release-templates/release-tag.yml +102 -0
- package/dist/report/parse.d.ts +415 -0
- package/dist/schema/project-id.d.ts +79 -0
- package/dist/schema/tiers.d.ts +107 -0
- package/package.json +41 -3
- package/dist/index-hf2k497d.js +0 -16
- 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;
|