@storyshelf/core 0.6.2 → 0.8.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 (727) hide show
  1. package/README.md +49 -32
  2. package/dist/adapter/capture-queue.d.mts +44 -4
  3. package/dist/adapter/capture-runner.d.mts +134 -12
  4. package/dist/adapter/capture-runner.mjs +9 -12
  5. package/dist/adapter/database.d.mts +89 -10
  6. package/dist/adapter/database.mjs +4 -3
  7. package/dist/adapter/database.mjs.map +1 -1
  8. package/dist/adapter/design.d.mts +83 -0
  9. package/dist/adapter/design.mjs +27 -0
  10. package/dist/adapter/design.mjs.map +1 -0
  11. package/dist/adapter/email-sender.d.mts +0 -2
  12. package/dist/adapter/metadata.d.mts +3 -12
  13. package/dist/adapter/notifier.d.mts +6 -3
  14. package/dist/adapter/notifier.mjs +5 -1
  15. package/dist/adapter/oauth.d.mts +50 -0
  16. package/dist/adapter/setup.d.mts +4 -7
  17. package/dist/adapter/setup.mjs +3 -3
  18. package/dist/adapter/setup.mjs.map +1 -1
  19. package/dist/adapter/storage.d.mts +1 -3
  20. package/dist/adapter/webhook-events.d.mts +0 -2
  21. package/dist/adapter/webhook-events.mjs +13 -14
  22. package/dist/adapter/webhook-events.mjs.map +1 -1
  23. package/dist/adapters/capture-target-offers.d.mts +26 -0
  24. package/dist/adapters/capture-target-offers.mjs +57 -0
  25. package/dist/adapters/capture-target-offers.mjs.map +1 -0
  26. package/dist/adapters/capture-target.d.mts +69 -0
  27. package/dist/adapters/capture-target.mjs +75 -0
  28. package/dist/adapters/capture-target.mjs.map +1 -0
  29. package/dist/adapters/notifier/build-comment.d.mts +22 -0
  30. package/dist/adapters/notifier/build-comment.mjs +23 -0
  31. package/dist/adapters/notifier/build-comment.mjs.map +1 -0
  32. package/dist/adapters/notifier/build-state.d.mts +97 -0
  33. package/dist/adapters/notifier/build-state.mjs +217 -0
  34. package/dist/adapters/notifier/build-state.mjs.map +1 -0
  35. package/dist/adapters/notifier/events.d.mts +4 -4
  36. package/dist/adapters/notifier/events.mjs +32 -20
  37. package/dist/adapters/notifier/events.mjs.map +1 -1
  38. package/dist/adapters/notifier/format.d.mts +0 -2
  39. package/dist/adapters/notifier/format.mjs +1 -2
  40. package/dist/adapters/notifier/provider.d.mts +53 -9
  41. package/dist/adapters/notifier/provider.mjs +11 -0
  42. package/dist/adapters/notifier/provider.mjs.map +1 -0
  43. package/dist/adapters/notifier/state.d.mts +22 -0
  44. package/dist/adapters/notifier/state.mjs +65 -0
  45. package/dist/adapters/notifier/state.mjs.map +1 -0
  46. package/dist/adapters/notifier/types.d.mts +51 -4
  47. package/dist/adapters/remote-browser.d.mts +168 -0
  48. package/dist/adapters/remote-browser.mjs +201 -0
  49. package/dist/adapters/remote-browser.mjs.map +1 -0
  50. package/dist/adapters/remote-lease-attempt.mjs +39 -0
  51. package/dist/adapters/remote-lease-attempt.mjs.map +1 -0
  52. package/dist/adapters/remote-lease-error.d.mts +25 -0
  53. package/dist/adapters/remote-lease-error.mjs +54 -0
  54. package/dist/adapters/remote-lease-error.mjs.map +1 -0
  55. package/dist/adapters/remote-lease-held.d.mts +15 -0
  56. package/dist/adapters/remote-lease-held.mjs +86 -0
  57. package/dist/adapters/remote-lease-held.mjs.map +1 -0
  58. package/dist/adapters/remote-lease-shutdown.d.mts +12 -0
  59. package/dist/adapters/remote-lease-shutdown.mjs +32 -0
  60. package/dist/adapters/remote-lease-shutdown.mjs.map +1 -0
  61. package/dist/adapters/remote-lease.d.mts +39 -0
  62. package/dist/adapters/remote-lease.mjs +175 -0
  63. package/dist/adapters/remote-lease.mjs.map +1 -0
  64. package/dist/adapters/remote-limiter.mjs +121 -0
  65. package/dist/adapters/remote-limiter.mjs.map +1 -0
  66. package/dist/adapters/remote-pool.d.mts +56 -0
  67. package/dist/adapters/remote-pool.mjs +195 -0
  68. package/dist/adapters/remote-pool.mjs.map +1 -0
  69. package/dist/adapters/render-runs.d.mts +23 -0
  70. package/dist/adapters/render-runs.mjs +40 -0
  71. package/dist/adapters/render-runs.mjs.map +1 -0
  72. package/dist/adapters/setup-runner.d.mts +0 -2
  73. package/dist/adapters/setup-runner.mjs +3 -6
  74. package/dist/adapters/setup-runner.mjs.map +1 -1
  75. package/dist/adapters/sources.d.mts +10 -0
  76. package/dist/adapters/sources.mjs +19 -0
  77. package/dist/adapters/sources.mjs.map +1 -0
  78. package/dist/adapters/validate.d.mts +0 -2
  79. package/dist/adapters/validate.mjs +27 -20
  80. package/dist/adapters/validate.mjs.map +1 -1
  81. package/dist/ai.d.mts +103 -0
  82. package/dist/ai.mjs +18 -0
  83. package/dist/ai.mjs.map +1 -0
  84. package/dist/auth.d.mts +0 -2
  85. package/dist/auth.mjs +1 -2
  86. package/dist/brand-packs.d.mts +0 -2
  87. package/dist/brand-packs.mjs +0 -1
  88. package/dist/capture/a11y/axe-source.d.mts +16 -0
  89. package/dist/capture/a11y/axe-source.mjs +36 -0
  90. package/dist/capture/a11y/axe-source.mjs.map +1 -0
  91. package/dist/capture/a11y/in-page.d.mts +25 -0
  92. package/dist/capture/a11y/in-page.mjs +59 -0
  93. package/dist/capture/a11y/in-page.mjs.map +1 -0
  94. package/dist/capture/a11y/map.d.mts +22 -0
  95. package/dist/capture/a11y/map.mjs +47 -0
  96. package/dist/capture/a11y/map.mjs.map +1 -0
  97. package/dist/capture/a11y/options.d.mts +21 -0
  98. package/dist/capture/a11y/options.mjs +74 -0
  99. package/dist/capture/a11y/options.mjs.map +1 -0
  100. package/dist/capture/adapter.d.mts +46 -5
  101. package/dist/capture/adapter.mjs +1 -2
  102. package/dist/capture/adapter.mjs.map +1 -1
  103. package/dist/capture/affected.d.mts +2 -2
  104. package/dist/capture/affected.mjs +14 -6
  105. package/dist/capture/affected.mjs.map +1 -1
  106. package/dist/capture/attempt-logger.mjs +83 -0
  107. package/dist/capture/attempt-logger.mjs.map +1 -0
  108. package/dist/capture/browsers.d.mts +33 -0
  109. package/dist/capture/browsers.mjs +66 -0
  110. package/dist/capture/browsers.mjs.map +1 -0
  111. package/dist/capture/build-state/deliver.d.mts +30 -0
  112. package/dist/capture/build-state/deliver.mjs +117 -0
  113. package/dist/capture/build-state/deliver.mjs.map +1 -0
  114. package/dist/capture/build-state/index.d.mts +8 -0
  115. package/dist/capture/build-state/notify-job.d.mts +54 -0
  116. package/dist/capture/build-state/notify-job.mjs +132 -0
  117. package/dist/capture/build-state/notify-job.mjs.map +1 -0
  118. package/dist/capture/build-state/publisher.d.mts +21 -0
  119. package/dist/capture/build-state/publisher.mjs +106 -0
  120. package/dist/capture/build-state/publisher.mjs.map +1 -0
  121. package/dist/capture/build-state/resume.d.mts +7 -0
  122. package/dist/capture/build-state/resume.mjs +46 -0
  123. package/dist/capture/build-state/resume.mjs.map +1 -0
  124. package/dist/capture/build-state/types.d.mts +47 -0
  125. package/dist/capture/catalog/catalogue.d.mts +20 -0
  126. package/dist/capture/catalog/catalogue.mjs +52 -0
  127. package/dist/capture/catalog/catalogue.mjs.map +1 -0
  128. package/dist/capture/catalog/components.d.mts +21 -0
  129. package/dist/capture/catalog/components.mjs +54 -0
  130. package/dist/capture/catalog/components.mjs.map +1 -0
  131. package/dist/capture/catalog/history.d.mts +28 -0
  132. package/dist/capture/catalog/history.mjs +50 -0
  133. package/dist/capture/catalog/history.mjs.map +1 -0
  134. package/dist/capture/catalog/hook.d.mts +12 -0
  135. package/dist/capture/catalog/hook.mjs +18 -0
  136. package/dist/capture/catalog/hook.mjs.map +1 -0
  137. package/dist/capture/catalog/index.d.mts +11 -0
  138. package/dist/capture/catalog/load.d.mts +24 -0
  139. package/dist/capture/catalog/load.mjs +48 -0
  140. package/dist/capture/catalog/load.mjs.map +1 -0
  141. package/dist/capture/catalog/parse.d.mts +13 -0
  142. package/dist/capture/catalog/parse.mjs +55 -0
  143. package/dist/capture/catalog/parse.mjs.map +1 -0
  144. package/dist/capture/catalog/read.d.mts +15 -0
  145. package/dist/capture/catalog/read.mjs +39 -0
  146. package/dist/capture/catalog/read.mjs.map +1 -0
  147. package/dist/capture/catalog/types.d.mts +31 -0
  148. package/dist/capture/comparison-reference.d.mts +27 -0
  149. package/dist/capture/comparison-reference.mjs +75 -0
  150. package/dist/capture/comparison-reference.mjs.map +1 -0
  151. package/dist/capture/consumer/capture-handler.d.mts +56 -0
  152. package/dist/capture/consumer/capture-handler.mjs +117 -0
  153. package/dist/capture/consumer/capture-handler.mjs.map +1 -0
  154. package/dist/capture/consumer/idle.d.mts +15 -0
  155. package/dist/capture/consumer/idle.mjs +24 -0
  156. package/dist/capture/consumer/idle.mjs.map +1 -0
  157. package/dist/capture/consumer/index.d.mts +11 -0
  158. package/dist/capture/consumer/index.mjs +138 -0
  159. package/dist/capture/consumer/index.mjs.map +1 -0
  160. package/dist/capture/consumer/loop.mjs +109 -0
  161. package/dist/capture/consumer/loop.mjs.map +1 -0
  162. package/dist/capture/consumer/metrics.d.mts +39 -0
  163. package/dist/capture/consumer/metrics.mjs +37 -0
  164. package/dist/capture/consumer/metrics.mjs.map +1 -0
  165. package/dist/capture/consumer/retry.d.mts +31 -0
  166. package/dist/capture/consumer/retry.mjs +33 -0
  167. package/dist/capture/consumer/retry.mjs.map +1 -0
  168. package/dist/capture/consumer/run-job.mjs +194 -0
  169. package/dist/capture/consumer/run-job.mjs.map +1 -0
  170. package/dist/capture/consumer/startup.mjs +37 -0
  171. package/dist/capture/consumer/startup.mjs.map +1 -0
  172. package/dist/capture/consumer/types.d.mts +95 -0
  173. package/dist/capture/crash-failure.d.mts +23 -0
  174. package/dist/capture/crash-failure.mjs +69 -0
  175. package/dist/capture/crash-failure.mjs.map +1 -0
  176. package/dist/capture/db-queue/cadence.d.mts +30 -0
  177. package/dist/capture/db-queue/cadence.mjs +37 -0
  178. package/dist/capture/db-queue/cadence.mjs.map +1 -0
  179. package/dist/capture/db-queue/claim.d.mts +7 -0
  180. package/dist/capture/db-queue/claim.mjs +148 -0
  181. package/dist/capture/db-queue/claim.mjs.map +1 -0
  182. package/dist/capture/db-queue/clock.mjs +11 -0
  183. package/dist/capture/db-queue/clock.mjs.map +1 -0
  184. package/dist/capture/db-queue/codec.mjs +75 -0
  185. package/dist/capture/db-queue/codec.mjs.map +1 -0
  186. package/dist/capture/db-queue/index.d.mts +18 -0
  187. package/dist/capture/db-queue/index.mjs +89 -0
  188. package/dist/capture/db-queue/index.mjs.map +1 -0
  189. package/dist/capture/db-queue/keeper.d.mts +34 -0
  190. package/dist/capture/db-queue/keeper.mjs +91 -0
  191. package/dist/capture/db-queue/keeper.mjs.map +1 -0
  192. package/dist/capture/db-queue/lease.mjs +45 -0
  193. package/dist/capture/db-queue/lease.mjs.map +1 -0
  194. package/dist/capture/db-queue/lifecycle.mjs +35 -0
  195. package/dist/capture/db-queue/lifecycle.mjs.map +1 -0
  196. package/dist/capture/db-queue/operations.mjs +93 -0
  197. package/dist/capture/db-queue/operations.mjs.map +1 -0
  198. package/dist/capture/db-queue/poll-loop.d.mts +28 -0
  199. package/dist/capture/db-queue/poll-loop.mjs +88 -0
  200. package/dist/capture/db-queue/poll-loop.mjs.map +1 -0
  201. package/dist/capture/db-queue/poll.d.mts +8 -0
  202. package/dist/capture/db-queue/poll.mjs +68 -0
  203. package/dist/capture/db-queue/poll.mjs.map +1 -0
  204. package/dist/capture/db-queue/timers.d.mts +10 -0
  205. package/dist/capture/db-queue/timers.mjs +36 -0
  206. package/dist/capture/db-queue/timers.mjs.map +1 -0
  207. package/dist/capture/db-queue/types.d.mts +81 -0
  208. package/dist/capture/db-queue/types.mjs +11 -0
  209. package/dist/capture/db-queue/types.mjs.map +1 -0
  210. package/dist/capture/db-queue/wake.mjs +47 -0
  211. package/dist/capture/db-queue/wake.mjs.map +1 -0
  212. package/dist/capture/design.mjs +38 -0
  213. package/dist/capture/design.mjs.map +1 -0
  214. package/dist/capture/diff-record.mjs +24 -5
  215. package/dist/capture/diff-record.mjs.map +1 -1
  216. package/dist/capture/dispatch.d.mts +13 -10
  217. package/dist/capture/dispatch.mjs +102 -111
  218. package/dist/capture/dispatch.mjs.map +1 -1
  219. package/dist/capture/enrich.mjs +59 -0
  220. package/dist/capture/enrich.mjs.map +1 -0
  221. package/dist/capture/failure-sets.mjs +21 -0
  222. package/dist/capture/failure-sets.mjs.map +1 -0
  223. package/dist/capture/fingerprint.d.mts +40 -0
  224. package/dist/capture/fingerprint.mjs +82 -0
  225. package/dist/capture/fingerprint.mjs.map +1 -0
  226. package/dist/capture/forked/channel.d.mts +23 -0
  227. package/dist/capture/forked/child.d.mts +32 -0
  228. package/dist/capture/forked/child.mjs +201 -0
  229. package/dist/capture/forked/child.mjs.map +1 -0
  230. package/dist/capture/forked/crash-tracker.mjs +22 -0
  231. package/dist/capture/forked/crash-tracker.mjs.map +1 -0
  232. package/dist/capture/forked/decision.d.mts +24 -0
  233. package/dist/capture/forked/decision.mjs +26 -0
  234. package/dist/capture/forked/decision.mjs.map +1 -0
  235. package/dist/capture/forked/exit.mjs +11 -0
  236. package/dist/capture/forked/exit.mjs.map +1 -0
  237. package/dist/capture/forked/fork.d.mts +29 -0
  238. package/dist/capture/forked/fork.mjs +92 -0
  239. package/dist/capture/forked/fork.mjs.map +1 -0
  240. package/dist/capture/forked/forked-capture.d.mts +38 -0
  241. package/dist/capture/forked/forked-capture.mjs +239 -0
  242. package/dist/capture/forked/forked-capture.mjs.map +1 -0
  243. package/dist/capture/forked/lease-book.mjs +45 -0
  244. package/dist/capture/forked/lease-book.mjs.map +1 -0
  245. package/dist/capture/forked/protocol.d.mts +36 -0
  246. package/dist/capture/forked/protocol.mjs +24 -0
  247. package/dist/capture/forked/protocol.mjs.map +1 -0
  248. package/dist/capture/forked/runs.mjs +74 -0
  249. package/dist/capture/forked/runs.mjs.map +1 -0
  250. package/dist/capture/guards.mjs +1 -2
  251. package/dist/capture/ignore-regions.d.mts +36 -0
  252. package/dist/capture/ignore-regions.mjs +65 -0
  253. package/dist/capture/ignore-regions.mjs.map +1 -0
  254. package/dist/capture/isolate.mjs +140 -0
  255. package/dist/capture/isolate.mjs.map +1 -0
  256. package/dist/capture/job-lease.d.mts +26 -0
  257. package/dist/capture/job-lease.mjs +56 -0
  258. package/dist/capture/job-lease.mjs.map +1 -0
  259. package/dist/capture/matrix.mjs +298 -0
  260. package/dist/capture/matrix.mjs.map +1 -0
  261. package/dist/capture/modes.d.mts +23 -0
  262. package/dist/capture/modes.mjs +106 -0
  263. package/dist/capture/modes.mjs.map +1 -0
  264. package/dist/capture/orchestrator.d.mts +37 -7
  265. package/dist/capture/orchestrator.mjs +120 -110
  266. package/dist/capture/orchestrator.mjs.map +1 -1
  267. package/dist/capture/pipeline.d.mts +9 -4
  268. package/dist/capture/pipeline.mjs +284 -48
  269. package/dist/capture/pipeline.mjs.map +1 -1
  270. package/dist/capture/progress.d.mts +9 -0
  271. package/dist/capture/progress.mjs +27 -0
  272. package/dist/capture/progress.mjs.map +1 -0
  273. package/dist/capture/publish/accept.d.mts +37 -0
  274. package/dist/capture/publish/accept.mjs +27 -0
  275. package/dist/capture/publish/accept.mjs.map +1 -0
  276. package/dist/capture/publish/complete.d.mts +44 -0
  277. package/dist/capture/publish/complete.mjs +63 -0
  278. package/dist/capture/publish/complete.mjs.map +1 -0
  279. package/dist/capture/publish/extract.d.mts +18 -0
  280. package/dist/capture/publish/extract.mjs +120 -0
  281. package/dist/capture/publish/extract.mjs.map +1 -0
  282. package/dist/capture/publish/handler.d.mts +26 -0
  283. package/dist/capture/publish/handler.mjs +61 -0
  284. package/dist/capture/publish/handler.mjs.map +1 -0
  285. package/dist/capture/publish/index.d.mts +11 -0
  286. package/dist/capture/publish/invalid-zip.d.mts +8 -0
  287. package/dist/capture/publish/invalid-zip.mjs +11 -0
  288. package/dist/capture/publish/invalid-zip.mjs.map +1 -0
  289. package/dist/capture/publish/limits.d.mts +35 -0
  290. package/dist/capture/publish/limits.mjs +38 -0
  291. package/dist/capture/publish/limits.mjs.map +1 -0
  292. package/dist/capture/publish/published.d.mts +13 -0
  293. package/dist/capture/publish/published.mjs +17 -0
  294. package/dist/capture/publish/published.mjs.map +1 -0
  295. package/dist/capture/publish/run.d.mts +31 -0
  296. package/dist/capture/publish/run.mjs +29 -0
  297. package/dist/capture/publish/run.mjs.map +1 -0
  298. package/dist/capture/rediff.d.mts +1 -3
  299. package/dist/capture/rediff.mjs +23 -9
  300. package/dist/capture/rediff.mjs.map +1 -1
  301. package/dist/capture/render.mjs +52 -0
  302. package/dist/capture/render.mjs.map +1 -0
  303. package/dist/capture/scratch.d.mts +42 -0
  304. package/dist/capture/scratch.mjs +136 -0
  305. package/dist/capture/scratch.mjs.map +1 -0
  306. package/dist/capture/shard-plan.mjs +127 -0
  307. package/dist/capture/shard-plan.mjs.map +1 -0
  308. package/dist/capture/shards/context.mjs +51 -0
  309. package/dist/capture/shards/context.mjs.map +1 -0
  310. package/dist/capture/shards/execute.mjs +175 -0
  311. package/dist/capture/shards/execute.mjs.map +1 -0
  312. package/dist/capture/shards/fan-out.mjs +125 -0
  313. package/dist/capture/shards/fan-out.mjs.map +1 -0
  314. package/dist/capture/shards/history.mjs +34 -0
  315. package/dist/capture/shards/history.mjs.map +1 -0
  316. package/dist/capture/shards/reduce.mjs +96 -0
  317. package/dist/capture/shards/reduce.mjs.map +1 -0
  318. package/dist/capture/sizing.d.mts +0 -2
  319. package/dist/capture/sizing.mjs +2 -13
  320. package/dist/capture/sizing.mjs.map +1 -1
  321. package/dist/capture/skip-checks.mjs +3 -67
  322. package/dist/capture/skip-checks.mjs.map +1 -1
  323. package/dist/capture/statics.d.mts +7 -10
  324. package/dist/capture/statics.mjs +26 -74
  325. package/dist/capture/statics.mjs.map +1 -1
  326. package/dist/capture/storybook-urls.d.mts +38 -0
  327. package/dist/capture/storybook-urls.mjs +41 -0
  328. package/dist/capture/storybook-urls.mjs.map +1 -0
  329. package/dist/capture/storybook.d.mts +12 -6
  330. package/dist/capture/storybook.mjs +63 -19
  331. package/dist/capture/storybook.mjs.map +1 -1
  332. package/dist/capture/target.mjs +20 -0
  333. package/dist/capture/target.mjs.map +1 -0
  334. package/dist/capture/targets.d.mts +23 -0
  335. package/dist/capture/targets.mjs +31 -0
  336. package/dist/capture/targets.mjs.map +1 -0
  337. package/dist/capture/tests.d.mts +35 -0
  338. package/dist/capture/tests.mjs +136 -0
  339. package/dist/capture/tests.mjs.map +1 -0
  340. package/dist/capture/token-chunks.mjs +91 -0
  341. package/dist/capture/token-chunks.mjs.map +1 -0
  342. package/dist/capture.d.mts +63 -7
  343. package/dist/capture.mjs +51 -3
  344. package/dist/config.d.mts +136 -14
  345. package/dist/config.mjs +14 -8
  346. package/dist/config.mjs.map +1 -1
  347. package/dist/diff.d.mts +7 -3
  348. package/dist/diff.mjs +15 -3
  349. package/dist/diff.mjs.map +1 -0
  350. package/dist/index.d.mts +3 -3
  351. package/dist/index.mjs +2 -2
  352. package/dist/insights/budget.d.mts +18 -0
  353. package/dist/insights/budget.mjs +32 -0
  354. package/dist/insights/budget.mjs.map +1 -0
  355. package/dist/insights/envelope.d.mts +57 -0
  356. package/dist/insights/envelope.mjs +59 -0
  357. package/dist/insights/envelope.mjs.map +1 -0
  358. package/dist/insights/evidence-health.d.mts +30 -0
  359. package/dist/insights/evidence-health.mjs +27 -0
  360. package/dist/insights/evidence-health.mjs.map +1 -0
  361. package/dist/insights/evidence.d.mts +53 -0
  362. package/dist/insights/evidence.mjs +63 -0
  363. package/dist/insights/evidence.mjs.map +1 -0
  364. package/dist/insights/hash.d.mts +16 -0
  365. package/dist/insights/hash.mjs +20 -0
  366. package/dist/insights/hash.mjs.map +1 -0
  367. package/dist/insights/limits.d.mts +22 -0
  368. package/dist/insights/limits.mjs +23 -0
  369. package/dist/insights/limits.mjs.map +1 -0
  370. package/dist/insights/prompts/v1.d.mts +11 -0
  371. package/dist/insights/prompts/v1.mjs +33 -0
  372. package/dist/insights/prompts/v1.mjs.map +1 -0
  373. package/dist/insights/redact.d.mts +7 -0
  374. package/dist/insights/redact.mjs +23 -0
  375. package/dist/insights/redact.mjs.map +1 -0
  376. package/dist/insights/text.mjs +43 -0
  377. package/dist/insights/text.mjs.map +1 -0
  378. package/dist/insights.d.mts +11 -0
  379. package/dist/insights.mjs +12 -0
  380. package/dist/logger.d.mts +0 -2
  381. package/dist/logger.mjs +18 -3
  382. package/dist/logger.mjs.map +1 -1
  383. package/dist/models/ai-budget-alert.d.mts +21 -0
  384. package/dist/models/ai-budget-alert.mjs +37 -0
  385. package/dist/models/ai-budget-alert.mjs.map +1 -0
  386. package/dist/models/ai-usage.d.mts +38 -0
  387. package/dist/models/ai-usage.mjs +60 -0
  388. package/dist/models/ai-usage.mjs.map +1 -0
  389. package/dist/models/baseline-approval.d.mts +18 -0
  390. package/dist/models/baseline-approval.mjs +41 -0
  391. package/dist/models/baseline-approval.mjs.map +1 -0
  392. package/dist/models/baseline-archive.d.mts +82 -0
  393. package/dist/models/baseline-archive.mjs +105 -0
  394. package/dist/models/baseline-archive.mjs.map +1 -0
  395. package/dist/models/baseline-atomic.mjs +66 -0
  396. package/dist/models/baseline-atomic.mjs.map +1 -0
  397. package/dist/models/baseline-conflict.d.mts +6 -0
  398. package/dist/models/baseline-conflict.mjs +9 -0
  399. package/dist/models/baseline-conflict.mjs.map +1 -0
  400. package/dist/models/baseline-export.d.mts +17 -0
  401. package/dist/models/baseline-export.mjs +135 -0
  402. package/dist/models/baseline-export.mjs.map +1 -0
  403. package/dist/models/baseline-import.mjs +117 -0
  404. package/dist/models/baseline-import.mjs.map +1 -0
  405. package/dist/models/baseline-origin.d.mts +16 -0
  406. package/dist/models/baseline-origin.mjs +35 -0
  407. package/dist/models/baseline-origin.mjs.map +1 -0
  408. package/dist/models/baseline-status.d.mts +0 -2
  409. package/dist/models/baseline-status.mjs +1 -2
  410. package/dist/models/baseline-transfer.d.mts +48 -0
  411. package/dist/models/baseline-transfer.mjs +149 -0
  412. package/dist/models/baseline-transfer.mjs.map +1 -0
  413. package/dist/models/baseline.d.mts +37 -7
  414. package/dist/models/baseline.mjs +152 -28
  415. package/dist/models/baseline.mjs.map +1 -1
  416. package/dist/models/build-component.d.mts +26 -0
  417. package/dist/models/build-component.mjs +54 -0
  418. package/dist/models/build-component.mjs.map +1 -0
  419. package/dist/models/build-review-status.d.mts +27 -0
  420. package/dist/models/build-review-status.mjs +36 -0
  421. package/dist/models/build-review-status.mjs.map +1 -0
  422. package/dist/models/build-review.d.mts +26 -0
  423. package/dist/models/build-review.mjs +53 -0
  424. package/dist/models/build-review.mjs.map +1 -0
  425. package/dist/models/build.d.mts +25 -5
  426. package/dist/models/build.mjs +53 -15
  427. package/dist/models/build.mjs.map +1 -1
  428. package/dist/models/capture-attempt.d.mts +7 -3
  429. package/dist/models/capture-attempt.mjs +30 -6
  430. package/dist/models/capture-attempt.mjs.map +1 -1
  431. package/dist/models/capture-job.d.mts +104 -0
  432. package/dist/models/capture-job.mjs +287 -0
  433. package/dist/models/capture-job.mjs.map +1 -0
  434. package/dist/models/capture-log.d.mts +1 -3
  435. package/dist/models/capture-log.mjs +5 -5
  436. package/dist/models/capture-log.mjs.map +1 -1
  437. package/dist/models/capture-shard.d.mts +60 -0
  438. package/dist/models/capture-shard.mjs +189 -0
  439. package/dist/models/capture-shard.mjs.map +1 -0
  440. package/dist/models/changeset.d.mts +46 -0
  441. package/dist/models/changeset.mjs +80 -0
  442. package/dist/models/changeset.mjs.map +1 -0
  443. package/dist/models/columns.mjs +18 -0
  444. package/dist/models/columns.mjs.map +1 -0
  445. package/dist/models/comment.d.mts +4 -3
  446. package/dist/models/comment.mjs +8 -6
  447. package/dist/models/comment.mjs.map +1 -1
  448. package/dist/models/component.d.mts +41 -0
  449. package/dist/models/component.mjs +59 -0
  450. package/dist/models/component.mjs.map +1 -0
  451. package/dist/models/connections.d.mts +39 -0
  452. package/dist/models/connections.mjs +111 -0
  453. package/dist/models/connections.mjs.map +1 -0
  454. package/dist/models/content-ref.d.mts +1 -3
  455. package/dist/models/content-ref.mjs +2 -3
  456. package/dist/models/content-ref.mjs.map +1 -1
  457. package/dist/models/credentials.d.mts +0 -2
  458. package/dist/models/credentials.mjs +13 -4
  459. package/dist/models/credentials.mjs.map +1 -1
  460. package/dist/models/design-credentials.d.mts +39 -0
  461. package/dist/models/design-credentials.mjs +85 -0
  462. package/dist/models/design-credentials.mjs.map +1 -0
  463. package/dist/models/design-health.d.mts +14 -0
  464. package/dist/models/design-health.mjs +24 -0
  465. package/dist/models/design-health.mjs.map +1 -0
  466. package/dist/models/design-nodes.d.mts +81 -0
  467. package/dist/models/design-nodes.mjs +221 -0
  468. package/dist/models/design-nodes.mjs.map +1 -0
  469. package/dist/models/design-renders.mjs +73 -0
  470. package/dist/models/design-renders.mjs.map +1 -0
  471. package/dist/models/design-resolve.d.mts +27 -0
  472. package/dist/models/design-resolve.mjs +40 -0
  473. package/dist/models/design-resolve.mjs.map +1 -0
  474. package/dist/models/design.d.mts +37 -0
  475. package/dist/models/design.mjs +89 -0
  476. package/dist/models/design.mjs.map +1 -0
  477. package/dist/models/inherited-ratio.d.mts +19 -0
  478. package/dist/models/inherited-ratio.mjs +21 -0
  479. package/dist/models/inherited-ratio.mjs.map +1 -0
  480. package/dist/models/insight.d.mts +68 -0
  481. package/dist/models/insight.mjs +161 -0
  482. package/dist/models/insight.mjs.map +1 -0
  483. package/dist/models/label.d.mts +4 -4
  484. package/dist/models/label.mjs +26 -12
  485. package/dist/models/label.mjs.map +1 -1
  486. package/dist/models/member.d.mts +1 -3
  487. package/dist/models/member.mjs +6 -6
  488. package/dist/models/member.mjs.map +1 -1
  489. package/dist/models/notification-channel.d.mts +37 -5
  490. package/dist/models/notification-channel.mjs +89 -11
  491. package/dist/models/notification-channel.mjs.map +1 -1
  492. package/dist/models/notification-subscription.d.mts +1 -4
  493. package/dist/models/notification-subscription.mjs +8 -11
  494. package/dist/models/notification-subscription.mjs.map +1 -1
  495. package/dist/models/project-group-mapping.d.mts +6 -3
  496. package/dist/models/project-group-mapping.mjs +15 -6
  497. package/dist/models/project-group-mapping.mjs.map +1 -1
  498. package/dist/models/project.d.mts +9 -3
  499. package/dist/models/project.mjs +10 -6
  500. package/dist/models/project.mjs.map +1 -1
  501. package/dist/models/public-origin.d.mts +61 -0
  502. package/dist/models/public-origin.mjs +113 -0
  503. package/dist/models/public-origin.mjs.map +1 -0
  504. package/dist/models/review-switch-off.d.mts +12 -0
  505. package/dist/models/review-switch-off.mjs +25 -0
  506. package/dist/models/review-switch-off.mjs.map +1 -0
  507. package/dist/models/site-settings.d.mts +21 -0
  508. package/dist/models/site-settings.mjs +47 -0
  509. package/dist/models/site-settings.mjs.map +1 -0
  510. package/dist/models/snapshot.d.mts +39 -3
  511. package/dist/models/snapshot.mjs +54 -5
  512. package/dist/models/snapshot.mjs.map +1 -1
  513. package/dist/models/token.d.mts +13 -3
  514. package/dist/models/token.mjs +24 -7
  515. package/dist/models/token.mjs.map +1 -1
  516. package/dist/models/usage.d.mts +74 -0
  517. package/dist/models/usage.mjs +148 -0
  518. package/dist/models/usage.mjs.map +1 -0
  519. package/dist/models/user.d.mts +1 -3
  520. package/dist/models/user.mjs +5 -5
  521. package/dist/models/user.mjs.map +1 -1
  522. package/dist/models/webhook.d.mts +15 -3
  523. package/dist/models/webhook.mjs +34 -6
  524. package/dist/models/webhook.mjs.map +1 -1
  525. package/dist/models.d.mts +30 -4
  526. package/dist/models.mjs +31 -6
  527. package/dist/options.d.mts +16 -0
  528. package/dist/options.mjs +88 -0
  529. package/dist/options.mjs.map +1 -0
  530. package/dist/paths.d.mts +21 -6
  531. package/dist/paths.mjs +57 -12
  532. package/dist/paths.mjs.map +1 -1
  533. package/dist/retention/ai.d.mts +16 -0
  534. package/dist/retention/ai.mjs +22 -0
  535. package/dist/retention/ai.mjs.map +1 -0
  536. package/dist/retention/pending.mjs +46 -0
  537. package/dist/retention/pending.mjs.map +1 -0
  538. package/dist/retention/published-gzip-sweep.mjs +28 -0
  539. package/dist/retention/published-gzip-sweep.mjs.map +1 -0
  540. package/dist/retention/screenshot-sweep.d.mts +49 -0
  541. package/dist/retention/screenshot-sweep.mjs +127 -0
  542. package/dist/retention/screenshot-sweep.mjs.map +1 -0
  543. package/dist/retention.d.mts +16 -4
  544. package/dist/retention.mjs +76 -16
  545. package/dist/retention.mjs.map +1 -1
  546. package/dist/schema/baseline.d.mts +8 -2
  547. package/dist/schema/build-component.d.mts +23 -0
  548. package/dist/schema/build-review.d.mts +15 -0
  549. package/dist/schema/build.d.mts +7 -2
  550. package/dist/schema/capture-attempt.d.mts +0 -2
  551. package/dist/schema/capture-job.d.mts +58 -0
  552. package/dist/schema/capture-shard.d.mts +33 -0
  553. package/dist/schema/comment.d.mts +2 -2
  554. package/dist/schema/connection.d.mts +22 -0
  555. package/dist/schema/content-ref.d.mts +0 -2
  556. package/dist/schema/design.d.mts +56 -0
  557. package/dist/schema/insight.d.mts +52 -0
  558. package/dist/schema/label.d.mts +0 -2
  559. package/dist/schema/member.d.mts +0 -2
  560. package/dist/schema/notification.d.mts +4 -2
  561. package/dist/schema/project-group-mapping.d.mts +0 -2
  562. package/dist/schema/project.d.mts +21 -2
  563. package/dist/schema/site-setting.d.mts +19 -0
  564. package/dist/schema/snapshot.d.mts +17 -2
  565. package/dist/schema/token.d.mts +0 -2
  566. package/dist/schema/usage.d.mts +28 -0
  567. package/dist/schema/user.d.mts +0 -2
  568. package/dist/schema/webhook.d.mts +0 -2
  569. package/dist/schema.d.mts +12 -6
  570. package/dist/serverless.d.mts +19 -0
  571. package/dist/serverless.mjs +36 -0
  572. package/dist/serverless.mjs.map +1 -0
  573. package/dist/test-helpers/build-reference-contract.d.mts +14 -0
  574. package/dist/test-helpers/build-reference-contract.mjs +66 -0
  575. package/dist/test-helpers/build-reference-contract.mjs.map +1 -0
  576. package/dist/test-helpers/create-project.d.mts +1 -3
  577. package/dist/test-helpers/create-project.mjs +1 -2
  578. package/dist/test-helpers/database-contract.d.mts +0 -2
  579. package/dist/test-helpers/database-contract.mjs +37 -2
  580. package/dist/test-helpers/database-contract.mjs.map +1 -1
  581. package/dist/test-helpers/deferred.d.mts +37 -0
  582. package/dist/test-helpers/deferred.mjs +60 -0
  583. package/dist/test-helpers/deferred.mjs.map +1 -0
  584. package/dist/test-helpers/design-contract.d.mts +19 -0
  585. package/dist/test-helpers/design-contract.mjs +39 -0
  586. package/dist/test-helpers/design-contract.mjs.map +1 -0
  587. package/dist/test-helpers/fake-adapters.d.mts +0 -2
  588. package/dist/test-helpers/fake-adapters.mjs +1 -2
  589. package/dist/test-helpers/fake-database.d.mts +0 -2
  590. package/dist/test-helpers/fake-database.mjs +21 -4
  591. package/dist/test-helpers/fake-database.mjs.map +1 -1
  592. package/dist/test-helpers/fake-design.d.mts +41 -0
  593. package/dist/test-helpers/fake-design.mjs +113 -0
  594. package/dist/test-helpers/fake-design.mjs.map +1 -0
  595. package/dist/test-helpers/fake-notifier.d.mts +19 -0
  596. package/dist/test-helpers/fake-notifier.mjs +43 -0
  597. package/dist/test-helpers/fake-notifier.mjs.map +1 -0
  598. package/dist/test-helpers/fake-storage.d.mts +0 -2
  599. package/dist/test-helpers/fake-storage.mjs +1 -2
  600. package/dist/test-helpers/fake-tables.mjs +260 -69
  601. package/dist/test-helpers/fake-tables.mjs.map +1 -1
  602. package/dist/test-helpers/manual-clock.d.mts +25 -0
  603. package/dist/test-helpers/manual-clock.mjs +55 -0
  604. package/dist/test-helpers/manual-clock.mjs.map +1 -0
  605. package/dist/test-helpers/notifier-contract.d.mts +7 -3
  606. package/dist/test-helpers/notifier-contract.mjs +33 -5
  607. package/dist/test-helpers/notifier-contract.mjs.map +1 -1
  608. package/dist/test-helpers/queue-contract.d.mts +16 -4
  609. package/dist/test-helpers/queue-contract.mjs +196 -3
  610. package/dist/test-helpers/queue-contract.mjs.map +1 -1
  611. package/dist/test-helpers/queue-multi-instance.d.mts +18 -0
  612. package/dist/test-helpers/queue-multi-instance.mjs +205 -0
  613. package/dist/test-helpers/queue-multi-instance.mjs.map +1 -0
  614. package/dist/test-helpers/runner-contract.d.mts +0 -2
  615. package/dist/test-helpers/runner-contract.mjs +12 -2
  616. package/dist/test-helpers/runner-contract.mjs.map +1 -1
  617. package/dist/test-helpers/sql-chunks.mjs +25 -2
  618. package/dist/test-helpers/sql-chunks.mjs.map +1 -1
  619. package/dist/test-helpers/storage-contract.d.mts +0 -2
  620. package/dist/test-helpers/storage-contract.mjs +2 -3
  621. package/dist/test-helpers.d.mts +10 -4
  622. package/dist/test-helpers.mjs +9 -3
  623. package/dist/tracing.mjs +36 -6
  624. package/dist/tracing.mjs.map +1 -1
  625. package/dist/types.d.mts +14 -5
  626. package/dist/types.mjs +22 -6
  627. package/dist/types.mjs.map +1 -1
  628. package/dist/urls.d.mts +4 -2
  629. package/dist/urls.mjs +12 -4
  630. package/dist/urls.mjs.map +1 -1
  631. package/dist/utils/adaptive-concurrency.d.mts +64 -0
  632. package/dist/utils/adaptive-concurrency.mjs +148 -0
  633. package/dist/utils/adaptive-concurrency.mjs.map +1 -0
  634. package/dist/utils/connect-retry.d.mts +26 -0
  635. package/dist/utils/connect-retry.mjs +42 -0
  636. package/dist/utils/connect-retry.mjs.map +1 -0
  637. package/dist/utils/encrypt.d.mts +0 -2
  638. package/dist/utils/encrypt.mjs +1 -2
  639. package/dist/utils/environment.d.mts +17 -0
  640. package/dist/utils/environment.mjs +58 -0
  641. package/dist/utils/environment.mjs.map +1 -0
  642. package/dist/utils/hash.d.mts +0 -2
  643. package/dist/utils/hash.mjs +1 -2
  644. package/dist/utils/http.d.mts +29 -3
  645. package/dist/utils/http.mjs +100 -19
  646. package/dist/utils/http.mjs.map +1 -1
  647. package/dist/utils/map-adaptive.d.mts +13 -0
  648. package/dist/utils/map-adaptive.mjs +71 -0
  649. package/dist/utils/map-adaptive.mjs.map +1 -0
  650. package/dist/utils/map-limit.d.mts +8 -0
  651. package/dist/utils/map-limit.mjs +31 -0
  652. package/dist/utils/map-limit.mjs.map +1 -0
  653. package/dist/utils/mime.d.mts +0 -2
  654. package/dist/utils/mime.mjs +1 -2
  655. package/dist/utils/page-limiter.d.mts +35 -0
  656. package/dist/utils/page-limiter.mjs +55 -0
  657. package/dist/utils/page-limiter.mjs.map +1 -0
  658. package/dist/utils/path-security.d.mts +0 -2
  659. package/dist/utils/path-security.mjs +2 -4
  660. package/dist/utils/path-security.mjs.map +1 -1
  661. package/dist/utils/public-origin.d.mts +18 -0
  662. package/dist/utils/public-origin.mjs +74 -0
  663. package/dist/utils/public-origin.mjs.map +1 -0
  664. package/dist/utils/redact.d.mts +0 -2
  665. package/dist/utils/redact.mjs +4 -2
  666. package/dist/utils/redact.mjs.map +1 -1
  667. package/dist/utils/remote-environment.d.mts +76 -0
  668. package/dist/utils/remote-environment.mjs +163 -0
  669. package/dist/utils/remote-environment.mjs.map +1 -0
  670. package/dist/utils/resources.d.mts +62 -0
  671. package/dist/utils/resources.mjs +121 -0
  672. package/dist/utils/resources.mjs.map +1 -0
  673. package/dist/utils/screenshot-store.d.mts +13 -0
  674. package/dist/utils/screenshot-store.mjs +40 -0
  675. package/dist/utils/screenshot-store.mjs.map +1 -0
  676. package/dist/utils/session-gate.d.mts +23 -0
  677. package/dist/utils/session-gate.mjs +69 -0
  678. package/dist/utils/session-gate.mjs.map +1 -0
  679. package/dist/utils/storybook-token.d.mts +89 -0
  680. package/dist/utils/storybook-token.mjs +135 -0
  681. package/dist/utils/storybook-token.mjs.map +1 -0
  682. package/dist/utils/storybook-url.d.mts +13 -0
  683. package/dist/utils/storybook-url.mjs +20 -0
  684. package/dist/utils/storybook-url.mjs.map +1 -0
  685. package/dist/utils/timing.d.mts +0 -2
  686. package/dist/utils/timing.mjs +2 -3
  687. package/dist/utils/timing.mjs.map +1 -1
  688. package/dist/utils/ulid.d.mts +0 -2
  689. package/dist/utils/ulid.mjs +1 -2
  690. package/dist/utils/zip-read.mjs +128 -0
  691. package/dist/utils/zip-read.mjs.map +1 -0
  692. package/dist/utils/zip-write.d.mts +16 -0
  693. package/dist/utils/zip-write.mjs +187 -0
  694. package/dist/utils/zip-write.mjs.map +1 -0
  695. package/dist/utils.d.mts +17 -3
  696. package/dist/utils.mjs +18 -4
  697. package/package.json +35 -26
  698. package/dist/adapter/capture-runner.mjs.map +0 -1
  699. package/dist/adapter/git-host/comments.d.mts +0 -41
  700. package/dist/adapter/git-host/comments.mjs +0 -54
  701. package/dist/adapter/git-host/comments.mjs.map +0 -1
  702. package/dist/adapter/git-host/helpers.d.mts +0 -13
  703. package/dist/adapter/git-host/helpers.mjs +0 -30
  704. package/dist/adapter/git-host/helpers.mjs.map +0 -1
  705. package/dist/adapter/git-host.d.mts +0 -42
  706. package/dist/capture/queue.d.mts +0 -38
  707. package/dist/capture/queue.mjs +0 -98
  708. package/dist/capture/queue.mjs.map +0 -1
  709. package/dist/capture/recovery.d.mts +0 -21
  710. package/dist/capture/recovery.mjs +0 -83
  711. package/dist/capture/recovery.mjs.map +0 -1
  712. package/dist/capture/status-fanout.mjs +0 -72
  713. package/dist/capture/status-fanout.mjs.map +0 -1
  714. package/dist/diff/engine.d.mts +0 -16
  715. package/dist/diff/engine.mjs +0 -87
  716. package/dist/diff/engine.mjs.map +0 -1
  717. package/dist/diff/options.d.mts +0 -42
  718. package/dist/diff/options.mjs +0 -15
  719. package/dist/diff/options.mjs.map +0 -1
  720. package/dist/models/status-config.d.mts +0 -38
  721. package/dist/models/status-config.mjs +0 -63
  722. package/dist/models/status-config.mjs.map +0 -1
  723. package/dist/schema/status-config.d.mts +0 -16
  724. package/dist/test-helpers/git-contract.d.mts +0 -11
  725. package/dist/test-helpers/git-contract.mjs +0 -34
  726. package/dist/test-helpers/git-contract.mjs.map +0 -1
  727. /package/dist/adapter/{git-host.mjs → oauth.mjs} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending.mjs","names":[],"sources":["../../src/retention/pending.ts"],"sourcesContent":["/**\n * Pending builds nobody finishes (ADR 0013, decision 9; ADR 0009, decision 4). A `pending` build\n * waits for a review verdict, so the terminal-status rule never reaches it; this rule gives it an\n * exit. A pending build is purgeable once its last activity is older than the retention window:\n * the later of its own last update (upload) and its newest comment or verdict. A build superseded\n * by a newer one on its branch, with no attempts, verdicts or comments, has no activity beyond\n * its upload, so it leaves at the same window; the build under review is never cut short.\n */\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { BuildReviewModel } from \"../models/build-review.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { CommentModel } from \"../models/comment.ts\";\nimport type { Build } from \"../schema/build.ts\";\n\n/** Why a pending build is purgeable. */\nexport type PendingReason = \"superseded\" | \"idle\";\n\n/** A pending build the retention pass may remove. */\nexport interface PendingCandidate {\n buildId: string;\n reason: PendingReason;\n}\n\n/**\n * The project's pending builds whose last activity (upload, comment or verdict) is before\n * `cutoff`. `capture_attempts` need no check: a build capture worked on leaves `pending`.\n */\nexport async function pendingCandidates(\n db: DatabaseAdapter,\n projectId: string,\n cutoff: string,\n): Promise<PendingCandidate[]> {\n const builds = new BuildModel(db);\n const pending = await builds.list(projectId, { status: \"pending\" });\n if (pending.length === 0) {\n return [];\n }\n const all = await builds.list(projectId);\n const found = await Promise.all(\n pending.map(async (build): Promise<PendingCandidate | null> => {\n const activity = await activityOf(db, build);\n if (activity.last >= cutoff) {\n return null;\n }\n const superseded = !activity.touched && hasNewerOnBranch(all, build);\n return { buildId: build.id, reason: superseded ? \"superseded\" : \"idle\" };\n }),\n );\n return found.filter((candidate) => candidate !== null);\n}\n\ninterface Activity {\n /** Newest of the build's own update, its comments and its verdicts. */\n last: string;\n /** A person commented on or gave a verdict to the build. */\n touched: boolean;\n}\n\nasync function activityOf(db: DatabaseAdapter, build: Build): Promise<Activity> {\n const [comments, reviews] = await Promise.all([\n new CommentModel(db).listByBuild(build.id),\n new BuildReviewModel(db).listByBuild(build.id),\n ]);\n const stamps = [\n build.updatedAt,\n ...comments.map((item) => stampOf(item)),\n ...reviews.map((item) => stampOf(item)),\n ];\n return {\n last: stamps.toSorted().at(-1) ?? build.updatedAt,\n touched: comments.length > 0 || reviews.length > 0,\n };\n}\n\nfunction stampOf(item: { createdAt: string }): string {\n return item.createdAt;\n}\n\nfunction hasNewerOnBranch(all: readonly Build[], build: Build): boolean {\n return all.some(\n (other) =>\n other.id !== build.id &&\n other.gitBranch === build.gitBranch &&\n other.createdAt > build.createdAt,\n );\n}\n"],"mappings":";;;;;;;;;;;AA2BA,eAAsB,kBACpB,IACA,WACA,QAC6B;CAC7B,MAAM,SAAS,IAAI,WAAW,EAAE;CAChC,MAAM,UAAU,MAAM,OAAO,KAAK,WAAW,EAAE,QAAQ,UAAU,CAAC;CAClE,IAAI,QAAQ,WAAW,GACrB,OAAO,CAAC;CAEV,MAAM,MAAM,MAAM,OAAO,KAAK,SAAS;CAWvC,QAAO,MAVa,QAAQ,IAC1B,QAAQ,IAAI,OAAO,UAA4C;EAC7D,MAAM,WAAW,MAAM,WAAW,IAAI,KAAK;EAC3C,IAAI,SAAS,QAAQ,QACnB,OAAO;EAET,MAAM,aAAa,CAAC,SAAS,WAAW,iBAAiB,KAAK,KAAK;EACnE,OAAO;GAAE,SAAS,MAAM;GAAI,QAAQ,aAAa,eAAe;EAAO;CACzE,CAAC,CACH,EAAA,CACa,QAAQ,cAAc,cAAc,IAAI;AACvD;AASA,eAAe,WAAW,IAAqB,OAAiC;CAC9E,MAAM,CAAC,UAAU,WAAW,MAAM,QAAQ,IAAI,CAC5C,IAAI,aAAa,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE,GACzC,IAAI,iBAAiB,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE,CAC/C,CAAC;CAMD,OAAO;EACL,MAAM;GALN,MAAM;GACN,GAAG,SAAS,KAAK,SAAS,QAAQ,IAAI,CAAC;GACvC,GAAG,QAAQ,KAAK,SAAS,QAAQ,IAAI,CAAC;EAG3B,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,KAAK,MAAM;EACxC,SAAS,SAAS,SAAS,KAAK,QAAQ,SAAS;CACnD;AACF;AAEA,SAAS,QAAQ,MAAqC;CACpD,OAAO,KAAK;AACd;AAEA,SAAS,iBAAiB,KAAuB,OAAuB;CACtE,OAAO,IAAI,MACR,UACC,MAAM,OAAO,MAAM,MACnB,MAAM,cAAc,MAAM,aAC1B,MAAM,YAAY,MAAM,SAC5B;AACF"}
@@ -0,0 +1,28 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ // src/retention/published-gzip-sweep.ts
5
+ const PREFIX = "derived/published-gzip/";
6
+ const MAX_PER_SWEEP = 128;
7
+ const cursors = /* @__PURE__ */ new WeakMap();
8
+ /** Reclaim disposable orphan variants; never delete content or other storage keys. */
9
+ async function sweepPublishedGzip(storage) {
10
+ const keys = (await storage.list(PREFIX)).filter((key) => /^derived\/published-gzip\/[a-f0-9]{64}\.gz$/u.test(key)).toSorted();
11
+ if (keys.length === 0) return;
12
+ const cursor = cursors.get(storage);
13
+ const next = cursor ? keys.findIndex((key) => key > cursor) : 0;
14
+ const start = Math.max(0, next);
15
+ const batch = [...keys.slice(start), ...keys.slice(0, start)].slice(0, MAX_PER_SWEEP);
16
+ for (const key of batch) await removeOrphan(storage, key);
17
+ const last = batch.at(-1);
18
+ if (last) cursors.set(storage, last);
19
+ }
20
+ async function removeOrphan(storage, key) {
21
+ const hash = key.slice(23, -3);
22
+ try {
23
+ if (!await storage.exists(`content/${hash}`)) await storage.delete(key);
24
+ } catch {}
25
+ }
26
+ export { sweepPublishedGzip };
27
+
28
+ //# sourceMappingURL=published-gzip-sweep.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"published-gzip-sweep.mjs","names":[],"sources":["../../src/retention/published-gzip-sweep.ts"],"sourcesContent":["import type { StorageAdapter } from \"../adapters/storage.ts\";\n\nconst PREFIX = \"derived/published-gzip/\";\nconst MAX_PER_SWEEP = 128;\nconst cursors = new WeakMap<StorageAdapter, string>();\n\n/** Reclaim disposable orphan variants; never delete content or other storage keys. */\nexport async function sweepPublishedGzip(storage: StorageAdapter): Promise<void> {\n const keys = (await storage.list(PREFIX))\n .filter((key) => /^derived\\/published-gzip\\/[a-f0-9]{64}\\.gz$/u.test(key))\n .toSorted();\n if (keys.length === 0) return;\n const cursor = cursors.get(storage);\n const next = cursor ? keys.findIndex((key) => key > cursor) : 0;\n const start = Math.max(0, next);\n const batch = [...keys.slice(start), ...keys.slice(0, start)].slice(0, MAX_PER_SWEEP);\n for (const key of batch) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- bounded, sequential storage work\n await removeOrphan(storage, key);\n }\n const last = batch.at(-1);\n if (last) cursors.set(storage, last);\n}\n\nasync function removeOrphan(storage: StorageAdapter, key: string): Promise<void> {\n const hash = key.slice(PREFIX.length, -3);\n try {\n if (!(await storage.exists(`content/${hash}`))) await storage.delete(key);\n } catch {\n // Retention is best effort; a missing variant can always be regenerated.\n }\n}\n"],"mappings":";;;;AAEA,MAAM,SAAS;AACf,MAAM,gBAAgB;AACtB,MAAM,0BAAU,IAAI,QAAgC;;AAGpD,eAAsB,mBAAmB,SAAwC;CAC/E,MAAM,QAAQ,MAAM,QAAQ,KAAK,MAAM,EAAA,CACpC,QAAQ,QAAQ,+CAA+C,KAAK,GAAG,CAAC,CAAC,CACzE,SAAS;CACZ,IAAI,KAAK,WAAW,GAAG;CACvB,MAAM,SAAS,QAAQ,IAAI,OAAO;CAClC,MAAM,OAAO,SAAS,KAAK,WAAW,QAAQ,MAAM,MAAM,IAAI;CAC9D,MAAM,QAAQ,KAAK,IAAI,GAAG,IAAI;CAC9B,MAAM,QAAQ,CAAC,GAAG,KAAK,MAAM,KAAK,GAAG,GAAG,KAAK,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,aAAa;CACpF,KAAK,MAAM,OAAO,OAEhB,MAAM,aAAa,SAAS,GAAG;CAEjC,MAAM,OAAO,MAAM,GAAG,EAAE;CACxB,IAAI,MAAM,QAAQ,IAAI,SAAS,IAAI;AACrC;AAEA,eAAe,aAAa,SAAyB,KAA4B;CAC/E,MAAM,OAAO,IAAI,MAAM,IAAe,EAAE;CACxC,IAAI;EACF,IAAI,CAAE,MAAM,QAAQ,OAAO,WAAW,MAAM,GAAI,MAAM,QAAQ,OAAO,GAAG;CAC1E,QAAQ,CAER;AACF"}
@@ -0,0 +1,49 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { DatabaseAdapter } from "../adapter/database.mjs";
4
+ import { StorageAdapter } from "../adapter/storage.mjs";
5
+ import { Logger } from "pino";
6
+ import { Table } from "drizzle-orm";
7
+ /** Longest an unreferenced screenshot is kept after the sweep first sees it (default 24 h). */
8
+ declare const SWEEP_GRACE_MS: number;
9
+ /** Tables the sweep reads references and in-flight builds from. */
10
+ interface SweepTables {
11
+ snapshots: Table;
12
+ baselines: Table;
13
+ builds: Table;
14
+ }
15
+ /** Inputs for {@link sweepScreenshotContent}. */
16
+ interface SweepDeps {
17
+ db: DatabaseAdapter;
18
+ storage: StorageAdapter;
19
+ tables: SweepTables;
20
+ logger?: Logger;
21
+ }
22
+ /** What one sweep did. */
23
+ interface SweepResult {
24
+ /** Objects deleted. */
25
+ removed: number;
26
+ /** Unreferenced objects still inside their grace period. */
27
+ pending: number;
28
+ }
29
+ /**
30
+ * Delete content-addressed screenshots that no snapshot or baseline points at any more.
31
+ *
32
+ * An object is deleted only after it has been unreferenced across two sweeps at least `graceMs`
33
+ * apart (first-seen times live in a marker object, so no schema is needed and the storage
34
+ * adapter needs no modification times). A capture writes its screenshot before the rows that
35
+ * reference it, so a single look is never enough; each candidate is also re-checked against the
36
+ * database immediately before it is deleted.
37
+ *
38
+ * Reusing an object that already exists writes nothing, so the sweep cannot see it by looking at
39
+ * storage. Deletion is therefore deferred (objects stay pending, first-seen times kept) while the
40
+ * project has reuse in flight: a build still capturing, or a reuse claim from a baseline import or
41
+ * approval ({@link withContentClaim}). Both checks are bounded by the grace period, so a build
42
+ * stuck in `capturing` or a claim left by a crash delays reclaiming storage, never forever.
43
+ */
44
+ declare function sweepScreenshotContent(deps: SweepDeps, projectId: string, options?: {
45
+ now?: number;
46
+ graceMs?: number;
47
+ }): Promise<SweepResult>;
48
+ export { SWEEP_GRACE_MS, SweepDeps, SweepResult, SweepTables, sweepScreenshotContent };
49
+ //# sourceMappingURL=screenshot-sweep.d.mts.map
@@ -0,0 +1,127 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { col } from "../models/columns.mjs";
5
+ import { contentDir } from "../paths.mjs";
6
+ import { CLAIMS_DIR } from "../utils/screenshot-store.mjs";
7
+ import { and, eq, gte, inArray, like } from "drizzle-orm";
8
+ // src/retention/screenshot-sweep.ts
9
+ /** Mark-and-sweep for a project's content-addressed screenshots. */
10
+ /** Longest an unreferenced screenshot is kept after the sweep first sees it (default 24 h). */
11
+ const SWEEP_GRACE_MS = 864e5;
12
+ /** Marker object remembering when each unreferenced screenshot was first seen. */
13
+ const MARKER = ".sweep.json";
14
+ /**
15
+ * Build statuses during which a capture may have reused an existing object (the "already exists,
16
+ * skip the write" path) without yet writing the snapshot rows that reference it.
17
+ */
18
+ const ACTIVE_BUILD_STATUSES = [
19
+ "pending",
20
+ "capturing",
21
+ "comparing"
22
+ ];
23
+ /**
24
+ * Delete content-addressed screenshots that no snapshot or baseline points at any more.
25
+ *
26
+ * An object is deleted only after it has been unreferenced across two sweeps at least `graceMs`
27
+ * apart (first-seen times live in a marker object, so no schema is needed and the storage
28
+ * adapter needs no modification times). A capture writes its screenshot before the rows that
29
+ * reference it, so a single look is never enough; each candidate is also re-checked against the
30
+ * database immediately before it is deleted.
31
+ *
32
+ * Reusing an object that already exists writes nothing, so the sweep cannot see it by looking at
33
+ * storage. Deletion is therefore deferred (objects stay pending, first-seen times kept) while the
34
+ * project has reuse in flight: a build still capturing, or a reuse claim from a baseline import or
35
+ * approval ({@link withContentClaim}). Both checks are bounded by the grace period, so a build
36
+ * stuck in `capturing` or a claim left by a crash delays reclaiming storage, never forever.
37
+ */
38
+ async function sweepScreenshotContent(deps, projectId, options = {}) {
39
+ const dir = `${contentDir(projectId)}/`;
40
+ const unreferenced = await findUnreferenced(deps, projectId, dir);
41
+ const firstSeen = await readMarker(deps.storage, dir);
42
+ const outcome = await deleteDue(deps, projectId, unreferenced, firstSeen, {
43
+ now: options.now ?? Date.now(),
44
+ graceMs: options.graceMs ?? 864e5
45
+ });
46
+ await writeMarker(deps.storage, dir, outcome.pending, firstSeen);
47
+ if (outcome.removed > 0) deps.logger?.info({
48
+ projectId,
49
+ removed: outcome.removed
50
+ }, "removed unreferenced screenshots");
51
+ return {
52
+ removed: outcome.removed,
53
+ pending: Object.keys(outcome.pending).length
54
+ };
55
+ }
56
+ async function findUnreferenced(deps, projectId, dir) {
57
+ const objects = (await deps.storage.list(dir)).filter((path) => path.startsWith(dir) && path.endsWith(".png"));
58
+ const referenced = await referencedPaths(deps, projectId);
59
+ return objects.filter((path) => !referenced.has(path));
60
+ }
61
+ /** Delete what has been unreferenced for the whole grace period; return the rest with first-seen times. */
62
+ async function deleteDue(deps, projectId, unreferenced, firstSeen, clock) {
63
+ const pending = {};
64
+ let removed = 0;
65
+ for (const path of unreferenced) {
66
+ const seen = firstSeen[path] ?? clock.now;
67
+ if (clock.now - seen >= clock.graceMs && !await isHeld(deps, projectId, path, clock)) {
68
+ await deps.storage.delete(path);
69
+ removed += 1;
70
+ } else pending[path] = seen;
71
+ }
72
+ return {
73
+ removed,
74
+ pending
75
+ };
76
+ }
77
+ async function referencedPaths(deps, projectId) {
78
+ const rows = await Promise.all([deps.tables.snapshots, deps.tables.baselines].map(async (table) => await deps.db.list(table, { where: and(eq(col(table, "projectId"), projectId), like(col(table, "screenshotPath"), `${contentDir(projectId)}/%`)) })));
79
+ return new Set(rows.flat().map((row) => row.screenshotPath));
80
+ }
81
+ /** Whether `path` must be kept now: referenced by a row, or reuse may be in flight. */
82
+ async function isHeld(deps, projectId, path, clock) {
83
+ return await isReferenced(deps, projectId, path) || await hasActiveBuild(deps, projectId, clock) || await hasActiveClaim(deps.storage, projectId, clock);
84
+ }
85
+ /** A build of the project that is still capturing and was updated within the grace period. */
86
+ async function hasActiveBuild(deps, projectId, clock) {
87
+ const table = deps.tables.builds;
88
+ return (await deps.db.list(table, {
89
+ where: and(eq(col(table, "projectId"), projectId), inArray(col(table, "status"), ACTIVE_BUILD_STATUSES), gte(col(table, "updatedAt"), new Date(clock.now - clock.graceMs).toISOString())),
90
+ limit: 1
91
+ })).length > 0;
92
+ }
93
+ /** A reuse claim younger than the grace period; older ones (left by a crash) are ignored. */
94
+ async function hasActiveClaim(storage, projectId, clock) {
95
+ const claims = await storage.list(`${contentDir(projectId)}/${CLAIMS_DIR}/`);
96
+ return (await Promise.all(claims.map(async (path) => await claimTime(storage, path)))).some((at) => at === null || clock.now - at < clock.graceMs);
97
+ }
98
+ /** When a claim was made; null when it cannot be read (treated as active: fail safe). */
99
+ async function claimTime(storage, path) {
100
+ try {
101
+ const parsed = JSON.parse((await storage.read(path)).toString("utf8"));
102
+ return typeof parsed.at === "number" ? parsed.at : null;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ async function isReferenced(deps, projectId, path) {
108
+ return (await Promise.all([deps.tables.snapshots, deps.tables.baselines].map(async (table) => (await deps.db.list(table, {
109
+ where: and(eq(col(table, "projectId"), projectId), eq(col(table, "screenshotPath"), path)),
110
+ limit: 1
111
+ })).length > 0))).some(Boolean);
112
+ }
113
+ async function readMarker(storage, dir) {
114
+ try {
115
+ if (!await storage.exists(`${dir}${MARKER}`)) return {};
116
+ const parsed = JSON.parse((await storage.read(`${dir}${MARKER}`)).toString("utf8"));
117
+ return parsed && typeof parsed === "object" ? parsed : {};
118
+ } catch {
119
+ return {};
120
+ }
121
+ }
122
+ async function writeMarker(storage, dir, next, previous) {
123
+ if (JSON.stringify(next) !== JSON.stringify(previous)) await storage.write(`${dir}${MARKER}`, Buffer.from(JSON.stringify(next)));
124
+ }
125
+ export { SWEEP_GRACE_MS, sweepScreenshotContent };
126
+
127
+ //# sourceMappingURL=screenshot-sweep.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot-sweep.mjs","names":[],"sources":["../../src/retention/screenshot-sweep.ts"],"sourcesContent":["/** Mark-and-sweep for a project's content-addressed screenshots. */\nimport { and, eq, gte, inArray, like } from \"drizzle-orm\";\nimport type { Table } from \"drizzle-orm\";\nimport type { Logger } from \"pino\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { col } from \"../models/columns.ts\";\nimport { contentDir } from \"../utils/paths.ts\";\nimport { CLAIMS_DIR } from \"../utils/screenshot-store.ts\";\n\n/** Longest an unreferenced screenshot is kept after the sweep first sees it (default 24 h). */\nexport const SWEEP_GRACE_MS = 24 * 60 * 60 * 1000;\n\n/** Marker object remembering when each unreferenced screenshot was first seen. */\nconst MARKER = \".sweep.json\";\n\n/**\n * Build statuses during which a capture may have reused an existing object (the \"already exists,\n * skip the write\" path) without yet writing the snapshot rows that reference it.\n */\nconst ACTIVE_BUILD_STATUSES = [\"pending\", \"capturing\", \"comparing\"];\n\n/** Tables the sweep reads references and in-flight builds from. */\nexport interface SweepTables {\n snapshots: Table;\n baselines: Table;\n builds: Table;\n}\n\n/** Inputs for {@link sweepScreenshotContent}. */\nexport interface SweepDeps {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n tables: SweepTables;\n logger?: Logger;\n}\n\n/** What one sweep did. */\nexport interface SweepResult {\n /** Objects deleted. */\n removed: number;\n /** Unreferenced objects still inside their grace period. */\n pending: number;\n}\n\n/**\n * Delete content-addressed screenshots that no snapshot or baseline points at any more.\n *\n * An object is deleted only after it has been unreferenced across two sweeps at least `graceMs`\n * apart (first-seen times live in a marker object, so no schema is needed and the storage\n * adapter needs no modification times). A capture writes its screenshot before the rows that\n * reference it, so a single look is never enough; each candidate is also re-checked against the\n * database immediately before it is deleted.\n *\n * Reusing an object that already exists writes nothing, so the sweep cannot see it by looking at\n * storage. Deletion is therefore deferred (objects stay pending, first-seen times kept) while the\n * project has reuse in flight: a build still capturing, or a reuse claim from a baseline import or\n * approval ({@link withContentClaim}). Both checks are bounded by the grace period, so a build\n * stuck in `capturing` or a claim left by a crash delays reclaiming storage, never forever.\n */\nexport async function sweepScreenshotContent(\n deps: SweepDeps,\n projectId: string,\n options: { now?: number; graceMs?: number } = {},\n): Promise<SweepResult> {\n const dir = `${contentDir(projectId)}/`;\n const unreferenced = await findUnreferenced(deps, projectId, dir);\n const firstSeen = await readMarker(deps.storage, dir);\n const outcome = await deleteDue(deps, projectId, unreferenced, firstSeen, {\n now: options.now ?? Date.now(),\n graceMs: options.graceMs ?? SWEEP_GRACE_MS,\n });\n await writeMarker(deps.storage, dir, outcome.pending, firstSeen);\n if (outcome.removed > 0) {\n deps.logger?.info({ projectId, removed: outcome.removed }, \"removed unreferenced screenshots\");\n }\n return { removed: outcome.removed, pending: Object.keys(outcome.pending).length };\n}\n\nasync function findUnreferenced(\n deps: SweepDeps,\n projectId: string,\n dir: string,\n): Promise<string[]> {\n // Only this project's content screenshots: the prefix guards against an adapter listing keys\n // in another form (such as OS-separated paths), which would otherwise match no reference.\n const objects = (await deps.storage.list(dir)).filter(\n (path) => path.startsWith(dir) && path.endsWith(\".png\"),\n );\n const referenced = await referencedPaths(deps, projectId);\n return objects.filter((path) => !referenced.has(path));\n}\n\n/** Delete what has been unreferenced for the whole grace period; return the rest with first-seen times. */\nasync function deleteDue(\n deps: SweepDeps,\n projectId: string,\n unreferenced: string[],\n firstSeen: Record<string, number>,\n clock: { now: number; graceMs: number },\n): Promise<{ removed: number; pending: Record<string, number> }> {\n const pending: Record<string, number> = {};\n let removed = 0;\n for (const path of unreferenced) {\n const seen = firstSeen[path] ?? clock.now;\n // oxlint-disable-next-line eslint/no-await-in-loop -- re-check and delete one object at a time\n if (clock.now - seen >= clock.graceMs && !(await isHeld(deps, projectId, path, clock))) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- see above\n await deps.storage.delete(path);\n removed += 1;\n } else {\n pending[path] = seen;\n }\n }\n return { removed, pending };\n}\n\nasync function referencedPaths(deps: SweepDeps, projectId: string): Promise<Set<string>> {\n const rows = await Promise.all(\n [deps.tables.snapshots, deps.tables.baselines].map(\n async (table) =>\n (await deps.db.list(table, {\n where: and(\n eq(col(table, \"projectId\"), projectId),\n like(col(table, \"screenshotPath\"), `${contentDir(projectId)}/%`),\n ),\n })) as unknown as { screenshotPath: string }[],\n ),\n );\n return new Set(rows.flat().map((row) => row.screenshotPath));\n}\n\n/** Whether `path` must be kept now: referenced by a row, or reuse may be in flight. */\nasync function isHeld(\n deps: SweepDeps,\n projectId: string,\n path: string,\n clock: { now: number; graceMs: number },\n): Promise<boolean> {\n return (\n (await isReferenced(deps, projectId, path)) ||\n (await hasActiveBuild(deps, projectId, clock)) ||\n (await hasActiveClaim(deps.storage, projectId, clock))\n );\n}\n\n/** A build of the project that is still capturing and was updated within the grace period. */\nasync function hasActiveBuild(\n deps: SweepDeps,\n projectId: string,\n clock: { now: number; graceMs: number },\n): Promise<boolean> {\n const table = deps.tables.builds;\n const rows = await deps.db.list(table, {\n where: and(\n eq(col(table, \"projectId\"), projectId),\n inArray(col(table, \"status\"), ACTIVE_BUILD_STATUSES),\n gte(col(table, \"updatedAt\"), new Date(clock.now - clock.graceMs).toISOString()),\n ),\n limit: 1,\n });\n return rows.length > 0;\n}\n\n/** A reuse claim younger than the grace period; older ones (left by a crash) are ignored. */\nasync function hasActiveClaim(\n storage: StorageAdapter,\n projectId: string,\n clock: { now: number; graceMs: number },\n): Promise<boolean> {\n const claims = await storage.list(`${contentDir(projectId)}/${CLAIMS_DIR}/`);\n const times = await Promise.all(claims.map(async (path) => await claimTime(storage, path)));\n return times.some((at) => at === null || clock.now - at < clock.graceMs);\n}\n\n/** When a claim was made; null when it cannot be read (treated as active: fail safe). */\nasync function claimTime(storage: StorageAdapter, path: string): Promise<number | null> {\n try {\n const parsed = JSON.parse((await storage.read(path)).toString(\"utf8\")) as { at?: unknown };\n return typeof parsed.at === \"number\" ? parsed.at : null;\n } catch {\n return null;\n }\n}\n\nasync function isReferenced(deps: SweepDeps, projectId: string, path: string): Promise<boolean> {\n const hits = await Promise.all(\n [deps.tables.snapshots, deps.tables.baselines].map(\n async (table) =>\n (\n await deps.db.list(table, {\n where: and(\n eq(col(table, \"projectId\"), projectId),\n eq(col(table, \"screenshotPath\"), path),\n ),\n limit: 1,\n })\n ).length > 0,\n ),\n );\n return hits.some(Boolean);\n}\n\nasync function readMarker(storage: StorageAdapter, dir: string): Promise<Record<string, number>> {\n try {\n if (!(await storage.exists(`${dir}${MARKER}`))) {\n return {};\n }\n const parsed = JSON.parse((await storage.read(`${dir}${MARKER}`)).toString(\"utf8\")) as unknown;\n return parsed && typeof parsed === \"object\" ? (parsed as Record<string, number>) : {};\n } catch {\n return {};\n }\n}\n\nasync function writeMarker(\n storage: StorageAdapter,\n dir: string,\n next: Record<string, number>,\n previous: Record<string, number>,\n): Promise<void> {\n if (JSON.stringify(next) !== JSON.stringify(previous)) {\n await storage.write(`${dir}${MARKER}`, Buffer.from(JSON.stringify(next)));\n }\n}\n"],"mappings":"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"}
@@ -3,9 +3,10 @@ import __tsdown_shims_url from 'node:url';
3
3
  import { DatabaseAdapter } from "./adapter/database.mjs";
4
4
  import { StorageAdapter } from "./adapter/storage.mjs";
5
5
  import { Project } from "./schema/project.mjs";
6
+ import { SWEEP_GRACE_MS, SweepDeps, SweepResult, SweepTables, sweepScreenshotContent } from "./retention/screenshot-sweep.mjs";
7
+ import { AiPurgeResult, STALE_RUN_MS, purgeAiData } from "./retention/ai.mjs";
6
8
  import { Logger } from "pino";
7
9
  import { Table } from "drizzle-orm";
8
- //#region src/retention/purge.d.ts
9
10
  /** Table handles for retention queries. */
10
11
  interface RetentionTables {
11
12
  builds: Table;
@@ -18,8 +19,20 @@ declare class Retention {
18
19
  private readonly storage;
19
20
  private readonly tables;
20
21
  private readonly logger?;
21
- constructor(db: DatabaseAdapter, storage: StorageAdapter, tables: RetentionTables, logger?: Logger | undefined);
22
+ constructor(db: DatabaseAdapter, storage: StorageAdapter, tables: RetentionTables, logger?: Logger);
22
23
  purge(project: Project, options: PurgeOptions): Promise<PurgeResult>;
24
+ /**
25
+ * The builds a purge removes: terminal builds past the window (minus the latest per branch when
26
+ * asked), and pending builds idle past it (ADR 0013, decision 9). Persistent builds stay.
27
+ */
28
+ private selectBuilds;
29
+ /** Reclaim screenshots no snapshot or baseline references any more; never blocks the purge. */
30
+ private sweepScreenshots;
31
+ /**
32
+ * Drop finished queue jobs older than the build cutoff (they have no foreign key to a build, and
33
+ * a finished job is only history); never blocks the purge. Queued and running jobs are kept.
34
+ */
35
+ private purgeFinishedJobs;
23
36
  private removeBuild;
24
37
  private latestPerBranch;
25
38
  private deleteBuildFiles;
@@ -40,6 +53,5 @@ interface BranchGcResult {
40
53
  removedBranches: number;
41
54
  removedBaselines: number;
42
55
  }
43
- //#endregion
44
- export { BranchGcResult, PurgeOptions, PurgeResult, Retention, RetentionTables };
56
+ export { type AiPurgeResult, BranchGcResult, PurgeOptions, PurgeResult, Retention, RetentionTables, STALE_RUN_MS, SWEEP_GRACE_MS, type SweepDeps, type SweepResult, type SweepTables, purgeAiData, sweepScreenshotContent };
45
57
  //# sourceMappingURL=retention.d.mts.map
@@ -2,11 +2,21 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { TERMINAL_BUILD_STATUSES } from "./types.mjs";
5
+ import { col } from "./models/columns.mjs";
5
6
  import { BuildModel } from "./models/build.mjs";
7
+ import { publishedGzipPath } from "./paths.mjs";
6
8
  import { BaselineModel } from "./models/baseline.mjs";
9
+ import { CaptureJobModel } from "./models/capture-job.mjs";
10
+ import { BuildComponentModel } from "./models/build-component.mjs";
11
+ import { InsightModel } from "./models/insight.mjs";
7
12
  import { LabelModel } from "./models/label.mjs";
8
- import { and, desc, eq, getTableColumns, inArray, lt } from "drizzle-orm";
9
- //#region src/retention/purge.ts
13
+ import { PackageUsageModel } from "./models/usage.mjs";
14
+ import { pendingCandidates } from "./retention/pending.mjs";
15
+ import { sweepPublishedGzip } from "./retention/published-gzip-sweep.mjs";
16
+ import { SWEEP_GRACE_MS, sweepScreenshotContent } from "./retention/screenshot-sweep.mjs";
17
+ import { STALE_RUN_MS, purgeAiData } from "./retention/ai.mjs";
18
+ import { and, desc, eq, inArray, lt } from "drizzle-orm";
19
+ // src/retention/purge.ts
10
20
  /** Removes expired transient builds while keeping baselines and persistent builds. */
11
21
  var Retention = class {
12
22
  db;
@@ -21,38 +31,86 @@ var Retention = class {
21
31
  }
22
32
  async purge(project, options) {
23
33
  const cutoff = (/* @__PURE__ */ new Date(Date.now() - options.ttlDays * 864e5)).toISOString();
24
- const candidates = await this.db.list(this.tables.builds, { where: and(eq(getTableColumns(this.tables.builds)["projectId"], project.id), inArray(getTableColumns(this.tables.builds)["status"], [...TERMINAL_BUILD_STATUSES]), lt(getTableColumns(this.tables.builds)["updatedAt"], cutoff)) });
25
- const keep = options.keepLatestPerBranch ? await this.latestPerBranch(project.id) : /* @__PURE__ */ new Set();
26
- const labelModel = new LabelModel(this.db, this.tables);
27
- const buildIds = (await Promise.all(candidates.map(async (build) => {
28
- const buildId = build.id;
29
- if (keep.has(buildId) || await labelModel.hasPersistent(project.id, buildId)) return null;
30
- return buildId;
31
- }))).filter((id) => id !== null);
34
+ const { buildIds, pending } = await this.selectBuilds(project, options, cutoff);
32
35
  const results = await Promise.all(buildIds.map(async (buildId) => ({
33
36
  files: await this.deleteBuildFiles(project.id, buildId),
34
37
  removed: await this.removeBuild(buildId)
35
38
  })));
36
39
  const removedBuilds = results.filter((r) => r.removed).length;
37
40
  const removedFiles = results.reduce((sum, r) => sum + r.files, 0);
41
+ await this.sweepScreenshots(project);
42
+ const removedJobs = await this.purgeFinishedJobs(cutoff);
38
43
  this.logger?.info({
39
44
  projectId: project.id,
40
45
  removedBuilds,
41
- removedFiles
46
+ removedFiles,
47
+ removedJobs,
48
+ removedPending: pending
42
49
  }, "build retention purge complete");
43
50
  return {
44
51
  removedBuilds,
45
52
  removedFiles
46
53
  };
47
54
  }
55
+ /**
56
+ * The builds a purge removes: terminal builds past the window (minus the latest per branch when
57
+ * asked), and pending builds idle past it (ADR 0013, decision 9). Persistent builds stay.
58
+ */
59
+ async selectBuilds(project, options, cutoff) {
60
+ const candidates = await this.db.list(this.tables.builds, { where: and(eq(col(this.tables.builds, "projectId"), project.id), inArray(col(this.tables.builds, "status"), [...TERMINAL_BUILD_STATUSES]), lt(col(this.tables.builds, "updatedAt"), cutoff)) });
61
+ const keep = options.keepLatestPerBranch ? await this.latestPerBranch(project.id) : /* @__PURE__ */ new Set();
62
+ const persistent = await new LabelModel(this.db, this.tables).persistentBuildIds(project.id);
63
+ const terminal = candidates.map((build) => build.id).filter((buildId) => !keep.has(buildId));
64
+ const pending = (await pendingCandidates(this.db, project.id, cutoff)).map((candidate) => candidate.buildId);
65
+ return {
66
+ buildIds: [...terminal, ...pending].filter((buildId) => !persistent.has(buildId)),
67
+ pending: pending.length
68
+ };
69
+ }
70
+ /** Reclaim screenshots no snapshot or baseline references any more; never blocks the purge. */
71
+ async sweepScreenshots(project) {
72
+ try {
73
+ await sweepScreenshotContent({
74
+ db: this.db,
75
+ storage: this.storage,
76
+ tables: {
77
+ snapshots: this.db.tables.snapshots,
78
+ baselines: this.tables.baselines,
79
+ builds: this.tables.builds
80
+ },
81
+ logger: this.logger
82
+ }, project.id);
83
+ } catch (error) {
84
+ this.logger?.warn({
85
+ err: error,
86
+ projectId: project.id
87
+ }, "screenshot sweep failed");
88
+ }
89
+ }
90
+ /**
91
+ * Drop finished queue jobs older than the build cutoff (they have no foreign key to a build, and
92
+ * a finished job is only history); never blocks the purge. Queued and running jobs are kept.
93
+ */
94
+ async purgeFinishedJobs(cutoff) {
95
+ try {
96
+ return await new CaptureJobModel(this.db).purgeFinished(cutoff);
97
+ } catch (error) {
98
+ this.logger?.warn({ err: error }, "capture job purge failed");
99
+ return 0;
100
+ }
101
+ }
48
102
  async removeBuild(buildId) {
103
+ await new InsightModel(this.db).removeForBuild(buildId);
104
+ await new CaptureJobModel(this.db).removeForBuild(buildId);
105
+ await new BuildComponentModel(this.db).removeForBuild(buildId);
106
+ await new PackageUsageModel(this.db).removeForBuild(buildId);
49
107
  await new BuildModel(this.db, this.tables).remove(buildId);
50
108
  return true;
51
109
  }
52
110
  async latestPerBranch(projectId) {
53
111
  const rows = await this.db.list(this.tables.builds, {
54
- where: eq(getTableColumns(this.tables.builds)["projectId"], projectId),
55
- orderBy: desc(getTableColumns(this.tables.builds)["updatedAt"])
112
+ where: eq(col(this.tables.builds, "projectId"), projectId),
113
+ orderBy: desc(col(this.tables.builds, "updatedAt"))
56
114
  });
57
115
  const latest = /* @__PURE__ */ new Map();
58
116
  for (const row of rows) {
@@ -97,9 +155,11 @@ var Retention = class {
97
155
  const all = await this.db.list(contentRefs);
98
156
  for (const row of all) if (row.refCount === 0 && row.lastSeenAt < cutoff) {
99
157
  await this.storage.delete(`content/${row.hash}`).catch(() => {});
158
+ if (/^[a-f0-9]{64}$/u.test(row.hash)) await this.storage.delete(publishedGzipPath(row.hash)).catch(() => {});
100
159
  await this.db.remove(contentRefs, row.hash);
101
160
  }
102
161
  } catch {}
162
+ await sweepPublishedGzip(this.storage).catch(() => {});
103
163
  return files.length;
104
164
  }
105
165
  async purgeStaleBranches(project, ttlDays) {
@@ -108,12 +168,13 @@ var Retention = class {
108
168
  removedBaselines: 0
109
169
  };
110
170
  const cutoff = (/* @__PURE__ */ new Date(Date.now() - ttlDays * 864e5)).toISOString();
111
- const stale = collectStaleBranches(await this.db.list(this.tables.builds, { where: eq(getTableColumns(this.tables.builds)["projectId"], project.id) }), project.gitDefaultBranch, cutoff);
171
+ const stale = collectStaleBranches(await this.db.list(this.tables.builds, { where: eq(col(this.tables.builds, "projectId"), project.id) }), project.gitDefaultBranch, cutoff);
112
172
  if (stale.size === 0) return {
113
173
  removedBranches: 0,
114
174
  removedBaselines: 0
115
175
  };
116
176
  const removedBaselines = await new BaselineModel(this.db, this.tables, this.storage).removeStaleBranches(project.id, stale);
177
+ await this.sweepScreenshots(project);
117
178
  this.logger?.info({
118
179
  projectId: project.id,
119
180
  removedBranches: stale.size,
@@ -139,7 +200,6 @@ function branchLatestAt(rows) {
139
200
  }
140
201
  return latestAt;
141
202
  }
142
- //#endregion
143
- export { Retention };
203
+ export { Retention, STALE_RUN_MS, SWEEP_GRACE_MS, purgeAiData, sweepScreenshotContent };
144
204
 
145
205
  //# sourceMappingURL=retention.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"retention.mjs","names":[],"sources":["../src/retention/purge.ts"],"sourcesContent":["import { and, desc, eq, getTableColumns, inArray, lt } from \"drizzle-orm\";\nimport type { SQLWrapper, Table } from \"drizzle-orm\";\nimport type { Logger } from \"pino\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel, type BuildTables } from \"../models/build.ts\";\nimport { LabelModel, type LabelTables } from \"../models/label.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { TERMINAL_BUILD_STATUSES } from \"../types.ts\";\n\n/** Table handles for retention queries. */\nexport interface RetentionTables {\n builds: Table;\n buildLabels: Table;\n baselines: Table;\n}\n\n/** Removes expired transient builds while keeping baselines and persistent builds. */\nexport class Retention {\n constructor(\n private readonly db: DatabaseAdapter,\n private readonly storage: StorageAdapter,\n private readonly tables: RetentionTables,\n private readonly logger?: Logger,\n ) {}\n\n async purge(project: Project, options: PurgeOptions): Promise<PurgeResult> {\n const cutoff = new Date(Date.now() - options.ttlDays * 86_400_000).toISOString();\n const candidates = await this.db.list(this.tables.builds, {\n where: and(\n eq(getTableColumns(this.tables.builds)[\"projectId\"] as unknown as SQLWrapper, project.id),\n inArray(getTableColumns(this.tables.builds)[\"status\"] as unknown as SQLWrapper, [\n ...TERMINAL_BUILD_STATUSES,\n ]),\n lt(getTableColumns(this.tables.builds)[\"updatedAt\"] as unknown as SQLWrapper, cutoff),\n ),\n });\n\n const keep = options.keepLatestPerBranch\n ? await this.latestPerBranch(project.id)\n : new Set<string>();\n const labelModel = new LabelModel(this.db, this.tables as unknown as LabelTables);\n const target = await Promise.all(\n candidates.map(async (build) => {\n const buildId = (build as { id: string }).id;\n if (keep.has(buildId) || (await labelModel.hasPersistent(project.id, buildId))) {\n return null;\n }\n return buildId;\n }),\n );\n\n const buildIds = target.filter((id): id is string => id !== null);\n const results = await Promise.all(\n buildIds.map(async (buildId) => ({\n files: await this.deleteBuildFiles(project.id, buildId),\n removed: await this.removeBuild(buildId),\n })),\n );\n const removedBuilds = results.filter((r: { removed: boolean }) => r.removed).length;\n const removedFiles = results.reduce((sum: number, r: { files: number }) => sum + r.files, 0);\n this.logger?.info(\n { projectId: project.id, removedBuilds, removedFiles },\n \"build retention purge complete\",\n );\n return { removedBuilds, removedFiles };\n }\n\n private async removeBuild(buildId: string): Promise<boolean> {\n await new BuildModel(this.db, this.tables as unknown as BuildTables).remove(buildId);\n return true;\n }\n\n private async latestPerBranch(projectId: string): Promise<Set<string>> {\n const rows = (await this.db.list(this.tables.builds, {\n where: eq(\n getTableColumns(this.tables.builds)[\"projectId\"] as unknown as SQLWrapper,\n projectId,\n ),\n orderBy: desc(getTableColumns(this.tables.builds)[\"updatedAt\"] as unknown as SQLWrapper),\n })) as unknown as { gitBranch: string; id: string }[];\n const latest = new Map<string, string>();\n for (const row of rows) {\n const branch = row.gitBranch;\n const current = latest.get(branch);\n if (!current) {\n latest.set(branch, row.id);\n }\n }\n return new Set(latest.values());\n }\n\n private async deleteBuildFiles(projectId: string, buildId: string): Promise<number> {\n const prefix = `${projectId}/builds/${buildId}/`;\n const manifestPath = `${prefix}storybook/manifest.json`;\n let manifestFiles: string[] = [];\n try {\n if (await this.storage.exists(manifestPath)) {\n const raw = await this.storage.read(manifestPath);\n const manifest = JSON.parse(raw.toString(\"utf8\")) as Record<string, string>;\n manifestFiles = Object.values(manifest);\n }\n } catch {\n // ignore — manifest may not exist for old builds\n }\n const files = await this.storage.list(prefix);\n await Promise.all(\n files.map(async (file) => {\n await this.storage.delete(file);\n }),\n );\n // Decrement content refs for this build's manifest\n for (const hash of manifestFiles) {\n try {\n const contentRefs = this.db.tables.contentRefs;\n const existing = (await this.db.get(contentRefs, hash)) as unknown as {\n refCount: number;\n } | null;\n // oxlint-disable-next-line unicorn/prefer-ternary -- refCount check is clearer as if/else\n if (existing) {\n const refCount = existing.refCount;\n // oxlint-disable-next-line unicorn/prefer-ternary -- grace logic is clearer as if/else\n if (refCount <= 1) {\n // Keep for 7-day grace: set refCount 0 and update lastSeenAt, don't delete yet\n await this.db.update(contentRefs, hash, {\n refCount: 0,\n lastSeenAt: new Date().toISOString(),\n });\n } else {\n await this.db.update(contentRefs, hash, {\n refCount: refCount - 1,\n lastSeenAt: new Date().toISOString(),\n });\n }\n }\n } catch {\n // ignore — content_refs may not exist\n }\n }\n // GC stale content_refs with 7-day grace\n try {\n const contentRefs = this.db.tables.contentRefs;\n const cutoff = new Date(Date.now() - 7 * 86_400_000).toISOString();\n const all = (await this.db.list(contentRefs)) as unknown as {\n hash: string;\n refCount: number;\n lastSeenAt: string;\n }[];\n for (const row of all) {\n if (row.refCount === 0 && row.lastSeenAt < cutoff) {\n await this.storage.delete(`content/${row.hash}`).catch(() => {\n // ignore storage delete failure\n });\n await this.db.remove(contentRefs, row.hash);\n }\n }\n } catch {\n // ignore — content_refs may not exist\n }\n return files.length;\n }\n\n async purgeStaleBranches(project: Project, ttlDays: number): Promise<BranchGcResult> {\n if (ttlDays <= 0) {\n return { removedBranches: 0, removedBaselines: 0 };\n }\n const cutoff = new Date(Date.now() - ttlDays * 86_400_000).toISOString();\n const rows = (await this.db.list(this.tables.builds, {\n where: eq(\n getTableColumns(this.tables.builds)[\"projectId\"] as unknown as SQLWrapper,\n project.id,\n ),\n })) as unknown as { gitBranch: string; updatedAt: string }[];\n const stale = collectStaleBranches(rows, project.gitDefaultBranch, cutoff);\n if (stale.size === 0) {\n return { removedBranches: 0, removedBaselines: 0 };\n }\n const baselines = new BaselineModel(this.db, this.tables, this.storage);\n const removedBaselines = await baselines.removeStaleBranches(project.id, stale);\n this.logger?.info(\n { projectId: project.id, removedBranches: stale.size, removedBaselines },\n \"branch GC complete\",\n );\n return { removedBranches: stale.size, removedBaselines };\n }\n}\n\n/** Options controlling which builds a retention purge removes. */\nexport interface PurgeOptions {\n ttlDays: number;\n keepLatestPerBranch: boolean;\n}\n\n/** Counts of builds and files removed by a retention purge. */\nexport interface PurgeResult {\n removedBuilds: number;\n removedFiles: number;\n}\n\n/** Counts of stale branches and baselines removed. */\nexport interface BranchGcResult {\n removedBranches: number;\n removedBaselines: number;\n}\n\nfunction collectStaleBranches(\n rows: { gitBranch: string; updatedAt: string }[],\n defaultBranch: string,\n cutoff: string,\n): Set<string> {\n const latestAt = branchLatestAt(rows);\n const stale = new Set<string>();\n for (const [branch, updatedAt] of latestAt) {\n if (branch !== defaultBranch && updatedAt < cutoff) {\n stale.add(branch);\n }\n }\n return stale;\n}\n\nfunction branchLatestAt(rows: { gitBranch: string; updatedAt: string }[]): Map<string, string> {\n const latestAt = new Map<string, string>();\n for (const row of rows) {\n const current = latestAt.get(row.gitBranch);\n if (!current || row.updatedAt > current) {\n latestAt.set(row.gitBranch, row.updatedAt);\n }\n }\n return latestAt;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"retention.mjs","names":[],"sources":["../src/retention/purge.ts"],"sourcesContent":["import { and, desc, eq, inArray, lt } from \"drizzle-orm\";\nimport type { Table } from \"drizzle-orm\";\nimport type { Logger } from \"pino\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildComponentModel } from \"../models/build-component.ts\";\nimport { BuildModel, type BuildTables } from \"../models/build.ts\";\nimport { CaptureJobModel } from \"../models/capture-job.ts\";\nimport { InsightModel } from \"../models/insight.ts\";\nimport { LabelModel, type LabelTables } from \"../models/label.ts\";\nimport { PackageUsageModel } from \"../models/usage.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { TERMINAL_BUILD_STATUSES } from \"../types.ts\";\nimport { pendingCandidates } from \"./pending.ts\";\nimport { sweepPublishedGzip } from \"./published-gzip-sweep.ts\";\nimport { sweepScreenshotContent } from \"./screenshot-sweep.ts\";\n\nexport {\n sweepScreenshotContent,\n SWEEP_GRACE_MS,\n type SweepDeps,\n type SweepResult,\n type SweepTables,\n} from \"./screenshot-sweep.ts\";\nimport { col } from \"../models/columns.ts\";\nimport { publishedGzipPath } from \"../utils/paths.ts\";\nexport { purgeAiData, STALE_RUN_MS, type AiPurgeResult } from \"./ai.ts\";\n\n/** Table handles for retention queries. */\nexport interface RetentionTables {\n builds: Table;\n buildLabels: Table;\n baselines: Table;\n}\n\n/** Removes expired transient builds while keeping baselines and persistent builds. */\nexport class Retention {\n private readonly db: DatabaseAdapter;\n private readonly storage: StorageAdapter;\n private readonly tables: RetentionTables;\n private readonly logger?: Logger;\n\n constructor(\n db: DatabaseAdapter,\n storage: StorageAdapter,\n tables: RetentionTables,\n logger?: Logger,\n ) {\n this.db = db;\n this.storage = storage;\n this.tables = tables;\n this.logger = logger;\n }\n\n async purge(project: Project, options: PurgeOptions): Promise<PurgeResult> {\n const cutoff = new Date(Date.now() - options.ttlDays * 86_400_000).toISOString();\n const { buildIds, pending } = await this.selectBuilds(project, options, cutoff);\n const results = await Promise.all(\n buildIds.map(async (buildId) => ({\n files: await this.deleteBuildFiles(project.id, buildId),\n removed: await this.removeBuild(buildId),\n })),\n );\n const removedBuilds = results.filter((r: { removed: boolean }) => r.removed).length;\n const removedFiles = results.reduce((sum: number, r: { files: number }) => sum + r.files, 0);\n await this.sweepScreenshots(project);\n const removedJobs = await this.purgeFinishedJobs(cutoff);\n this.logger?.info(\n { projectId: project.id, removedBuilds, removedFiles, removedJobs, removedPending: pending },\n \"build retention purge complete\",\n );\n return { removedBuilds, removedFiles };\n }\n\n /**\n * The builds a purge removes: terminal builds past the window (minus the latest per branch when\n * asked), and pending builds idle past it (ADR 0013, decision 9). Persistent builds stay.\n */\n private async selectBuilds(\n project: Project,\n options: PurgeOptions,\n cutoff: string,\n ): Promise<{ buildIds: string[]; pending: number }> {\n const candidates = await this.db.list(this.tables.builds, {\n where: and(\n eq(col(this.tables.builds, \"projectId\"), project.id),\n inArray(col(this.tables.builds, \"status\"), [...TERMINAL_BUILD_STATUSES]),\n lt(col(this.tables.builds, \"updatedAt\"), cutoff),\n ),\n });\n const keep = options.keepLatestPerBranch\n ? await this.latestPerBranch(project.id)\n : new Set<string>();\n const persistent = await new LabelModel(\n this.db,\n this.tables as unknown as LabelTables,\n ).persistentBuildIds(project.id);\n const terminal = candidates\n .map((build) => (build as { id: string }).id)\n .filter((buildId) => !keep.has(buildId));\n // Pending builds leave on their own clock; \"the latest on a branch\" does not shield them.\n const pending = (await pendingCandidates(this.db, project.id, cutoff)).map(\n (candidate) => candidate.buildId,\n );\n return {\n buildIds: [...terminal, ...pending].filter((buildId) => !persistent.has(buildId)),\n pending: pending.length,\n };\n }\n\n /** Reclaim screenshots no snapshot or baseline references any more; never blocks the purge. */\n private async sweepScreenshots(project: Project): Promise<void> {\n try {\n await sweepScreenshotContent(\n {\n db: this.db,\n storage: this.storage,\n tables: {\n snapshots: this.db.tables.snapshots,\n baselines: this.tables.baselines,\n builds: this.tables.builds,\n },\n logger: this.logger,\n },\n project.id,\n );\n } catch (error) {\n this.logger?.warn({ err: error, projectId: project.id }, \"screenshot sweep failed\");\n }\n }\n\n /**\n * Drop finished queue jobs older than the build cutoff (they have no foreign key to a build, and\n * a finished job is only history); never blocks the purge. Queued and running jobs are kept.\n */\n private async purgeFinishedJobs(cutoff: string): Promise<number> {\n try {\n return await new CaptureJobModel(this.db).purgeFinished(cutoff);\n } catch (error) {\n this.logger?.warn({ err: error }, \"capture job purge failed\");\n return 0;\n }\n }\n\n private async removeBuild(buildId: string): Promise<boolean> {\n await new InsightModel(this.db).removeForBuild(buildId);\n await new CaptureJobModel(this.db).removeForBuild(buildId);\n await new BuildComponentModel(this.db).removeForBuild(buildId);\n await new PackageUsageModel(this.db).removeForBuild(buildId);\n await new BuildModel(this.db, this.tables as unknown as BuildTables).remove(buildId);\n return true;\n }\n\n private async latestPerBranch(projectId: string): Promise<Set<string>> {\n const rows = (await this.db.list(this.tables.builds, {\n where: eq(col(this.tables.builds, \"projectId\"), projectId),\n orderBy: desc(col(this.tables.builds, \"updatedAt\")),\n })) as unknown as { gitBranch: string; id: string }[];\n const latest = new Map<string, string>();\n for (const row of rows) {\n const branch = row.gitBranch;\n const current = latest.get(branch);\n if (!current) {\n latest.set(branch, row.id);\n }\n }\n return new Set(latest.values());\n }\n\n private async deleteBuildFiles(projectId: string, buildId: string): Promise<number> {\n const prefix = `${projectId}/builds/${buildId}/`;\n const manifestPath = `${prefix}storybook/manifest.json`;\n let manifestFiles: string[] = [];\n try {\n if (await this.storage.exists(manifestPath)) {\n const raw = await this.storage.read(manifestPath);\n const manifest = JSON.parse(raw.toString(\"utf8\")) as Record<string, string>;\n manifestFiles = Object.values(manifest);\n }\n } catch {\n // ignore — manifest may not exist for old builds\n }\n const files = await this.storage.list(prefix);\n await Promise.all(\n files.map(async (file) => {\n await this.storage.delete(file);\n }),\n );\n // Decrement content refs for this build's manifest\n for (const hash of manifestFiles) {\n try {\n const contentRefs = this.db.tables.contentRefs;\n const existing = (await this.db.get(contentRefs, hash)) as unknown as {\n refCount: number;\n } | null;\n // oxlint-disable-next-line unicorn/prefer-ternary -- refCount check is clearer as if/else\n if (existing) {\n const refCount = existing.refCount;\n // oxlint-disable-next-line unicorn/prefer-ternary -- grace logic is clearer as if/else\n if (refCount <= 1) {\n // Keep for 7-day grace: set refCount 0 and update lastSeenAt, don't delete yet\n await this.db.update(contentRefs, hash, {\n refCount: 0,\n lastSeenAt: new Date().toISOString(),\n });\n } else {\n await this.db.update(contentRefs, hash, {\n refCount: refCount - 1,\n lastSeenAt: new Date().toISOString(),\n });\n }\n }\n } catch {\n // ignore — content_refs may not exist\n }\n }\n // GC stale content_refs with 7-day grace\n try {\n const contentRefs = this.db.tables.contentRefs;\n const cutoff = new Date(Date.now() - 7 * 86_400_000).toISOString();\n const all = (await this.db.list(contentRefs)) as unknown as {\n hash: string;\n refCount: number;\n lastSeenAt: string;\n }[];\n for (const row of all) {\n if (row.refCount === 0 && row.lastSeenAt < cutoff) {\n await this.storage.delete(`content/${row.hash}`).catch(() => {\n // ignore storage delete failure\n });\n // Variants are disposable; never let their deletion affect authoritative data.\n if (/^[a-f0-9]{64}$/u.test(row.hash)) {\n await this.storage.delete(publishedGzipPath(row.hash)).catch(() => {});\n }\n await this.db.remove(contentRefs, row.hash);\n }\n }\n } catch {\n // ignore — content_refs may not exist\n }\n await sweepPublishedGzip(this.storage).catch(() => {});\n return files.length;\n }\n\n async purgeStaleBranches(project: Project, ttlDays: number): Promise<BranchGcResult> {\n if (ttlDays <= 0) {\n return { removedBranches: 0, removedBaselines: 0 };\n }\n const cutoff = new Date(Date.now() - ttlDays * 86_400_000).toISOString();\n const rows = (await this.db.list(this.tables.builds, {\n where: eq(col(this.tables.builds, \"projectId\"), project.id),\n })) as unknown as { gitBranch: string; updatedAt: string }[];\n const stale = collectStaleBranches(rows, project.gitDefaultBranch, cutoff);\n if (stale.size === 0) {\n return { removedBranches: 0, removedBaselines: 0 };\n }\n const baselines = new BaselineModel(this.db, this.tables, this.storage);\n const removedBaselines = await baselines.removeStaleBranches(project.id, stale);\n await this.sweepScreenshots(project);\n this.logger?.info(\n { projectId: project.id, removedBranches: stale.size, removedBaselines },\n \"branch GC complete\",\n );\n return { removedBranches: stale.size, removedBaselines };\n }\n}\n\n/** Options controlling which builds a retention purge removes. */\nexport interface PurgeOptions {\n ttlDays: number;\n keepLatestPerBranch: boolean;\n}\n\n/** Counts of builds and files removed by a retention purge. */\nexport interface PurgeResult {\n removedBuilds: number;\n removedFiles: number;\n}\n\n/** Counts of stale branches and baselines removed. */\nexport interface BranchGcResult {\n removedBranches: number;\n removedBaselines: number;\n}\n\nfunction collectStaleBranches(\n rows: { gitBranch: string; updatedAt: string }[],\n defaultBranch: string,\n cutoff: string,\n): Set<string> {\n const latestAt = branchLatestAt(rows);\n const stale = new Set<string>();\n for (const [branch, updatedAt] of latestAt) {\n if (branch !== defaultBranch && updatedAt < cutoff) {\n stale.add(branch);\n }\n }\n return stale;\n}\n\nfunction branchLatestAt(rows: { gitBranch: string; updatedAt: string }[]): Map<string, string> {\n const latestAt = new Map<string, string>();\n for (const row of rows) {\n const current = latestAt.get(row.gitBranch);\n if (!current || row.updatedAt > current) {\n latestAt.set(row.gitBranch, row.updatedAt);\n }\n }\n return latestAt;\n}\n"],"mappings":"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"}
@@ -1,6 +1,5 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/schema/baseline.d.ts
4
3
  /** A baseline row. */
5
4
  interface Baseline {
6
5
  id: string;
@@ -9,11 +8,18 @@ interface Baseline {
9
8
  viewportName: string;
10
9
  branch: string;
11
10
  snapshotId: string | null;
11
+ /** Build that actually rendered this image; null for unknown or imported origins. */
12
+ sourceBuildId?: string | null;
12
13
  screenshotPath: string;
13
14
  infraHash: string | null;
15
+ /** JSON `Fingerprint` behind `infraHash`; null on baselines recorded before fingerprints. */
16
+ fingerprint: string | null;
17
+ /** Resolved mode definition (JSON) — display/capture only; identity is `viewportName`. */
18
+ mode?: string | null;
19
+ /** Engine the image was captured with. */
20
+ browser?: string;
14
21
  createdAt: string;
15
22
  updatedAt: string;
16
23
  }
17
- //#endregion
18
24
  export { Baseline };
19
25
  //# sourceMappingURL=baseline.d.mts.map
@@ -0,0 +1,23 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ /**
4
+ * Row type of `build_components` (ADR 0006, decision 9): one row per component of a published
5
+ * build, written from the build's Storybook index when publishing ends. A component is the
6
+ * stories and docs entries that share a story file (`importPath`).
7
+ */
8
+ /** One component of one build, as persisted from the Storybook index. */
9
+ interface BuildComponent {
10
+ id: string;
11
+ projectId: string;
12
+ buildId: string;
13
+ /** Story file the component's entries come from, without a leading `./`; empty when the index has none. */
14
+ importPath: string;
15
+ title: string;
16
+ /** Story entries (docs entries are not counted). */
17
+ storyCount: number;
18
+ /** SHA-256 of the component's entries (id, name, type), so two builds compare with one string. */
19
+ hash: string;
20
+ createdAt: string;
21
+ }
22
+ export { BuildComponent };
23
+ //# sourceMappingURL=build-component.d.mts.map
@@ -0,0 +1,15 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { ReviewVerdict } from "../types.mjs";
4
+ /** One verdict on one build. */
5
+ interface BuildReview {
6
+ id: string;
7
+ buildId: string;
8
+ /** The reviewer; null when auth is off or an admin token wrote it. */
9
+ userId: string | null;
10
+ verdict: ReviewVerdict;
11
+ body: string | null;
12
+ createdAt: string;
13
+ }
14
+ export { BuildReview };
15
+ //# sourceMappingURL=build-review.d.mts.map
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { BuildStatus } from "../types.mjs";
4
- //#region src/schema/build.d.ts
5
4
  /** A build row. */
6
5
  interface Build {
7
6
  id: string;
@@ -18,6 +17,8 @@ interface Build {
18
17
  changedCount: number;
19
18
  approvedCount: number;
20
19
  rejectedCount: number;
20
+ /** Snapshots inherited from the baseline without rendering (affected capture); a subset of `snapshotCount`. */
21
+ inheritedCount: number;
21
22
  /** Affected capture enabled for this build (default true). */
22
23
  affectedOnly: boolean;
23
24
  /** Ancestor commit the affected set was computed against, if any. */
@@ -26,9 +27,13 @@ interface Build {
26
27
  changedFiles: string | null;
27
28
  /** JSON array of affected story import paths; null means full capture. */
28
29
  affectedImportPaths: string | null;
30
+ /**
31
+ * JSON array of the results of the build's Storybook `.test()` entries (`TestResult`); null when
32
+ * the project does not run tests or the Storybook has none.
33
+ */
34
+ testResults?: string | null;
29
35
  createdAt: string;
30
36
  updatedAt: string;
31
37
  }
32
- //#endregion
33
38
  export { Build };
34
39
  //# sourceMappingURL=build.d.mts.map
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { JobStatus } from "../adapter/capture-queue.mjs";
4
- //#region src/schema/capture-attempt.d.ts
5
4
  /** A capture attempt row: one queued run of a build's capture pipeline. */
6
5
  interface CaptureAttempt {
7
6
  id: string;
@@ -33,6 +32,5 @@ interface CaptureLog {
33
32
  fields: string | null;
34
33
  createdAt: string;
35
34
  }
36
- //#endregion
37
35
  export { CaptureAttempt, CaptureLog, CaptureLogLevel };
38
36
  //# sourceMappingURL=capture-attempt.d.mts.map