@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,9 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ // src/models/baseline-conflict.ts
5
+ /** A baseline write lost a concurrency check and should be retried. */
6
+ var BaselineWriteConflict = class extends Error {};
7
+ export { BaselineWriteConflict };
8
+
9
+ //# sourceMappingURL=baseline-conflict.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline-conflict.mjs","names":[],"sources":["../../src/models/baseline-conflict.ts"],"sourcesContent":["/** A baseline write lost a concurrency check and should be retried. */\nexport class BaselineWriteConflict extends Error {}\n"],"mappings":";;;;;AACA,IAAa,wBAAb,cAA2C,MAAM,CAAC"}
@@ -0,0 +1,17 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import "../adapter/storage.mjs";
4
+ import { BaselineSkipped } from "./baseline-archive.mjs";
5
+ /** Options for {@link exportBaselineArchive}. */
6
+ interface BaselineExportOptions {
7
+ /** Export only this branch; omit (or null) for every branch. */
8
+ branch?: string | null;
9
+ /** Source project slug recorded in the manifest. */
10
+ project?: string | null;
11
+ /** Called for each baseline left out of the archive because its image is missing or too large. */
12
+ onSkipped?: (baseline: BaselineSkipped) => void;
13
+ /** Clock override for `exportedAt` (tests). */
14
+ now?: () => Date;
15
+ }
16
+ export { BaselineExportOptions };
17
+ //# sourceMappingURL=baseline-export.d.mts.map
@@ -0,0 +1,135 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import "../adapters/capture-target.mjs";
5
+ import { BASELINE_ARCHIVE_FORMAT, BASELINE_ARCHIVE_LIMITS, BASELINE_MANIFEST_NAME, BaselineArchiveError, archiveFileName } from "./baseline-archive.mjs";
6
+ import { zipStream } from "../utils/zip-write.mjs";
7
+ import { createHash } from "node:crypto";
8
+ // src/models/baseline-export.ts
9
+ /** Stream a project's baselines as a portable zip archive (format v1). */
10
+ /**
11
+ * Prepare a baseline archive for `rows`. Validation (the entry limit) runs
12
+ * eagerly so callers can fail the request before streaming; images are then
13
+ * read one at a time while the returned iterable is consumed, and the
14
+ * manifest is written last so it can carry each image's SHA-256. Baselines
15
+ * whose image is missing (or over the per-image limit) are listed under
16
+ * `skipped` and reported through `onSkipped`. Storage errors on an image that
17
+ * exists abort the stream, as does outgrowing the archive size limit, so a
18
+ * truncated stream is never mistaken for a complete backup.
19
+ *
20
+ * @throws {BaselineArchiveError} When there are more baselines than an archive may hold.
21
+ */
22
+ function exportBaselineArchive(storage, rows, options = {}) {
23
+ const branch = options.branch ?? null;
24
+ const selected = rows.filter((row) => branch === null || row.branch === branch).toSorted((a, b) => compareKeys(sortKey(a), sortKey(b)));
25
+ if (selected.length > BASELINE_ARCHIVE_LIMITS.maxBaselines) throw new BaselineArchiveError(`Too many baselines to export (${selected.length} > ${BASELINE_ARCHIVE_LIMITS.maxBaselines}); filter by branch`);
26
+ const manifest = {
27
+ format: BASELINE_ARCHIVE_FORMAT,
28
+ version: 1,
29
+ exportedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
30
+ project: options.project ?? null,
31
+ branch,
32
+ baselines: [],
33
+ skipped: []
34
+ };
35
+ return zipStream(archiveSources(storage, selected, manifest, options.onSkipped));
36
+ }
37
+ function sortKey(row) {
38
+ return [
39
+ row.branch,
40
+ row.storyId,
41
+ row.viewportName,
42
+ row.browser ?? "chromium"
43
+ ];
44
+ }
45
+ function compareKeys(a, b) {
46
+ const index = a.findIndex((part, at) => part !== b[at]);
47
+ if (index === -1) return 0;
48
+ return (a[index] ?? "") < (b[index] ?? "") ? -1 : 1;
49
+ }
50
+ async function* archiveSources(storage, rows, manifest, onSkipped) {
51
+ const state = {
52
+ names: /* @__PURE__ */ new Set(),
53
+ budget: new ArchiveBudget()
54
+ };
55
+ for (const row of rows) {
56
+ const source = await rowSource(storage, row, manifest, state, onSkipped);
57
+ if (source) yield source;
58
+ }
59
+ const json = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
60
+ yield {
61
+ name: BASELINE_MANIFEST_NAME,
62
+ data: json
63
+ };
64
+ }
65
+ /** The zip entry for one baseline, or null (after recording it as skipped) when its image is unavailable. */
66
+ async function rowSource(storage, row, manifest, state, onSkipped) {
67
+ const data = await readImage(storage, row);
68
+ if (!data) {
69
+ const identity = identityOf(row);
70
+ manifest.skipped?.push(identity);
71
+ onSkipped?.(identity);
72
+ return null;
73
+ }
74
+ const entry = manifestEntry(row, data, state.names);
75
+ state.budget.add(entry.file, data.length);
76
+ manifest.baselines.push(entry);
77
+ return {
78
+ name: entry.file,
79
+ data
80
+ };
81
+ }
82
+ /** Tracks the streamed size so an export never outgrows what an import accepts. */
83
+ var ArchiveBudget = class ArchiveBudget {
84
+ /** Zip local + central header bytes per entry, excluding the (twice-stored) name. */
85
+ static ENTRY_OVERHEAD = 76;
86
+ used = BASELINE_ARCHIVE_LIMITS.maxManifestBytes;
87
+ add(name, bytes) {
88
+ this.used += ArchiveBudget.ENTRY_OVERHEAD + Buffer.byteLength(name) * 2 + bytes;
89
+ if (this.used > BASELINE_ARCHIVE_LIMITS.maxArchiveBytes) throw new BaselineArchiveError(`Export exceeds ${BASELINE_ARCHIVE_LIMITS.maxArchiveBytes} bytes (the import limit); export one branch at a time`);
90
+ }
91
+ };
92
+ /** Read a baseline image; null when it is missing or too large, throws on other storage errors. */
93
+ async function readImage(storage, row) {
94
+ try {
95
+ const data = await storage.read(row.screenshotPath);
96
+ return data.length <= BASELINE_ARCHIVE_LIMITS.maxImageBytes ? data : null;
97
+ } catch (error) {
98
+ if (await storage.exists(row.screenshotPath).catch(() => false)) throw error;
99
+ return null;
100
+ }
101
+ }
102
+ function identityOf(row) {
103
+ return {
104
+ storyId: row.storyId,
105
+ viewport: row.viewportName,
106
+ browser: row.browser ?? "chromium",
107
+ branch: row.branch
108
+ };
109
+ }
110
+ function manifestEntry(row, data, names) {
111
+ const identity = identityOf(row);
112
+ return {
113
+ ...identity,
114
+ mode: row.mode ?? null,
115
+ infraHash: row.infraHash,
116
+ fingerprint: row.fingerprint ?? null,
117
+ createdAt: row.createdAt,
118
+ updatedAt: row.updatedAt,
119
+ file: uniqueName(archiveFileName(identity.branch, identity.storyId, identity.viewport, identity.browser), names),
120
+ sha256: createHash("sha256").update(data).digest("hex")
121
+ };
122
+ }
123
+ /**
124
+ * `modeSegment` can map two identities to one name (`a~firefox` on chromium vs
125
+ * `a` on firefox). Names are only lookup keys, so disambiguate with a suffix.
126
+ */
127
+ function uniqueName(name, names) {
128
+ let candidate = name;
129
+ for (let n = 2; names.has(candidate); n += 1) candidate = name.replace(/\.png$/u, `.${n}.png`);
130
+ names.add(candidate);
131
+ return candidate;
132
+ }
133
+ export { exportBaselineArchive };
134
+
135
+ //# sourceMappingURL=baseline-export.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline-export.mjs","names":[],"sources":["../../src/models/baseline-export.ts"],"sourcesContent":["/** Stream a project's baselines as a portable zip archive (format v1). */\nimport { createHash } from \"node:crypto\";\nimport { DEFAULT_BROWSER } from \"../adapters/capture-runner.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport type { Baseline } from \"../schema/baseline.ts\";\nimport { zipStream, type ZipSource } from \"../utils/zip-write.ts\";\nimport {\n archiveFileName,\n BASELINE_ARCHIVE_FORMAT,\n BASELINE_ARCHIVE_LIMITS,\n BASELINE_ARCHIVE_VERSION,\n BASELINE_MANIFEST_NAME,\n BaselineArchiveError,\n type BaselineManifest,\n type BaselineManifestEntry,\n type BaselineSkipped,\n} from \"./baseline-archive.ts\";\n\n/** Options for {@link exportBaselineArchive}. */\nexport interface BaselineExportOptions {\n /** Export only this branch; omit (or null) for every branch. */\n branch?: string | null;\n /** Source project slug recorded in the manifest. */\n project?: string | null;\n /** Called for each baseline left out of the archive because its image is missing or too large. */\n onSkipped?: (baseline: BaselineSkipped) => void;\n /** Clock override for `exportedAt` (tests). */\n now?: () => Date;\n}\n\n/**\n * Prepare a baseline archive for `rows`. Validation (the entry limit) runs\n * eagerly so callers can fail the request before streaming; images are then\n * read one at a time while the returned iterable is consumed, and the\n * manifest is written last so it can carry each image's SHA-256. Baselines\n * whose image is missing (or over the per-image limit) are listed under\n * `skipped` and reported through `onSkipped`. Storage errors on an image that\n * exists abort the stream, as does outgrowing the archive size limit, so a\n * truncated stream is never mistaken for a complete backup.\n *\n * @throws {BaselineArchiveError} When there are more baselines than an archive may hold.\n */\nexport function exportBaselineArchive(\n storage: StorageAdapter,\n rows: Baseline[],\n options: BaselineExportOptions = {},\n): AsyncIterable<Buffer> {\n const branch = options.branch ?? null;\n const selected = rows\n .filter((row) => branch === null || row.branch === branch)\n .toSorted((a, b) => compareKeys(sortKey(a), sortKey(b)));\n if (selected.length > BASELINE_ARCHIVE_LIMITS.maxBaselines) {\n throw new BaselineArchiveError(\n `Too many baselines to export (${selected.length} > ${BASELINE_ARCHIVE_LIMITS.maxBaselines}); filter by branch`,\n );\n }\n const manifest: BaselineManifest = {\n format: BASELINE_ARCHIVE_FORMAT,\n version: BASELINE_ARCHIVE_VERSION,\n exportedAt: (options.now?.() ?? new Date()).toISOString(),\n project: options.project ?? null,\n branch,\n baselines: [],\n skipped: [],\n };\n return zipStream(archiveSources(storage, selected, manifest, options.onSkipped));\n}\n\nfunction sortKey(row: Baseline): string[] {\n return [row.branch, row.storyId, row.viewportName, row.browser ?? DEFAULT_BROWSER];\n}\n\nfunction compareKeys(a: string[], b: string[]): number {\n const index = a.findIndex((part, at) => part !== b[at]);\n if (index === -1) {\n return 0;\n }\n // Code-unit order, not locale order, so archives are reproducible across hosts.\n return (a[index] ?? \"\") < (b[index] ?? \"\") ? -1 : 1;\n}\n\nasync function* archiveSources(\n storage: StorageAdapter,\n rows: Baseline[],\n manifest: BaselineManifest,\n onSkipped?: (baseline: BaselineSkipped) => void,\n): AsyncGenerator<ZipSource> {\n const state = { names: new Set<string>(), budget: new ArchiveBudget() };\n for (const row of rows) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- one image in memory at a time\n const source = await rowSource(storage, row, manifest, state, onSkipped);\n if (source) {\n yield source;\n }\n }\n const json = Buffer.from(`${JSON.stringify(manifest, null, 2)}\\n`);\n yield { name: BASELINE_MANIFEST_NAME, data: json };\n}\n\n/** The zip entry for one baseline, or null (after recording it as skipped) when its image is unavailable. */\nasync function rowSource(\n storage: StorageAdapter,\n row: Baseline,\n manifest: BaselineManifest,\n state: { names: Set<string>; budget: ArchiveBudget },\n onSkipped?: (baseline: BaselineSkipped) => void,\n): Promise<ZipSource | null> {\n const data = await readImage(storage, row);\n if (!data) {\n const identity = identityOf(row);\n manifest.skipped?.push(identity);\n onSkipped?.(identity);\n return null;\n }\n const entry = manifestEntry(row, data, state.names);\n state.budget.add(entry.file, data.length);\n manifest.baselines.push(entry);\n return { name: entry.file, data };\n}\n\n/** Tracks the streamed size so an export never outgrows what an import accepts. */\nclass ArchiveBudget {\n /** Zip local + central header bytes per entry, excluding the (twice-stored) name. */\n private static readonly ENTRY_OVERHEAD = 76;\n private used = BASELINE_ARCHIVE_LIMITS.maxManifestBytes;\n\n add(name: string, bytes: number): void {\n this.used += ArchiveBudget.ENTRY_OVERHEAD + Buffer.byteLength(name) * 2 + bytes;\n if (this.used > BASELINE_ARCHIVE_LIMITS.maxArchiveBytes) {\n throw new BaselineArchiveError(\n `Export exceeds ${BASELINE_ARCHIVE_LIMITS.maxArchiveBytes} bytes (the import limit); export one branch at a time`,\n );\n }\n }\n}\n\n/** Read a baseline image; null when it is missing or too large, throws on other storage errors. */\nasync function readImage(storage: StorageAdapter, row: Baseline): Promise<Buffer | null> {\n try {\n const data = await storage.read(row.screenshotPath);\n return data.length <= BASELINE_ARCHIVE_LIMITS.maxImageBytes ? data : null;\n } catch (error) {\n if (await storage.exists(row.screenshotPath).catch(() => false)) {\n throw error;\n }\n return null;\n }\n}\n\nfunction identityOf(row: Baseline): BaselineSkipped {\n return {\n storyId: row.storyId,\n viewport: row.viewportName,\n browser: row.browser ?? DEFAULT_BROWSER,\n branch: row.branch,\n };\n}\n\nfunction manifestEntry(row: Baseline, data: Buffer, names: Set<string>): BaselineManifestEntry {\n const identity = identityOf(row);\n return {\n ...identity,\n mode: row.mode ?? null,\n infraHash: row.infraHash,\n fingerprint: row.fingerprint ?? null,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n file: uniqueName(\n archiveFileName(identity.branch, identity.storyId, identity.viewport, identity.browser),\n names,\n ),\n sha256: createHash(\"sha256\").update(data).digest(\"hex\"),\n };\n}\n\n/**\n * `modeSegment` can map two identities to one name (`a~firefox` on chromium vs\n * `a` on firefox). Names are only lookup keys, so disambiguate with a suffix.\n */\nfunction uniqueName(name: string, names: Set<string>): string {\n let candidate = name;\n for (let n = 2; names.has(candidate); n += 1) {\n candidate = name.replace(/\\.png$/u, `.${n}.png`);\n }\n names.add(candidate);\n return candidate;\n}\n"],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { baselinePath, contentPath } from "../paths.mjs";
5
+ import { BASELINE_ARCHIVE_LIMITS, BASELINE_MANIFEST_NAME, BaselineArchiveError, baselineIdentity, baselineManifestSchema, isPng } from "./baseline-archive.mjs";
6
+ import { ZipFormatError, readZip } from "../utils/zip-read.mjs";
7
+ import { createHash } from "node:crypto";
8
+ import { setImmediate } from "node:timers/promises";
9
+ // src/models/baseline-import.ts
10
+ /**
11
+ * Validate and apply a baseline archive (format v1).
12
+ *
13
+ * All-or-nothing validation: the manifest, every identity (through the same
14
+ * segment checks as {@link baselinePath}), every image's size, PNG signature
15
+ * and SHA-256 are verified before the first write. Storage paths are always
16
+ * derived from the manifest identity — never from zip entry names.
17
+ */
18
+ /**
19
+ * Parse and fully validate `archive` for import into `projectId`. Inflating and
20
+ * hashing is CPU-bound, so the loop yields to the event loop between entries
21
+ * instead of blocking the server for the whole archive.
22
+ *
23
+ * @throws {BaselineArchiveError} On any malformed, unsafe, or inconsistent input.
24
+ */
25
+ async function planBaselineImport(archive, projectId) {
26
+ const entries = openArchive(archive);
27
+ const manifest = parseManifest(entries);
28
+ const seen = /* @__PURE__ */ new Set();
29
+ const plan = [];
30
+ for (const entry of manifest.baselines) {
31
+ claimEntry(seen, entry);
32
+ plan.push(planEntry(entries, projectId, entry));
33
+ await setImmediate();
34
+ }
35
+ return plan;
36
+ }
37
+ /** Reject repeated identities or files, so total work stays bounded by the archive's own size limit. */
38
+ function claimEntry(seen, entry) {
39
+ const keys = [baselineIdentity(entry), `file:${entry.file}`];
40
+ if (keys.some((key) => seen.has(key))) fail(`Duplicate baseline or file in manifest: ${entry.file}`);
41
+ for (const key of keys) seen.add(key);
42
+ }
43
+ /** Write a planned baseline's image to storage. */
44
+ async function writePlannedImage(storage, planned) {
45
+ if (!await storage.exists(planned.storagePath)) await storage.write(planned.storagePath, planned.data);
46
+ }
47
+ function fail(message) {
48
+ throw new BaselineArchiveError(message);
49
+ }
50
+ function openArchive(archive) {
51
+ try {
52
+ return readZip(archive, {
53
+ maxEntries: BASELINE_ARCHIVE_LIMITS.maxBaselines + 1,
54
+ maxEntryBytes: Math.max(BASELINE_ARCHIVE_LIMITS.maxImageBytes, BASELINE_ARCHIVE_LIMITS.maxManifestBytes),
55
+ maxTotalBytes: BASELINE_ARCHIVE_LIMITS.maxArchiveBytes
56
+ });
57
+ } catch (error) {
58
+ if (error instanceof ZipFormatError) fail(error.message);
59
+ throw error;
60
+ }
61
+ }
62
+ function parseManifest(entries) {
63
+ const file = entries.get(BASELINE_MANIFEST_NAME);
64
+ if (!file) fail(`Archive has no ${BASELINE_MANIFEST_NAME}`);
65
+ if (file.size > BASELINE_ARCHIVE_LIMITS.maxManifestBytes) fail(`${BASELINE_MANIFEST_NAME} exceeds ${BASELINE_ARCHIVE_LIMITS.maxManifestBytes} bytes`);
66
+ const parsed = baselineManifestSchema.safeParse(parseJson(file.read().toString("utf8")));
67
+ if (!parsed.success) {
68
+ const issue = parsed.error.issues[0];
69
+ fail(`Invalid ${BASELINE_MANIFEST_NAME}: ${issue?.path.join(".") ?? ""} ${issue?.message ?? ""}`);
70
+ }
71
+ return parsed.data;
72
+ }
73
+ function parseJson(text) {
74
+ try {
75
+ return JSON.parse(text);
76
+ } catch {
77
+ return fail(`${BASELINE_MANIFEST_NAME} is not valid JSON`);
78
+ }
79
+ }
80
+ function planEntry(entries, projectId, entry) {
81
+ const zipEntry = entries.get(entry.file);
82
+ if (!zipEntry || entry.file === "manifest.json") fail(`Archive is missing ${entry.file}`);
83
+ if (zipEntry.size > BASELINE_ARCHIVE_LIMITS.maxImageBytes) fail(`${entry.file} exceeds ${BASELINE_ARCHIVE_LIMITS.maxImageBytes} bytes`);
84
+ assertSafeIdentity(projectId, entry);
85
+ const data = verifiedImage(zipEntry, entry);
86
+ return {
87
+ entry,
88
+ storagePath: contentPath(projectId, createHash("sha256").update(data).digest("hex")),
89
+ zipEntry,
90
+ data
91
+ };
92
+ }
93
+ /** The identity only has to be safe here: the image itself is stored by content. */
94
+ function assertSafeIdentity(projectId, entry) {
95
+ try {
96
+ baselinePath(projectId, entry.branch, entry.storyId, entry.viewport, entry.browser);
97
+ } catch {
98
+ fail(`Unsafe baseline identity for ${entry.file}`);
99
+ }
100
+ }
101
+ function verifiedImage(zipEntry, entry) {
102
+ const data = readEntry(zipEntry);
103
+ if (!isPng(data)) fail(`${entry.file} is not a PNG`);
104
+ if (createHash("sha256").update(data).digest("hex") !== entry.sha256) fail(`${entry.file} does not match its manifest checksum`);
105
+ return data;
106
+ }
107
+ function readEntry(zipEntry) {
108
+ try {
109
+ return zipEntry.read();
110
+ } catch (error) {
111
+ if (error instanceof ZipFormatError) return fail(error.message);
112
+ throw error;
113
+ }
114
+ }
115
+ export { planBaselineImport, writePlannedImage };
116
+
117
+ //# sourceMappingURL=baseline-import.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline-import.mjs","names":[],"sources":["../../src/models/baseline-import.ts"],"sourcesContent":["/**\n * Validate and apply a baseline archive (format v1).\n *\n * All-or-nothing validation: the manifest, every identity (through the same\n * segment checks as {@link baselinePath}), every image's size, PNG signature\n * and SHA-256 are verified before the first write. Storage paths are always\n * derived from the manifest identity — never from zip entry names.\n */\nimport { createHash } from \"node:crypto\";\nimport { setImmediate } from \"node:timers/promises\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { baselinePath, contentPath } from \"../utils/paths.ts\";\nimport { readZip, ZipFormatError, type ZipEntry } from \"../utils/zip-read.ts\";\nimport {\n BASELINE_ARCHIVE_LIMITS,\n BASELINE_MANIFEST_NAME,\n BaselineArchiveError,\n baselineIdentity,\n baselineManifestSchema,\n isPng,\n type BaselineManifest,\n type BaselineManifestEntry,\n} from \"./baseline-archive.ts\";\n\n/** A validated baseline ready to write. */\nexport interface PlannedBaseline {\n entry: BaselineManifestEntry;\n /** Content-addressed destination inside the importing project. */\n storagePath: string;\n zipEntry: ZipEntry;\n /** The verified PNG bytes, extracted once during planning. */\n data: Buffer;\n}\n\n/**\n * Parse and fully validate `archive` for import into `projectId`. Inflating and\n * hashing is CPU-bound, so the loop yields to the event loop between entries\n * instead of blocking the server for the whole archive.\n *\n * @throws {BaselineArchiveError} On any malformed, unsafe, or inconsistent input.\n */\nexport async function planBaselineImport(\n archive: Buffer,\n projectId: string,\n): Promise<PlannedBaseline[]> {\n const entries = openArchive(archive);\n const manifest = parseManifest(entries);\n const seen = new Set<string>();\n const plan: PlannedBaseline[] = [];\n for (const entry of manifest.baselines) {\n claimEntry(seen, entry);\n plan.push(planEntry(entries, projectId, entry));\n // oxlint-disable-next-line eslint/no-await-in-loop -- deliberate yield between CPU-bound entries\n await setImmediate();\n }\n return plan;\n}\n\n/** Reject repeated identities or files, so total work stays bounded by the archive's own size limit. */\nfunction claimEntry(seen: Set<string>, entry: BaselineManifestEntry): void {\n const keys = [baselineIdentity(entry), `file:${entry.file}`];\n if (keys.some((key) => seen.has(key))) {\n fail(`Duplicate baseline or file in manifest: ${entry.file}`);\n }\n for (const key of keys) {\n seen.add(key);\n }\n}\n\n/** Write a planned baseline's image to storage. */\nexport async function writePlannedImage(\n storage: StorageAdapter,\n planned: PlannedBaseline,\n): Promise<void> {\n if (!(await storage.exists(planned.storagePath))) {\n await storage.write(planned.storagePath, planned.data);\n }\n}\n\nfunction fail(message: string): never {\n throw new BaselineArchiveError(message);\n}\n\nfunction openArchive(archive: Buffer): Map<string, ZipEntry> {\n try {\n return readZip(archive, {\n maxEntries: BASELINE_ARCHIVE_LIMITS.maxBaselines + 1,\n maxEntryBytes: Math.max(\n BASELINE_ARCHIVE_LIMITS.maxImageBytes,\n BASELINE_ARCHIVE_LIMITS.maxManifestBytes,\n ),\n maxTotalBytes: BASELINE_ARCHIVE_LIMITS.maxArchiveBytes,\n });\n } catch (error) {\n if (error instanceof ZipFormatError) {\n fail(error.message);\n }\n throw error;\n }\n}\n\nfunction parseManifest(entries: Map<string, ZipEntry>): BaselineManifest {\n const file = entries.get(BASELINE_MANIFEST_NAME);\n if (!file) {\n fail(`Archive has no ${BASELINE_MANIFEST_NAME}`);\n }\n if (file.size > BASELINE_ARCHIVE_LIMITS.maxManifestBytes) {\n fail(`${BASELINE_MANIFEST_NAME} exceeds ${BASELINE_ARCHIVE_LIMITS.maxManifestBytes} bytes`);\n }\n const parsed = baselineManifestSchema.safeParse(parseJson(file.read().toString(\"utf8\")));\n if (!parsed.success) {\n const issue = parsed.error.issues[0];\n fail(\n `Invalid ${BASELINE_MANIFEST_NAME}: ${issue?.path.join(\".\") ?? \"\"} ${issue?.message ?? \"\"}`,\n );\n }\n return parsed.data;\n}\n\nfunction parseJson(text: string): unknown {\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return fail(`${BASELINE_MANIFEST_NAME} is not valid JSON`);\n }\n}\n\nfunction planEntry(\n entries: Map<string, ZipEntry>,\n projectId: string,\n entry: BaselineManifestEntry,\n): PlannedBaseline {\n const zipEntry = entries.get(entry.file);\n if (!zipEntry || entry.file === BASELINE_MANIFEST_NAME) {\n fail(`Archive is missing ${entry.file}`);\n }\n if (zipEntry.size > BASELINE_ARCHIVE_LIMITS.maxImageBytes) {\n fail(`${entry.file} exceeds ${BASELINE_ARCHIVE_LIMITS.maxImageBytes} bytes`);\n }\n assertSafeIdentity(projectId, entry);\n const data = verifiedImage(zipEntry, entry);\n const storagePath = contentPath(projectId, createHash(\"sha256\").update(data).digest(\"hex\"));\n return { entry, storagePath, zipEntry, data };\n}\n\n/** The identity only has to be safe here: the image itself is stored by content. */\nfunction assertSafeIdentity(projectId: string, entry: BaselineManifestEntry): void {\n try {\n baselinePath(projectId, entry.branch, entry.storyId, entry.viewport, entry.browser);\n } catch {\n fail(`Unsafe baseline identity for ${entry.file}`);\n }\n}\n\nfunction verifiedImage(zipEntry: ZipEntry, entry: BaselineManifestEntry): Buffer {\n const data = readEntry(zipEntry);\n if (!isPng(data)) {\n fail(`${entry.file} is not a PNG`);\n }\n if (createHash(\"sha256\").update(data).digest(\"hex\") !== entry.sha256) {\n fail(`${entry.file} does not match its manifest checksum`);\n }\n return data;\n}\n\nfunction readEntry(zipEntry: ZipEntry): Buffer {\n try {\n return zipEntry.read();\n } catch (error) {\n if (error instanceof ZipFormatError) {\n return fail(error.message);\n }\n throw error;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { Build } from "../schema/build.mjs";
4
+ import { Baseline } from "../schema/baseline.mjs";
5
+ import { Snapshot } from "../schema/snapshot.mjs";
6
+ import "../schema.mjs";
7
+ /** The records needed to prove which build rendered a baseline image. */
8
+ interface BaselineOriginReader {
9
+ baseline(id: string): Promise<Baseline | null>;
10
+ snapshot(id: string): Promise<Snapshot | null>;
11
+ build(id: string): Promise<Build | null>;
12
+ }
13
+ /** A proven source, or null when the recorded history is incomplete or has changed. */
14
+ declare function resolveBaselineOrigin(baseline: Baseline, reader: BaselineOriginReader): Promise<Build | null>;
15
+ export { BaselineOriginReader, resolveBaselineOrigin };
16
+ //# sourceMappingURL=baseline-origin.d.mts.map
@@ -0,0 +1,35 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import "../adapters/capture-target.mjs";
5
+ // src/models/baseline-origin.ts
6
+ /** A proven source, or null when the recorded history is incomplete or has changed. */
7
+ async function resolveBaselineOrigin(baseline, reader) {
8
+ return await traceOrigin(baseline, baseline.projectId, reader, /* @__PURE__ */ new Set());
9
+ }
10
+ async function traceOrigin(current, projectId, reader, seen) {
11
+ if (current.sourceBuildId) return await verifiedBuild(current.sourceBuildId, projectId, reader);
12
+ if (seen.size >= 32 || seen.has(current.id) || !current.snapshotId) return null;
13
+ seen.add(current.id);
14
+ const snapshot = await reader.snapshot(current.snapshotId);
15
+ if (!snapshot || !matchesImage(current, snapshot)) return null;
16
+ if (!snapshot.inherited) return await verifiedBuild(snapshot.buildId, projectId, reader);
17
+ return await traceInherited(snapshot, projectId, reader, seen);
18
+ }
19
+ async function traceInherited(snapshot, projectId, reader, seen) {
20
+ if (snapshot.sourceBuildId) return await verifiedBuild(snapshot.sourceBuildId, projectId, reader);
21
+ if (!snapshot.baselineId || !snapshot.baselineVersion) return null;
22
+ const parent = await reader.baseline(snapshot.baselineId);
23
+ if (!parent || parent.updatedAt !== snapshot.baselineVersion || !matchesImage(parent, snapshot)) return null;
24
+ return await traceOrigin(parent, projectId, reader, seen);
25
+ }
26
+ function matchesImage(baseline, snapshot) {
27
+ return baseline.projectId === snapshot.projectId && baseline.storyId === snapshot.storyId && baseline.viewportName === snapshot.viewportName && (baseline.browser ?? "chromium") === (snapshot.browser ?? "chromium") && baseline.screenshotPath === snapshot.screenshotPath;
28
+ }
29
+ async function verifiedBuild(buildId, projectId, reader) {
30
+ const build = await reader.build(buildId);
31
+ return build?.projectId === projectId ? build : null;
32
+ }
33
+ export { resolveBaselineOrigin };
34
+
35
+ //# sourceMappingURL=baseline-origin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline-origin.mjs","names":[],"sources":["../../src/models/baseline-origin.ts"],"sourcesContent":["import { DEFAULT_BROWSER } from \"../adapters/capture-runner.ts\";\nimport type { Baseline, Build, Snapshot } from \"../schema/index.ts\";\n\n/** The records needed to prove which build rendered a baseline image. */\nexport interface BaselineOriginReader {\n baseline(id: string): Promise<Baseline | null>;\n snapshot(id: string): Promise<Snapshot | null>;\n build(id: string): Promise<Build | null>;\n}\n\n/** A proven source, or null when the recorded history is incomplete or has changed. */\nexport async function resolveBaselineOrigin(\n baseline: Baseline,\n reader: BaselineOriginReader,\n): Promise<Build | null> {\n return await traceOrigin(baseline, baseline.projectId, reader, new Set());\n}\n\nasync function traceOrigin(\n current: Baseline,\n projectId: string,\n reader: BaselineOriginReader,\n seen: Set<string>,\n): Promise<Build | null> {\n if (current.sourceBuildId) return await verifiedBuild(current.sourceBuildId, projectId, reader);\n if (seen.size >= 32 || seen.has(current.id) || !current.snapshotId) return null;\n seen.add(current.id);\n const snapshot = await reader.snapshot(current.snapshotId);\n if (!snapshot || !matchesImage(current, snapshot)) return null;\n if (!snapshot.inherited) return await verifiedBuild(snapshot.buildId, projectId, reader);\n return await traceInherited(snapshot, projectId, reader, seen);\n}\n\nasync function traceInherited(\n snapshot: Snapshot,\n projectId: string,\n reader: BaselineOriginReader,\n seen: Set<string>,\n): Promise<Build | null> {\n if (snapshot.sourceBuildId) return await verifiedBuild(snapshot.sourceBuildId, projectId, reader);\n if (!snapshot.baselineId || !snapshot.baselineVersion) return null;\n const parent = await reader.baseline(snapshot.baselineId);\n if (!parent || parent.updatedAt !== snapshot.baselineVersion || !matchesImage(parent, snapshot))\n return null;\n return await traceOrigin(parent, projectId, reader, seen);\n}\n\nfunction matchesImage(baseline: Baseline, snapshot: Snapshot): boolean {\n return (\n baseline.projectId === snapshot.projectId &&\n baseline.storyId === snapshot.storyId &&\n baseline.viewportName === snapshot.viewportName &&\n (baseline.browser ?? DEFAULT_BROWSER) === (snapshot.browser ?? DEFAULT_BROWSER) &&\n baseline.screenshotPath === snapshot.screenshotPath\n );\n}\n\nasync function verifiedBuild(\n buildId: string,\n projectId: string,\n reader: BaselineOriginReader,\n): Promise<Build | null> {\n const build = await reader.build(buildId);\n return build?.projectId === projectId ? build : null;\n}\n"],"mappings":";;;;;;AAWA,eAAsB,sBACpB,UACA,QACuB;CACvB,OAAO,MAAM,YAAY,UAAU,SAAS,WAAW,wBAAQ,IAAI,IAAI,CAAC;AAC1E;AAEA,eAAe,YACb,SACA,WACA,QACA,MACuB;CACvB,IAAI,QAAQ,eAAe,OAAO,MAAM,cAAc,QAAQ,eAAe,WAAW,MAAM;CAC9F,IAAI,KAAK,QAAQ,MAAM,KAAK,IAAI,QAAQ,EAAE,KAAK,CAAC,QAAQ,YAAY,OAAO;CAC3E,KAAK,IAAI,QAAQ,EAAE;CACnB,MAAM,WAAW,MAAM,OAAO,SAAS,QAAQ,UAAU;CACzD,IAAI,CAAC,YAAY,CAAC,aAAa,SAAS,QAAQ,GAAG,OAAO;CAC1D,IAAI,CAAC,SAAS,WAAW,OAAO,MAAM,cAAc,SAAS,SAAS,WAAW,MAAM;CACvF,OAAO,MAAM,eAAe,UAAU,WAAW,QAAQ,IAAI;AAC/D;AAEA,eAAe,eACb,UACA,WACA,QACA,MACuB;CACvB,IAAI,SAAS,eAAe,OAAO,MAAM,cAAc,SAAS,eAAe,WAAW,MAAM;CAChG,IAAI,CAAC,SAAS,cAAc,CAAC,SAAS,iBAAiB,OAAO;CAC9D,MAAM,SAAS,MAAM,OAAO,SAAS,SAAS,UAAU;CACxD,IAAI,CAAC,UAAU,OAAO,cAAc,SAAS,mBAAmB,CAAC,aAAa,QAAQ,QAAQ,GAC5F,OAAO;CACT,OAAO,MAAM,YAAY,QAAQ,WAAW,QAAQ,IAAI;AAC1D;AAEA,SAAS,aAAa,UAAoB,UAA6B;CACrE,OACE,SAAS,cAAc,SAAS,aAChC,SAAS,YAAY,SAAS,WAC9B,SAAS,iBAAiB,SAAS,iBAClC,SAAS,WAAA,iBAAiC,SAAS,WAAA,eACpD,SAAS,mBAAmB,SAAS;AAEzC;AAEA,eAAe,cACb,SACA,WACA,QACuB;CACvB,MAAM,QAAQ,MAAM,OAAO,MAAM,OAAO;CACxC,OAAO,OAAO,cAAc,YAAY,QAAQ;AAClD"}
@@ -2,7 +2,6 @@ import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { Baseline } from "../schema/baseline.mjs";
4
4
  import { Snapshot } from "../schema/snapshot.mjs";
5
- //#region src/models/baseline-status.d.ts
6
5
  /** `baselineVersion` recorded when a story had no baseline at diff time. */
7
6
  declare const NO_BASELINE = "none";
8
7
  /**
@@ -24,6 +23,5 @@ type TrackedSnapshot = Pick<Snapshot, "baselineId" | "baselineVersion">;
24
23
  declare function baselineStatus(snapshot: TrackedSnapshot, current: Baseline | null): BaselineStatus;
25
24
  /** True when approving should be refused until the diff is recomputed (or forced). */
26
25
  declare function isBaselineDrifted(status: BaselineStatus): boolean;
27
- //#endregion
28
26
  export { BaselineStatus, NO_BASELINE, baselineStatus, isBaselineDrifted };
29
27
  //# sourceMappingURL=baseline-status.d.mts.map
@@ -1,7 +1,7 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- //#region src/models/baseline-status.ts
4
+ // src/models/baseline-status.ts
5
5
  /** `baselineVersion` recorded when a story had no baseline at diff time. */
6
6
  const NO_BASELINE = "none";
7
7
  /**
@@ -20,7 +20,6 @@ function baselineStatus(snapshot, current) {
20
20
  function isBaselineDrifted(status) {
21
21
  return status === "stale" || status === "removed";
22
22
  }
23
- //#endregion
24
23
  export { NO_BASELINE, baselineStatus, isBaselineDrifted };
25
24
 
26
25
  //# sourceMappingURL=baseline-status.mjs.map
@@ -0,0 +1,48 @@
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 { BaselineTables } from "./baseline.mjs";
6
+ import { BaselineExportOptions } from "./baseline-export.mjs";
7
+ /** Outcome of {@link BaselineTransferModel.import}. */
8
+ interface BaselineImportResult {
9
+ /** Baselines in the archive (all applied). */
10
+ imported: number;
11
+ /** Baselines that did not exist before. */
12
+ created: number;
13
+ /** Existing baselines that were overwritten. */
14
+ updated: number;
15
+ }
16
+ /** Export/import of baselines between StoryShelf instances (or for backup). */
17
+ declare class BaselineTransferModel {
18
+ private readonly tables;
19
+ private readonly baselines;
20
+ private readonly db;
21
+ private readonly storage;
22
+ constructor(db: DatabaseAdapter, storage: StorageAdapter, tables?: BaselineTables);
23
+ /**
24
+ * Stream the project's baselines (optionally one branch) as a zip archive.
25
+ *
26
+ * @throws {BaselineArchiveError} Before streaming, when over the archive entry limit.
27
+ */
28
+ export(projectId: string, options?: BaselineExportOptions): Promise<AsyncIterable<Buffer>>;
29
+ /**
30
+ * Upsert every baseline in `archive` into the project. The whole archive is
31
+ * validated before anything is written; images are then stored (idempotent,
32
+ * safe to retry) and all rows are applied in one transaction when the driver
33
+ * supports it, so a failure leaves the rows untouched. Imports into the same
34
+ * project run one at a time. Imported rows keep their original timestamps;
35
+ * `snapshotId` is cleared because snapshots are not portable.
36
+ *
37
+ * @throws {BaselineArchiveError} On a malformed, unsafe, or inconsistent archive.
38
+ */
39
+ import(projectId: string, archive: Buffer): Promise<BaselineImportResult>;
40
+ private existingByIdentity;
41
+ /** Apply every row, in a transaction when available; returns how many were created. */
42
+ private applyRows;
43
+ private assertUnchangedIdentities;
44
+ /** Write one planned row; resolves true when it was created. */
45
+ private applyRow;
46
+ }
47
+ export { BaselineImportResult, BaselineTransferModel };
48
+ //# sourceMappingURL=baseline-transfer.d.mts.map
@@ -0,0 +1,149 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import "../adapters/capture-target.mjs";
5
+ import { ulid } from "../utils/ulid.mjs";
6
+ import { withContentClaim } from "../utils/screenshot-store.mjs";
7
+ import { BaselineWriteConflict } from "./baseline-conflict.mjs";
8
+ import { BaselineModel } from "./baseline.mjs";
9
+ import { baselineIdentity } from "./baseline-archive.mjs";
10
+ import { exportBaselineArchive } from "./baseline-export.mjs";
11
+ import { planBaselineImport, writePlannedImage } from "./baseline-import.mjs";
12
+ // src/models/baseline-transfer.ts
13
+ /**
14
+ * Baseline portability: export a project's baselines as a zip archive and
15
+ * import (upsert) them from the same format. Format and limits live in
16
+ * `baseline-archive.ts`; see the portability guide in the docs site.
17
+ */
18
+ /** Export/import of baselines between StoryShelf instances (or for backup). */
19
+ var BaselineTransferModel = class {
20
+ tables;
21
+ baselines;
22
+ db;
23
+ storage;
24
+ constructor(db, storage, tables) {
25
+ this.db = db;
26
+ this.storage = storage;
27
+ this.tables = tables ?? { baselines: db.tables.baselines };
28
+ this.baselines = new BaselineModel(db, this.tables, storage);
29
+ }
30
+ /**
31
+ * Stream the project's baselines (optionally one branch) as a zip archive.
32
+ *
33
+ * @throws {BaselineArchiveError} Before streaming, when over the archive entry limit.
34
+ */
35
+ async export(projectId, options = {}) {
36
+ return exportBaselineArchive(this.storage, await this.baselines.list(projectId), options);
37
+ }
38
+ /**
39
+ * Upsert every baseline in `archive` into the project. The whole archive is
40
+ * validated before anything is written; images are then stored (idempotent,
41
+ * safe to retry) and all rows are applied in one transaction when the driver
42
+ * supports it, so a failure leaves the rows untouched. Imports into the same
43
+ * project run one at a time. Imported rows keep their original timestamps;
44
+ * `snapshotId` is cleared because snapshots are not portable.
45
+ *
46
+ * @throws {BaselineArchiveError} On a malformed, unsafe, or inconsistent archive.
47
+ */
48
+ async import(projectId, archive) {
49
+ const plan = await planBaselineImport(archive, projectId);
50
+ return await serialized(projectId, async () => await withContentClaim(this.storage, projectId, async () => {
51
+ for (const planned of plan) await writePlannedImage(this.storage, planned);
52
+ const existing = await this.existingByIdentity(projectId);
53
+ const created = await this.applyRows(projectId, plan, existing);
54
+ return {
55
+ imported: plan.length,
56
+ created,
57
+ updated: plan.length - created
58
+ };
59
+ }));
60
+ }
61
+ async existingByIdentity(projectId) {
62
+ const rows = await this.baselines.list(projectId);
63
+ return new Map(rows.map((row) => [baselineIdentity({
64
+ storyId: row.storyId,
65
+ viewport: row.viewportName,
66
+ browser: row.browser ?? "chromium",
67
+ branch: row.branch
68
+ }), row]));
69
+ }
70
+ /** Apply every row, in a transaction when available; returns how many were created. */
71
+ async applyRows(projectId, plan, existing) {
72
+ const run = async (store) => {
73
+ let created = 0;
74
+ for (const planned of plan) created += await this.applyRow(store, projectId, planned, existing) ? 1 : 0;
75
+ return created;
76
+ };
77
+ const lifecycle = this.db.buildReferenceLifecycle;
78
+ if (lifecycle) {
79
+ const touched = plan.map((planned) => existing.get(baselineIdentity(planned.entry))).filter((row) => row !== void 0);
80
+ const changes = await Promise.all(touched.map(async (row) => {
81
+ const owner = {
82
+ type: "baseline",
83
+ id: row.id,
84
+ projectId
85
+ };
86
+ return {
87
+ owner,
88
+ expectedGeneration: await lifecycle.referenceGeneration(owner),
89
+ buildIds: []
90
+ };
91
+ }));
92
+ return (await lifecycle.replaceManyFencedReferences(changes, async (store) => {
93
+ await this.assertUnchangedIdentities(store, touched);
94
+ return await run(store);
95
+ })).result;
96
+ }
97
+ return this.db.transact ? await this.db.transact(run) : await run(this.db);
98
+ }
99
+ async assertUnchangedIdentities(store, rows) {
100
+ const current = await Promise.all(rows.map(async (row) => await store.get(this.tables.baselines, row.id)));
101
+ for (const [index, row] of rows.entries()) {
102
+ const found = current[index];
103
+ if (!found || found.projectId !== row.projectId || found.storyId !== row.storyId || found.viewportName !== row.viewportName || found.branch !== row.branch || found.updatedAt !== row.updatedAt || found.snapshotId !== row.snapshotId || found.sourceBuildId !== row.sourceBuildId || found.screenshotPath !== row.screenshotPath || (found.browser ?? "chromium") !== (row.browser ?? "chromium")) throw new BaselineWriteConflict("Baseline changed during import; retry");
104
+ }
105
+ }
106
+ /** Write one planned row; resolves true when it was created. */
107
+ async applyRow(store, projectId, planned, existing) {
108
+ const { entry, storagePath } = planned;
109
+ const fields = {
110
+ snapshotId: null,
111
+ sourceBuildId: null,
112
+ screenshotPath: storagePath,
113
+ infraHash: entry.infraHash,
114
+ fingerprint: entry.fingerprint ?? null,
115
+ mode: entry.mode,
116
+ createdAt: entry.createdAt,
117
+ updatedAt: entry.updatedAt
118
+ };
119
+ const current = existing.get(baselineIdentity(entry));
120
+ if (current) {
121
+ await store.update(this.tables.baselines, current.id, fields);
122
+ return false;
123
+ }
124
+ await store.insert(this.tables.baselines, {
125
+ ...fields,
126
+ id: ulid(),
127
+ projectId,
128
+ storyId: entry.storyId,
129
+ viewportName: entry.viewport,
130
+ branch: entry.branch,
131
+ browser: entry.browser
132
+ });
133
+ return true;
134
+ }
135
+ };
136
+ const importQueues = /* @__PURE__ */ new Map();
137
+ /** Run `task` after earlier work for the same key settles (in-process serialization). */
138
+ async function serialized(key, task) {
139
+ const run = (importQueues.get(key) ?? Promise.resolve()).then(task, task);
140
+ importQueues.set(key, run);
141
+ try {
142
+ return await run;
143
+ } finally {
144
+ if (importQueues.get(key) === run) importQueues.delete(key);
145
+ }
146
+ }
147
+ export { BaselineTransferModel };
148
+
149
+ //# sourceMappingURL=baseline-transfer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline-transfer.mjs","names":[],"sources":["../../src/models/baseline-transfer.ts"],"sourcesContent":["/**\n * Baseline portability: export a project's baselines as a zip archive and\n * import (upsert) them from the same format. Format and limits live in\n * `baseline-archive.ts`; see the portability guide in the docs site.\n */\nimport { DEFAULT_BROWSER } from \"../adapters/capture-runner.ts\";\nimport type { DatabaseAdapter, TxStore } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport type { Baseline } from \"../schema/baseline.ts\";\nimport { withContentClaim } from \"../utils/screenshot-store.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\nimport { baselineIdentity } from \"./baseline-archive.ts\";\nimport { BaselineWriteConflict } from \"./baseline-conflict.ts\";\nimport { exportBaselineArchive, type BaselineExportOptions } from \"./baseline-export.ts\";\nimport { planBaselineImport, writePlannedImage, type PlannedBaseline } from \"./baseline-import.ts\";\nimport { BaselineModel, type BaselineTables } from \"./baseline.ts\";\n\n/** Outcome of {@link BaselineTransferModel.import}. */\nexport interface BaselineImportResult {\n /** Baselines in the archive (all applied). */\n imported: number;\n /** Baselines that did not exist before. */\n created: number;\n /** Existing baselines that were overwritten. */\n updated: number;\n}\n\n/** Export/import of baselines between StoryShelf instances (or for backup). */\nexport class BaselineTransferModel {\n private readonly tables: BaselineTables;\n private readonly baselines: BaselineModel;\n private readonly db: DatabaseAdapter;\n private readonly storage: StorageAdapter;\n\n constructor(db: DatabaseAdapter, storage: StorageAdapter, tables?: BaselineTables) {\n this.db = db;\n this.storage = storage;\n this.tables = tables ?? { baselines: db.tables.baselines };\n this.baselines = new BaselineModel(db, this.tables, storage);\n }\n\n /**\n * Stream the project's baselines (optionally one branch) as a zip archive.\n *\n * @throws {BaselineArchiveError} Before streaming, when over the archive entry limit.\n */\n async export(\n projectId: string,\n options: BaselineExportOptions = {},\n ): Promise<AsyncIterable<Buffer>> {\n return exportBaselineArchive(this.storage, await this.baselines.list(projectId), options);\n }\n\n /**\n * Upsert every baseline in `archive` into the project. The whole archive is\n * validated before anything is written; images are then stored (idempotent,\n * safe to retry) and all rows are applied in one transaction when the driver\n * supports it, so a failure leaves the rows untouched. Imports into the same\n * project run one at a time. Imported rows keep their original timestamps;\n * `snapshotId` is cleared because snapshots are not portable.\n *\n * @throws {BaselineArchiveError} On a malformed, unsafe, or inconsistent archive.\n */\n async import(projectId: string, archive: Buffer): Promise<BaselineImportResult> {\n const plan = await planBaselineImport(archive, projectId);\n // Images that already exist are reused, not written: claim the project's content so the\n // screenshot sweep cannot delete one before the rows pointing at it are committed.\n return await serialized(\n projectId,\n async () =>\n await withContentClaim(this.storage, projectId, async () => {\n for (const planned of plan) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- sequential writes bound storage load\n await writePlannedImage(this.storage, planned);\n }\n const existing = await this.existingByIdentity(projectId);\n const created = await this.applyRows(projectId, plan, existing);\n return { imported: plan.length, created, updated: plan.length - created };\n }),\n );\n }\n\n private async existingByIdentity(projectId: string): Promise<Map<string, Baseline>> {\n const rows = await this.baselines.list(projectId);\n return new Map(\n rows.map((row) => [\n baselineIdentity({\n storyId: row.storyId,\n viewport: row.viewportName,\n browser: row.browser ?? DEFAULT_BROWSER,\n branch: row.branch,\n }),\n row,\n ]),\n );\n }\n\n /** Apply every row, in a transaction when available; returns how many were created. */\n private async applyRows(\n projectId: string,\n plan: PlannedBaseline[],\n existing: Map<string, Baseline>,\n ): Promise<number> {\n const run = async (store: TxStore): Promise<number> => {\n let created = 0;\n for (const planned of plan) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- rows are applied in order within one transaction\n created += (await this.applyRow(store, projectId, planned, existing)) ? 1 : 0;\n }\n return created;\n };\n const lifecycle = this.db.buildReferenceLifecycle;\n if (lifecycle) {\n const touched = plan\n .map((planned) => existing.get(baselineIdentity(planned.entry)))\n .filter((row): row is Baseline => row !== undefined);\n const changes = await Promise.all(\n touched.map(async (row) => {\n const owner = { type: \"baseline\" as const, id: row.id, projectId };\n return {\n owner,\n expectedGeneration: await lifecycle.referenceGeneration(owner),\n buildIds: [],\n };\n }),\n );\n const updated = await lifecycle.replaceManyFencedReferences(changes, async (store) => {\n await this.assertUnchangedIdentities(store, touched);\n return await run(store);\n });\n return updated.result;\n }\n return this.db.transact ? await this.db.transact(run) : await run(this.db);\n }\n\n private async assertUnchangedIdentities(store: TxStore, rows: Baseline[]): Promise<void> {\n const current = await Promise.all(\n rows.map(\n async (row) =>\n (await store.get(this.tables.baselines, row.id)) as unknown as Baseline | null,\n ),\n );\n for (const [index, row] of rows.entries()) {\n const found = current[index];\n if (\n !found ||\n found.projectId !== row.projectId ||\n found.storyId !== row.storyId ||\n found.viewportName !== row.viewportName ||\n found.branch !== row.branch ||\n found.updatedAt !== row.updatedAt ||\n found.snapshotId !== row.snapshotId ||\n found.sourceBuildId !== row.sourceBuildId ||\n found.screenshotPath !== row.screenshotPath ||\n (found.browser ?? DEFAULT_BROWSER) !== (row.browser ?? DEFAULT_BROWSER)\n ) {\n throw new BaselineWriteConflict(\"Baseline changed during import; retry\");\n }\n }\n }\n\n /** Write one planned row; resolves true when it was created. */\n private async applyRow(\n store: TxStore,\n projectId: string,\n planned: PlannedBaseline,\n existing: Map<string, Baseline>,\n ): Promise<boolean> {\n const { entry, storagePath } = planned;\n const fields = {\n snapshotId: null,\n sourceBuildId: null,\n screenshotPath: storagePath,\n infraHash: entry.infraHash,\n fingerprint: entry.fingerprint ?? null,\n mode: entry.mode,\n createdAt: entry.createdAt,\n updatedAt: entry.updatedAt,\n } satisfies Partial<Baseline>;\n const current = existing.get(baselineIdentity(entry));\n if (current) {\n await store.update(this.tables.baselines, current.id, fields);\n return false;\n }\n await store.insert(this.tables.baselines, {\n ...fields,\n id: ulid(),\n projectId,\n storyId: entry.storyId,\n viewportName: entry.viewport,\n branch: entry.branch,\n browser: entry.browser,\n });\n return true;\n }\n}\n\nconst importQueues = new Map<string, Promise<unknown>>();\n\n/** Run `task` after earlier work for the same key settles (in-process serialization). */\nasync function serialized<T>(key: string, task: () => Promise<T>): Promise<T> {\n const run = (importQueues.get(key) ?? Promise.resolve()).then(task, task);\n importQueues.set(key, run);\n try {\n return await run;\n } finally {\n if (importQueues.get(key) === run) {\n importQueues.delete(key);\n }\n }\n}\n"],"mappings":"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"}