rvlib-shipkit 0.38.0 → 1.0.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 (252) hide show
  1. package/CHANGELOG.md +371 -16
  2. package/README.md +26 -14
  3. package/package.json +2 -2
  4. package/src/assets/gen/comfyts-gen.ts +27 -10
  5. package/src/assets/gen/generate-roll.ts +1 -1
  6. package/src/assets/kit-config.ts +6 -6
  7. package/src/assets/run-assets.ts +14 -10
  8. package/src/assets/server/studio-server.ts +6 -5
  9. package/src/check.ts +343 -0
  10. package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
  11. package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
  12. package/src/checks/SK003-claude-md-shape.check.ts +66 -0
  13. package/src/checks/SK004-gitignore-policy.check.ts +309 -0
  14. package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
  15. package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
  16. package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
  17. package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
  18. package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
  19. package/src/checks/SK010-undeclared-unit.check.ts +111 -0
  20. package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
  21. package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
  22. package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
  23. package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
  24. package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
  25. package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
  26. package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
  27. package/src/checks/SK018-test-files-run.check.ts +28 -0
  28. package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
  29. package/src/checks/SK020-self-spawning-check.check.ts +147 -0
  30. package/src/checks/contracts.check.ts +20 -0
  31. package/src/cli/cmd_Asks.ts +3 -2
  32. package/src/cli/cmd_Assets.ts +2 -2
  33. package/src/cli/cmd_Bump.ts +1 -1
  34. package/src/cli/cmd_Check.ts +6 -6
  35. package/src/cli/cmd_Checks.ts +95 -0
  36. package/src/cli/cmd_Ci.ts +48 -13
  37. package/src/cli/cmd_Commands.ts +2 -6
  38. package/src/cli/cmd_ConfigKeys.ts +69 -0
  39. package/src/cli/cmd_Contracts.ts +1 -1
  40. package/src/cli/cmd_Fleet.ts +6 -2
  41. package/src/cli/cmd_Hooks.ts +1 -1
  42. package/src/cli/cmd_Keys.ts +43 -18
  43. package/src/cli/cmd_Mv.ts +2 -2
  44. package/src/cli/cmd_Prose.ts +10 -1
  45. package/src/cli/cmd_Releases.ts +2 -2
  46. package/src/cli/cmd_Serve.ts +1 -1
  47. package/src/cli/cmd_Start.ts +6 -6
  48. package/src/cli/cmd_Status.ts +25 -0
  49. package/src/cli/cmd_Tui.ts +2 -2
  50. package/src/cli.ts +7 -4
  51. package/src/collections/actions.ts +4 -4
  52. package/src/collections/checks.ts +258 -0
  53. package/src/collections/collection-types.ts +254 -1
  54. package/src/collections/comfy.ts +23 -0
  55. package/src/collections/md-body.ts +14 -0
  56. package/src/collections/pings.ts +38 -12
  57. package/src/collections/posts.ts +19 -3
  58. package/src/collections/providers.ts +198 -91
  59. package/src/collections/reflections.ts +41 -0
  60. package/src/collections/state-rules.ts +73 -0
  61. package/src/collections/test-time.ts +288 -0
  62. package/src/collections/tests.ts +181 -0
  63. package/src/collections/wikis.ts +66 -0
  64. package/src/comfy/comfy-panel.ts +292 -0
  65. package/src/contracts/apply-contracts.ts +17 -12
  66. package/src/contracts/contract-types.ts +16 -4
  67. package/src/contracts/contracts-engine.ts +78 -42
  68. package/src/contracts/manifests/discord.contract.ts +6 -6
  69. package/src/contracts/manifests/identity.contract.ts +3 -3
  70. package/src/contracts/manifests/itch.contract.ts +12 -15
  71. package/src/contracts/manifests/social.contract.ts +5 -5
  72. package/src/contracts/manifests/steam.contract.ts +16 -16
  73. package/src/contracts/run-contracts.ts +4 -3
  74. package/src/core/attach.ts +96 -0
  75. package/src/core/canonical-scripts.ts +25 -0
  76. package/src/core/check-ctx.ts +204 -14
  77. package/src/core/check-file-names.ts +112 -0
  78. package/src/core/check-pass.ts +84 -0
  79. package/src/core/check-runner.ts +796 -0
  80. package/src/core/config-keys.ts +256 -0
  81. package/src/core/config-rewrite.ts +77 -0
  82. package/src/core/discover-checks.ts +166 -90
  83. package/src/core/discover-legacy-checks.ts +26 -0
  84. package/src/core/doc-block.ts +13 -0
  85. package/src/core/floor-resolve.ts +264 -0
  86. package/src/core/floor.ts +20 -137
  87. package/src/core/git-bin.ts +24 -0
  88. package/src/core/git-root.ts +108 -0
  89. package/src/core/instance.ts +195 -0
  90. package/src/core/jobs.ts +118 -0
  91. package/src/core/layout.ts +52 -0
  92. package/src/core/legacy-shapes.ts +207 -0
  93. package/src/core/lib-root.ts +5 -0
  94. package/src/core/locked-write.ts +110 -39
  95. package/src/core/machine-commit.ts +5 -4
  96. package/src/core/migrate-layout.ts +557 -0
  97. package/src/core/motherhome.ts +2 -8
  98. package/src/core/nesting.ts +4 -12
  99. package/src/core/one-way-in-policy.ts +73 -17
  100. package/src/core/outcome-words.ts +45 -0
  101. package/src/core/package-manager.ts +86 -0
  102. package/src/core/parallel.ts +21 -0
  103. package/src/core/process-tree.ts +44 -0
  104. package/src/core/raw-access.ts +93 -0
  105. package/src/core/read-set.ts +152 -0
  106. package/src/core/run-check.ts +155 -213
  107. package/src/core/run-ci.ts +153 -189
  108. package/src/core/run-fix.ts +96 -0
  109. package/src/core/run-fixers.ts +53 -30
  110. package/src/core/self-update.ts +9 -3
  111. package/src/core/slots.ts +83 -0
  112. package/src/core/spawn-timeout.ts +34 -3
  113. package/src/core/when-block.ts +401 -0
  114. package/src/core/when.ts +135 -0
  115. package/src/core/write-status.ts +5 -2
  116. package/src/hooks/leak-guard.ts +10 -8
  117. package/src/hooks/one-way-in-guard.ts +13 -5
  118. package/src/hooks/root-identity.ts +2 -1
  119. package/src/hooks/run-hooks.ts +35 -23
  120. package/src/init-repo.ts +39 -23
  121. package/src/ledger/check-stats.ts +155 -0
  122. package/src/ledger/check-table.ts +179 -0
  123. package/src/ledger/db.ts +255 -0
  124. package/src/ledger/gate-checks.ts +433 -0
  125. package/src/ledger/git-key.ts +116 -77
  126. package/src/ledger/git-scan.ts +96 -35
  127. package/src/ledger/ledger.ts +116 -42
  128. package/src/ledger/live-runs.ts +111 -0
  129. package/src/ledger/live-timeline.ts +97 -0
  130. package/src/ledger/repo-view.ts +73 -0
  131. package/src/ledger/run-log.ts +448 -0
  132. package/src/ledger/run-timeline.ts +173 -0
  133. package/src/ledger/store.ts +23 -12
  134. package/src/ledger/suite-log.ts +161 -0
  135. package/src/ledger/toolchain-identity.ts +7 -1
  136. package/src/ledger/world-cache.ts +111 -0
  137. package/src/mv/mv-md.ts +16 -6
  138. package/src/problems/extract.ts +182 -0
  139. package/src/problems/problem-board.ts +37 -0
  140. package/src/problems/problem-line.ts +77 -0
  141. package/src/prose/prose-view.ts +90 -0
  142. package/src/prose/run-prose.ts +60 -19
  143. package/src/prose/ste.ts +140 -24
  144. package/src/reflections/reflections-queue.ts +81 -0
  145. package/src/release/git-facts.ts +15 -9
  146. package/src/release/run-bump.ts +6 -5
  147. package/src/release/run-release.ts +25 -11
  148. package/src/release/run-releases.ts +2 -2
  149. package/src/release/unit-gate.ts +7 -54
  150. package/src/release/unit-resolve.ts +282 -0
  151. package/src/release/units.ts +33 -217
  152. package/src/repo-config.ts +155 -85
  153. package/src/rules/rule.ts +36 -96
  154. package/src/rules/untrack-generated.ts +5 -4
  155. package/src/scaffold/files/vscode-tasks.json +10 -31
  156. package/src/scaffold/jsonc-spans.ts +113 -0
  157. package/src/scaffold/project-scaffold.ts +16 -43
  158. package/src/scan.ts +98 -0
  159. package/src/serve/gate-trigger.ts +148 -0
  160. package/src/serve/health-schedule.ts +102 -0
  161. package/src/serve/hub-app/chart-view.ts +179 -0
  162. package/src/serve/hub-app/check-view.tsx +345 -0
  163. package/src/serve/hub-app/check-words.ts +164 -0
  164. package/src/serve/hub-app/checks-svc.ts +280 -0
  165. package/src/serve/hub-app/clock-svc.ts +33 -0
  166. package/src/serve/hub-app/col-card.tsx +23 -39
  167. package/src/serve/hub-app/cols-svc.ts +44 -20
  168. package/src/serve/hub-app/comfy-svc.ts +171 -0
  169. package/src/serve/hub-app/comfy-view.tsx +81 -0
  170. package/src/serve/hub-app/dur.ts +41 -0
  171. package/src/serve/hub-app/hash-route.ts +121 -0
  172. package/src/serve/hub-app/header-bar.tsx +117 -0
  173. package/src/serve/hub-app/health-svc.ts +23 -28
  174. package/src/serve/hub-app/help.tsx +29 -0
  175. package/src/serve/hub-app/hub-app.tsx +122 -119
  176. package/src/serve/hub-app/hub-st.ts +115 -42
  177. package/src/serve/hub-app/hub-types.ts +117 -2
  178. package/src/serve/hub-app/hub.css +534 -192
  179. package/src/serve/hub-app/index.html +2 -0
  180. package/src/serve/hub-app/live-svc.ts +174 -0
  181. package/src/serve/hub-app/md.ts +15 -0
  182. package/src/serve/hub-app/media-svc.ts +4 -1
  183. package/src/serve/hub-app/media-view.tsx +15 -0
  184. package/src/serve/hub-app/next-run.ts +68 -0
  185. package/src/serve/hub-app/project-gutter.css +65 -0
  186. package/src/serve/hub-app/project-gutter.tsx +128 -0
  187. package/src/serve/hub-app/projects-svc.ts +226 -0
  188. package/src/serve/hub-app/prose-hits-st.ts +124 -0
  189. package/src/serve/hub-app/prose-hits-view.tsx +176 -0
  190. package/src/serve/hub-app/prose-hits.css +21 -0
  191. package/src/serve/hub-app/run-strip.ts +47 -0
  192. package/src/serve/hub-app/runs-tabs.ts +24 -0
  193. package/src/serve/hub-app/test-view.tsx +107 -0
  194. package/src/serve/hub-app/tests-svc.ts +62 -0
  195. package/src/serve/hub-app/tests-view.tsx +238 -0
  196. package/src/serve/hub-app/time-ago.tsx +34 -0
  197. package/src/serve/hub-app/timeline-chart.tsx +274 -0
  198. package/src/serve/hub-app/timeline-geometry.ts +314 -0
  199. package/src/serve/hub-app/timeline-svc.ts +62 -0
  200. package/src/serve/hub-app/timeline-view.tsx +245 -0
  201. package/src/serve/hub-app/tree-build.ts +410 -0
  202. package/src/serve/hub-app/tree-row.tsx +42 -0
  203. package/src/serve/hub-app/tree-svc.ts +174 -0
  204. package/src/serve/hub-app/wikis-view.tsx +53 -0
  205. package/src/serve/other-repos.ts +79 -0
  206. package/src/serve/project-gutter-route.ts +350 -0
  207. package/src/serve/project-gutter.ts +449 -0
  208. package/src/serve/run-serve.ts +245 -16
  209. package/src/serve/same-origin.ts +14 -0
  210. package/src/serve/serve-hub.ts +213 -18
  211. package/src/shipkit.ts +19 -12
  212. package/src/smoke/run-smoke.ts +3 -2
  213. package/src/start-repo.ts +75 -36
  214. package/src/suite/judge-suites.ts +216 -18
  215. package/src/suite/kept-timings.ts +58 -0
  216. package/src/suite/runner-shapes.ts +88 -0
  217. package/src/suite/suite-cli.ts +147 -24
  218. package/src/suite/suite-cpu-preload.ts +92 -0
  219. package/src/suite/test-units.ts +243 -0
  220. package/src/ts/config.ts +4 -69
  221. package/src/ts/jsonc.ts +69 -0
  222. package/src/ts/lex.ts +143 -0
  223. package/src/ts/rename.ts +73 -10
  224. package/src/ts/source-graph.ts +28 -8
  225. package/src/ts/tsconfig-projects.ts +6 -97
  226. package/src/ts/tsconfig-resolve.ts +108 -0
  227. package/src/tui/build-steps-panel.ts +61 -0
  228. package/src/tui/checks-panel.ts +24 -16
  229. package/src/tui/run-tui.ts +2 -2
  230. package/src/tui/tui-panel.ts +5 -4
  231. package/src/wiki/graph-colors.ts +68 -0
  232. package/src/wiki/graph-mount.ts +7 -9
  233. package/src/wiki/graph-page.ts +42 -25
  234. package/src/wiki/graph-positions.ts +105 -0
  235. package/src/wiki/run-wiki.ts +1 -1
  236. package/src/wiki/serve-graph.ts +33 -3
  237. package/src/cli/cmd_Rules.ts +0 -51
  238. package/src/cli/cmd_Units.ts +0 -82
  239. package/src/collections/graphs.ts +0 -32
  240. package/src/contracts/contracts-once.ts +0 -43
  241. package/src/core/check-suite.ts +0 -302
  242. package/src/core/repair-contract-dirs.ts +0 -136
  243. package/src/core/task-runner.ts +0 -339
  244. package/src/ledger/gate-units.ts +0 -138
  245. package/src/ledger/unit-table.ts +0 -105
  246. package/src/rules/SK002-contracts-fulfillness.ts +0 -24
  247. package/src/rules/SK003-claude-md-shape.ts +0 -70
  248. package/src/rules/SK004-gitignore-policy.ts +0 -238
  249. package/src/rules/SK010-undeclared-release-unit.ts +0 -127
  250. package/src/rules/rule-registry.ts +0 -64
  251. package/src/rules/run-fix.ts +0 -72
  252. package/src/tui/tasks-panel.ts +0 -60
@@ -0,0 +1,255 @@
1
+ // THE ONE DB — the file the run record lives in, its schema, and the migration
2
+ // that creates it. `ledger/run-log.ts` owns what a row MEANS; this module owns
3
+ // the file around it.
4
+ //
5
+ // Sweeping is a delete by age, which no concurrent write can be caught
6
+ // inside of: a size sweep that REWRITES a file loses whatever was appended
7
+ // during the rewrite.
8
+ //
9
+ // THREE PROPERTIES, and every one is load bearing:
10
+ // 1. NEVER READ BY THE GATE. Nothing stored here can change what a check does
11
+ // or what it replays, so a corrupt, locked or missing db costs a run its
12
+ // HISTORY and never its verdict. Every failure path out of this module is
13
+ // a null the caller turns into one warn line.
14
+ // 2. NOT THE LEDGER. The keyed store beside it (`ledger/store.ts`) is still
15
+ // one immutable json file per key, which is what a replay reads. This db
16
+ // is the reporting record: what every check cost on every run, red runs
17
+ // included, and what each check last printed.
18
+ // 3. SEVERAL WRITERS AT ONCE is the normal case (three agents in one
19
+ // checkout). WAL, a busy timeout, and one immediate transaction per flush
20
+ // is what makes six gates land every row whole.
21
+ //
22
+ // WHERE. `<git-common-dir>/shipkit/shipkit.sqlite`, beside the store and
23
+ // outside the work tree, so nothing it writes can dirty a diff or reach a
24
+ // commit. Every linked worktree of the repo shares it, the same as the store.
25
+ import { Database } from 'bun:sqlite'
26
+ import { existsSync, mkdirSync } from 'fs'
27
+ import { dirname, join } from 'path'
28
+ import { shipkitRoot } from './store.ts'
29
+
30
+ export const DB_FILE = 'shipkit.sqlite'
31
+
32
+ /** the schema this build speaks, stored in `pragma user_version`. Bumped when
33
+ * a table changes, never when a row's meaning does: the migration is the one
34
+ * place an older file is brought forward.
35
+ *
36
+ * v2 dropped the `kind` column. `task`, `rule` and `check` stopped being a
37
+ * thing a row could carry, and every row is filed under its bare id now. The rows written under the old scheme are DROPPED rather
38
+ * than migrated: they are per checkout timings a fortnight deep at most, and
39
+ * re-keying them would invent a mapping nobody can verify.
40
+ *
41
+ * v3 added `invocations`, one row per command that wrote runs (what ran it,
42
+ * its label, when it started and ended), and `suites`, one row per test
43
+ * suite per run of the suite runner. Both are new tables, so a v2 file keeps
44
+ * every row it holds.
45
+ *
46
+ * v4 names the last lines table `check_last`, keyed by `id`. An older file's
47
+ * table is renamed in place, so its rows stay.
48
+ *
49
+ * v5 adds `live`, one row per run IN FLIGHT: what the runner is doing right
50
+ * now, rewritten as checks start and finish and deleted when the runner
51
+ * returns. A new table, so every older row stays.
52
+ *
53
+ * v6 adds `check_id` to `suites`: which check ran the suite runner that
54
+ * timed it (`test`, `test:<unit>`). An older row keeps '' there, and an older
55
+ * build still inserts, because the column has a default.
56
+ *
57
+ * v7 adds `unit` to `suites`: the unit whose test check timed it
58
+ * (`root`, `rvlib-shipkit`). Same shape as v6: '' on an older row, a default
59
+ * for an older writer.
60
+ *
61
+ * v8 adds three columns to `suites` (`cpu_ms`, null where no CPU figure is
62
+ * the file's own; `phase`, the runner phase that timed it; `fail_line`, the
63
+ * first failure bun printed for it) and the `suite_runs` table, one row per
64
+ * run of the suite runner: its wall time, its phases, its workers. Defaults
65
+ * again, so an older writer still inserts and every older row stays.
66
+ *
67
+ * v9 adds `phase_ms` to `suites`: the file's wall time in the phase that ran
68
+ * it, which `ms` is not once an alone rerun judged it. Same shape again. */
69
+ export const SCHEMA_VERSION = 9
70
+
71
+ /** how long a writer waits on another process's transaction before it gives
72
+ * up and the run says so. Long enough for a whole gate's flush, short enough
73
+ * that nothing waits on a dashboard. */
74
+ export const BUSY_MS = 5_000
75
+
76
+ export const dbPath = (root: string): string => join(shipkitRoot(root), DB_FILE)
77
+
78
+ export const INSERT_RUN = 'insert into runs (run_id, id, at, ms, status, replayed) values (?, ?, ?, ?, ?, ?)'
79
+
80
+ export const UPSERT_INVOCATION =
81
+ 'insert into invocations (run_id, kind, label, by, narrowed, started, ended) values (?, ?, ?, ?, ?, ?, ?)' +
82
+ ' on conflict(run_id) do update set ended = excluded.ended'
83
+
84
+ export const UPSERT_LIVE =
85
+ 'insert into live (run_id, kind, label, by, pid, started, updated, state) values (?, ?, ?, ?, ?, ?, ?, ?)' +
86
+ ' on conflict(run_id) do update set updated = excluded.updated, state = excluded.state'
87
+
88
+ export const INSERT_SUITE = 'insert into suites (at, file, ms, status, tier, budget_ms, check_id, unit, cpu_ms, phase, phase_ms, fail_line) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
89
+
90
+ export const INSERT_SUITE_RUN =
91
+ 'insert into suite_runs (at, check_id, unit, tier, wall_ms, setup_ms, workers, files, parallel_ms, parallel_cpu_ms, serial_ms, serial_cpu_ms, serial_files, alone_ms, alone_files, load1, cores)' +
92
+ ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
93
+
94
+ export const UPSERT_LAST =
95
+ 'insert into check_last (id, at, status, ms, lines) values (?, ?, ?, ?, ?)' +
96
+ ' on conflict(id) do update set at = excluded.at, status = excluded.status, ms = excluded.ms, lines = excluded.lines'
97
+
98
+ const SCHEMA = `
99
+ create table if not exists runs (
100
+ run_id text not null,
101
+ id text not null,
102
+ at integer not null,
103
+ ms integer not null,
104
+ status text not null,
105
+ replayed integer not null
106
+ );
107
+ create index if not exists runs_at on runs (at);
108
+ create index if not exists runs_id on runs (id, at);
109
+ create table if not exists check_last (
110
+ id text primary key,
111
+ at integer not null,
112
+ status text not null,
113
+ ms integer not null,
114
+ lines text not null
115
+ );
116
+ create table if not exists invocations (
117
+ run_id text primary key,
118
+ kind text not null,
119
+ label text not null,
120
+ by text not null,
121
+ narrowed integer not null,
122
+ started integer not null,
123
+ ended integer not null
124
+ );
125
+ create index if not exists invocations_started on invocations (started);
126
+ create table if not exists suites (
127
+ at integer not null,
128
+ file text not null,
129
+ ms integer not null,
130
+ status text not null,
131
+ tier text not null,
132
+ budget_ms integer not null,
133
+ check_id text not null default '',
134
+ unit text not null default '',
135
+ cpu_ms integer default null,
136
+ phase text not null default '',
137
+ phase_ms integer default null,
138
+ fail_line text not null default ''
139
+ );
140
+ create index if not exists suites_file on suites (file, at);
141
+ create table if not exists suite_runs (
142
+ at integer not null,
143
+ check_id text not null,
144
+ unit text not null,
145
+ tier text not null,
146
+ wall_ms integer not null,
147
+ setup_ms integer not null,
148
+ workers integer not null,
149
+ files integer not null,
150
+ parallel_ms integer not null,
151
+ parallel_cpu_ms integer default null,
152
+ serial_ms integer default null,
153
+ serial_cpu_ms integer default null,
154
+ serial_files integer not null default 0,
155
+ alone_ms integer not null default 0,
156
+ alone_files integer not null default 0,
157
+ load1 real default null,
158
+ cores integer default null
159
+ );
160
+ create index if not exists suite_runs_check on suite_runs (check_id, at);
161
+ create table if not exists live (
162
+ run_id text primary key,
163
+ kind text not null,
164
+ label text not null,
165
+ by text not null,
166
+ pid integer not null,
167
+ started integer not null,
168
+ updated integer not null,
169
+ state text not null
170
+ );
171
+ `
172
+
173
+ export type Opened = { db: Database }
174
+
175
+ /** open the record, create the schema if this is its first open, and hand back
176
+ * a handle the caller CLOSES. Null for every failure there is: no path, no
177
+ * db and `create: false`, a corrupt file, a locked one, a read only disk. */
178
+ export function openDb(path: string, o: { create?: boolean; busyMs?: number } = {}): Opened | null {
179
+ if (path === '') return null
180
+ const create = o.create !== false
181
+ if (!create && !existsSync(path)) return null
182
+ let db: Database | null = null
183
+ try {
184
+ if (create) mkdirSync(dirname(path), { recursive: true })
185
+ db = new Database(path, { create, readwrite: true })
186
+ const busyMs = Math.max(0, Math.round(o.busyMs ?? BUSY_MS))
187
+ // the timeout goes on FIRST, so every ordinary statement waits on
188
+ // another process's transaction instead of failing
189
+ db.exec(`pragma busy_timeout = ${busyMs}`)
190
+ ensureWal(db, busyMs)
191
+ db.exec('pragma synchronous = NORMAL')
192
+ migrate(db)
193
+ return { db }
194
+ } catch {
195
+ try { db?.close() } catch { /* never opened, or already gone */ }
196
+ return null
197
+ }
198
+ }
199
+
200
+ /** WAL is what lets readers and a writer share the file. Switching INTO it
201
+ * takes an exclusive lock that sqlite refuses WITHOUT consulting the busy
202
+ * handler, so `busy_timeout` does not cover it: six processes opening a fresh
203
+ * db together, one of them loses and throws `database is locked`. Reproduced.
204
+ * So the mode is read first (after any one process has switched it, the
205
+ * switch is not attempted at all), retried briefly, and never fatal: a db
206
+ * left on the rollback journal still writes every row correctly under the
207
+ * busy timeout, it only serialises readers against the writer. */
208
+ function ensureWal(db: Database, busyMs: number): void {
209
+ const mode = (): string => String((db.query('pragma journal_mode').get() as { journal_mode?: string } | null)?.journal_mode ?? '')
210
+ const deadline = Date.now() + Math.min(busyMs, 2_000)
211
+ for (;;) {
212
+ if (mode() === 'wal') return
213
+ try {
214
+ db.exec('pragma journal_mode = WAL')
215
+ if (mode() === 'wal') return
216
+ } catch { /* another process holds the exclusive lock: look again */ }
217
+ if (Date.now() >= deadline) return
218
+ Bun.sleepSync(5 + Math.floor(Math.random() * 15))
219
+ }
220
+ }
221
+
222
+ const userVersion = (db: Database): number => Number((db.query('pragma user_version').get() as { user_version?: number } | null)?.user_version ?? 0)
223
+
224
+ /** create the schema, once. The version is read a SECOND time inside the
225
+ * write lock: two processes can open a fresh db at the same moment, and the
226
+ * loser must find the schema already there rather than drop it. */
227
+ function migrate(db: Database): void {
228
+ if (userVersion(db) >= SCHEMA_VERSION) return
229
+ db.exec('begin immediate')
230
+ try {
231
+ const was = userVersion(db)
232
+ if (was < SCHEMA_VERSION) {
233
+ // the pre-v2 tables carry a `kind` column and keys of the form
234
+ // `<kind>:<id>`, which no reader speaks any more. v2 to v3 only
235
+ // adds tables, v3 to v4 renames one, v4 to v5 adds one and v5 to
236
+ // v6 and every step to v9 add columns (v8 a table too), so
237
+ // their rows stay
238
+ if (was < 2) db.exec('drop table if exists runs; drop table if exists unit_last;')
239
+ else if (was < 4 && db.query("select 1 from sqlite_master where type = 'table' and name = 'unit_last'").get() != null) db.exec('alter table unit_last rename to check_last; alter table check_last rename column unit to id; drop index if exists runs_unit;')
240
+ const suites = db.query("select name from pragma_table_info('suites')").all() as { name: string }[]
241
+ if (suites.length > 0 && !suites.some((c) => c.name === 'check_id')) db.exec("alter table suites add column check_id text not null default ''")
242
+ if (suites.length > 0 && !suites.some((c) => c.name === 'unit')) db.exec("alter table suites add column unit text not null default ''")
243
+ if (suites.length > 0 && !suites.some((c) => c.name === 'cpu_ms')) db.exec('alter table suites add column cpu_ms integer default null')
244
+ if (suites.length > 0 && !suites.some((c) => c.name === 'phase')) db.exec("alter table suites add column phase text not null default ''")
245
+ if (suites.length > 0 && !suites.some((c) => c.name === 'phase_ms')) db.exec('alter table suites add column phase_ms integer default null')
246
+ if (suites.length > 0 && !suites.some((c) => c.name === 'fail_line')) db.exec("alter table suites add column fail_line text not null default ''")
247
+ db.exec(SCHEMA)
248
+ db.exec(`pragma user_version = ${SCHEMA_VERSION}`)
249
+ }
250
+ db.exec('commit')
251
+ } catch (e) {
252
+ try { db.exec('rollback') } catch { /* the transaction is already gone */ }
253
+ throw e
254
+ }
255
+ }