@storyshelf/core 0.6.1 → 0.7.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 (631) hide show
  1. package/README.md +49 -32
  2. package/dist/adapter/capture-queue.d.mts +44 -2
  3. package/dist/adapter/capture-runner.d.mts +127 -10
  4. package/dist/adapter/capture-runner.mjs +5 -12
  5. package/dist/adapter/database.d.mts +27 -8
  6. package/dist/adapter/database.mjs.map +1 -1
  7. package/dist/adapter/design.d.mts +85 -0
  8. package/dist/adapter/design.mjs +28 -0
  9. package/dist/adapter/design.mjs.map +1 -0
  10. package/dist/adapter/metadata.d.mts +3 -10
  11. package/dist/adapter/notifier.d.mts +6 -3
  12. package/dist/adapter/notifier.mjs +5 -1
  13. package/dist/adapter/oauth.d.mts +52 -0
  14. package/dist/adapter/setup.d.mts +4 -5
  15. package/dist/adapter/setup.mjs +2 -1
  16. package/dist/adapter/setup.mjs.map +1 -1
  17. package/dist/adapter/webhook-events.mjs +1 -1
  18. package/dist/adapters/capture-target-offers.d.mts +28 -0
  19. package/dist/adapters/capture-target-offers.mjs +58 -0
  20. package/dist/adapters/capture-target-offers.mjs.map +1 -0
  21. package/dist/adapters/capture-target.d.mts +71 -0
  22. package/dist/adapters/capture-target.mjs +76 -0
  23. package/dist/adapters/capture-target.mjs.map +1 -0
  24. package/dist/adapters/notifier/build-comment.d.mts +24 -0
  25. package/dist/adapters/notifier/build-comment.mjs +24 -0
  26. package/dist/adapters/notifier/build-comment.mjs.map +1 -0
  27. package/dist/adapters/notifier/build-state.d.mts +99 -0
  28. package/dist/adapters/notifier/build-state.mjs +218 -0
  29. package/dist/adapters/notifier/build-state.mjs.map +1 -0
  30. package/dist/adapters/notifier/events.d.mts +4 -2
  31. package/dist/adapters/notifier/events.mjs +31 -18
  32. package/dist/adapters/notifier/events.mjs.map +1 -1
  33. package/dist/adapters/notifier/provider.d.mts +53 -7
  34. package/dist/adapters/notifier/provider.mjs +12 -0
  35. package/dist/adapters/notifier/provider.mjs.map +1 -0
  36. package/dist/adapters/notifier/state.d.mts +24 -0
  37. package/dist/adapters/notifier/state.mjs +66 -0
  38. package/dist/adapters/notifier/state.mjs.map +1 -0
  39. package/dist/adapters/notifier/types.d.mts +51 -2
  40. package/dist/adapters/remote-browser.d.mts +87 -0
  41. package/dist/adapters/remote-browser.mjs +103 -0
  42. package/dist/adapters/remote-browser.mjs.map +1 -0
  43. package/dist/adapters/render-runs.d.mts +25 -0
  44. package/dist/adapters/render-runs.mjs +41 -0
  45. package/dist/adapters/render-runs.mjs.map +1 -0
  46. package/dist/adapters/setup-runner.mjs +2 -4
  47. package/dist/adapters/setup-runner.mjs.map +1 -1
  48. package/dist/adapters/sources.d.mts +12 -0
  49. package/dist/adapters/sources.mjs +20 -0
  50. package/dist/adapters/sources.mjs.map +1 -0
  51. package/dist/adapters/validate.mjs +25 -18
  52. package/dist/adapters/validate.mjs.map +1 -1
  53. package/dist/ai.d.mts +105 -0
  54. package/dist/ai.mjs +19 -0
  55. package/dist/ai.mjs.map +1 -0
  56. package/dist/auth.d.mts +8 -1
  57. package/dist/auth.mjs +14 -1
  58. package/dist/auth.mjs.map +1 -0
  59. package/dist/capture/a11y/axe-source.d.mts +18 -0
  60. package/dist/capture/a11y/axe-source.mjs +37 -0
  61. package/dist/capture/a11y/axe-source.mjs.map +1 -0
  62. package/dist/capture/a11y/in-page.d.mts +27 -0
  63. package/dist/capture/a11y/in-page.mjs +60 -0
  64. package/dist/capture/a11y/in-page.mjs.map +1 -0
  65. package/dist/capture/a11y/map.d.mts +24 -0
  66. package/dist/capture/a11y/map.mjs +48 -0
  67. package/dist/capture/a11y/map.mjs.map +1 -0
  68. package/dist/capture/a11y/options.d.mts +23 -0
  69. package/dist/capture/a11y/options.mjs +75 -0
  70. package/dist/capture/a11y/options.mjs.map +1 -0
  71. package/dist/capture/adapter.d.mts +41 -3
  72. package/dist/capture/adapter.mjs.map +1 -1
  73. package/dist/capture/affected.d.mts +2 -0
  74. package/dist/capture/affected.mjs +13 -4
  75. package/dist/capture/affected.mjs.map +1 -1
  76. package/dist/capture/attempt-logger.mjs +84 -0
  77. package/dist/capture/attempt-logger.mjs.map +1 -0
  78. package/dist/capture/browsers.d.mts +35 -0
  79. package/dist/capture/browsers.mjs +67 -0
  80. package/dist/capture/browsers.mjs.map +1 -0
  81. package/dist/capture/build-state/deliver.d.mts +32 -0
  82. package/dist/capture/build-state/deliver.mjs +118 -0
  83. package/dist/capture/build-state/deliver.mjs.map +1 -0
  84. package/dist/capture/build-state/index.d.mts +8 -0
  85. package/dist/capture/build-state/notify-job.d.mts +56 -0
  86. package/dist/capture/build-state/notify-job.mjs +133 -0
  87. package/dist/capture/build-state/notify-job.mjs.map +1 -0
  88. package/dist/capture/build-state/publisher.d.mts +23 -0
  89. package/dist/capture/build-state/publisher.mjs +107 -0
  90. package/dist/capture/build-state/publisher.mjs.map +1 -0
  91. package/dist/capture/build-state/resume.d.mts +9 -0
  92. package/dist/capture/build-state/resume.mjs +47 -0
  93. package/dist/capture/build-state/resume.mjs.map +1 -0
  94. package/dist/capture/build-state/types.d.mts +49 -0
  95. package/dist/capture/catalog/catalogue.d.mts +22 -0
  96. package/dist/capture/catalog/catalogue.mjs +53 -0
  97. package/dist/capture/catalog/catalogue.mjs.map +1 -0
  98. package/dist/capture/catalog/components.d.mts +23 -0
  99. package/dist/capture/catalog/components.mjs +55 -0
  100. package/dist/capture/catalog/components.mjs.map +1 -0
  101. package/dist/capture/catalog/history.d.mts +30 -0
  102. package/dist/capture/catalog/history.mjs +51 -0
  103. package/dist/capture/catalog/history.mjs.map +1 -0
  104. package/dist/capture/catalog/hook.d.mts +14 -0
  105. package/dist/capture/catalog/hook.mjs +19 -0
  106. package/dist/capture/catalog/hook.mjs.map +1 -0
  107. package/dist/capture/catalog/index.d.mts +11 -0
  108. package/dist/capture/catalog/load.d.mts +26 -0
  109. package/dist/capture/catalog/load.mjs +49 -0
  110. package/dist/capture/catalog/load.mjs.map +1 -0
  111. package/dist/capture/catalog/parse.d.mts +15 -0
  112. package/dist/capture/catalog/parse.mjs +56 -0
  113. package/dist/capture/catalog/parse.mjs.map +1 -0
  114. package/dist/capture/catalog/read.d.mts +12 -0
  115. package/dist/capture/catalog/read.mjs +33 -0
  116. package/dist/capture/catalog/read.mjs.map +1 -0
  117. package/dist/capture/catalog/types.d.mts +33 -0
  118. package/dist/capture/consumer/capture-handler.d.mts +58 -0
  119. package/dist/capture/consumer/capture-handler.mjs +118 -0
  120. package/dist/capture/consumer/capture-handler.mjs.map +1 -0
  121. package/dist/capture/consumer/idle.d.mts +17 -0
  122. package/dist/capture/consumer/idle.mjs +25 -0
  123. package/dist/capture/consumer/idle.mjs.map +1 -0
  124. package/dist/capture/consumer/index.d.mts +13 -0
  125. package/dist/capture/consumer/index.mjs +139 -0
  126. package/dist/capture/consumer/index.mjs.map +1 -0
  127. package/dist/capture/consumer/loop.mjs +110 -0
  128. package/dist/capture/consumer/loop.mjs.map +1 -0
  129. package/dist/capture/consumer/metrics.d.mts +41 -0
  130. package/dist/capture/consumer/metrics.mjs +38 -0
  131. package/dist/capture/consumer/metrics.mjs.map +1 -0
  132. package/dist/capture/consumer/retry.d.mts +33 -0
  133. package/dist/capture/consumer/retry.mjs +34 -0
  134. package/dist/capture/consumer/retry.mjs.map +1 -0
  135. package/dist/capture/consumer/run-job.mjs +195 -0
  136. package/dist/capture/consumer/run-job.mjs.map +1 -0
  137. package/dist/capture/consumer/startup.mjs +38 -0
  138. package/dist/capture/consumer/startup.mjs.map +1 -0
  139. package/dist/capture/consumer/types.d.mts +97 -0
  140. package/dist/capture/crash-failure.d.mts +25 -0
  141. package/dist/capture/crash-failure.mjs +70 -0
  142. package/dist/capture/crash-failure.mjs.map +1 -0
  143. package/dist/capture/db-queue/cadence.d.mts +32 -0
  144. package/dist/capture/db-queue/cadence.mjs +38 -0
  145. package/dist/capture/db-queue/cadence.mjs.map +1 -0
  146. package/dist/capture/db-queue/claim.d.mts +9 -0
  147. package/dist/capture/db-queue/claim.mjs +132 -0
  148. package/dist/capture/db-queue/claim.mjs.map +1 -0
  149. package/dist/capture/db-queue/clock.mjs +12 -0
  150. package/dist/capture/db-queue/clock.mjs.map +1 -0
  151. package/dist/capture/db-queue/codec.mjs +76 -0
  152. package/dist/capture/db-queue/codec.mjs.map +1 -0
  153. package/dist/capture/db-queue/index.d.mts +20 -0
  154. package/dist/capture/db-queue/index.mjs +89 -0
  155. package/dist/capture/db-queue/index.mjs.map +1 -0
  156. package/dist/capture/db-queue/keeper.d.mts +36 -0
  157. package/dist/capture/db-queue/keeper.mjs +92 -0
  158. package/dist/capture/db-queue/keeper.mjs.map +1 -0
  159. package/dist/capture/db-queue/lease.mjs +46 -0
  160. package/dist/capture/db-queue/lease.mjs.map +1 -0
  161. package/dist/capture/db-queue/lifecycle.mjs +36 -0
  162. package/dist/capture/db-queue/lifecycle.mjs.map +1 -0
  163. package/dist/capture/db-queue/operations.mjs +94 -0
  164. package/dist/capture/db-queue/operations.mjs.map +1 -0
  165. package/dist/capture/db-queue/poll-loop.d.mts +30 -0
  166. package/dist/capture/db-queue/poll-loop.mjs +89 -0
  167. package/dist/capture/db-queue/poll-loop.mjs.map +1 -0
  168. package/dist/capture/db-queue/poll.d.mts +10 -0
  169. package/dist/capture/db-queue/poll.mjs +69 -0
  170. package/dist/capture/db-queue/poll.mjs.map +1 -0
  171. package/dist/capture/db-queue/timers.d.mts +12 -0
  172. package/dist/capture/db-queue/timers.mjs +37 -0
  173. package/dist/capture/db-queue/timers.mjs.map +1 -0
  174. package/dist/capture/db-queue/types.d.mts +83 -0
  175. package/dist/capture/db-queue/types.mjs +12 -0
  176. package/dist/capture/db-queue/types.mjs.map +1 -0
  177. package/dist/capture/db-queue/wake.mjs +48 -0
  178. package/dist/capture/db-queue/wake.mjs.map +1 -0
  179. package/dist/capture/design.mjs +39 -0
  180. package/dist/capture/design.mjs.map +1 -0
  181. package/dist/capture/diff-record.mjs +23 -3
  182. package/dist/capture/diff-record.mjs.map +1 -1
  183. package/dist/capture/dispatch.d.mts +12 -7
  184. package/dist/capture/dispatch.mjs +86 -107
  185. package/dist/capture/dispatch.mjs.map +1 -1
  186. package/dist/capture/enrich.mjs +60 -0
  187. package/dist/capture/enrich.mjs.map +1 -0
  188. package/dist/capture/failure-sets.mjs +22 -0
  189. package/dist/capture/failure-sets.mjs.map +1 -0
  190. package/dist/capture/fingerprint.d.mts +42 -0
  191. package/dist/capture/fingerprint.mjs +83 -0
  192. package/dist/capture/fingerprint.mjs.map +1 -0
  193. package/dist/capture/forked/channel.d.mts +25 -0
  194. package/dist/capture/forked/child.d.mts +34 -0
  195. package/dist/capture/forked/child.mjs +202 -0
  196. package/dist/capture/forked/child.mjs.map +1 -0
  197. package/dist/capture/forked/crash-tracker.mjs +23 -0
  198. package/dist/capture/forked/crash-tracker.mjs.map +1 -0
  199. package/dist/capture/forked/decision.d.mts +26 -0
  200. package/dist/capture/forked/decision.mjs +27 -0
  201. package/dist/capture/forked/decision.mjs.map +1 -0
  202. package/dist/capture/forked/exit.mjs +12 -0
  203. package/dist/capture/forked/exit.mjs.map +1 -0
  204. package/dist/capture/forked/fork.d.mts +31 -0
  205. package/dist/capture/forked/fork.mjs +93 -0
  206. package/dist/capture/forked/fork.mjs.map +1 -0
  207. package/dist/capture/forked/forked-capture.d.mts +40 -0
  208. package/dist/capture/forked/forked-capture.mjs +240 -0
  209. package/dist/capture/forked/forked-capture.mjs.map +1 -0
  210. package/dist/capture/forked/lease-book.mjs +46 -0
  211. package/dist/capture/forked/lease-book.mjs.map +1 -0
  212. package/dist/capture/forked/protocol.d.mts +38 -0
  213. package/dist/capture/forked/protocol.mjs +25 -0
  214. package/dist/capture/forked/protocol.mjs.map +1 -0
  215. package/dist/capture/forked/runs.mjs +75 -0
  216. package/dist/capture/forked/runs.mjs.map +1 -0
  217. package/dist/capture/isolate.mjs +141 -0
  218. package/dist/capture/isolate.mjs.map +1 -0
  219. package/dist/capture/job-lease.d.mts +28 -0
  220. package/dist/capture/job-lease.mjs +57 -0
  221. package/dist/capture/job-lease.mjs.map +1 -0
  222. package/dist/capture/matrix.mjs +298 -0
  223. package/dist/capture/matrix.mjs.map +1 -0
  224. package/dist/capture/modes.d.mts +25 -0
  225. package/dist/capture/modes.mjs +107 -0
  226. package/dist/capture/modes.mjs.map +1 -0
  227. package/dist/capture/orchestrator.d.mts +37 -5
  228. package/dist/capture/orchestrator.mjs +118 -107
  229. package/dist/capture/orchestrator.mjs.map +1 -1
  230. package/dist/capture/pipeline.d.mts +9 -2
  231. package/dist/capture/pipeline.mjs +250 -45
  232. package/dist/capture/pipeline.mjs.map +1 -1
  233. package/dist/capture/progress.d.mts +11 -0
  234. package/dist/capture/progress.mjs +28 -0
  235. package/dist/capture/progress.mjs.map +1 -0
  236. package/dist/capture/publish/accept.d.mts +39 -0
  237. package/dist/capture/publish/accept.mjs +28 -0
  238. package/dist/capture/publish/accept.mjs.map +1 -0
  239. package/dist/capture/publish/complete.d.mts +46 -0
  240. package/dist/capture/publish/complete.mjs +64 -0
  241. package/dist/capture/publish/complete.mjs.map +1 -0
  242. package/dist/capture/publish/extract.d.mts +20 -0
  243. package/dist/capture/publish/extract.mjs +119 -0
  244. package/dist/capture/publish/extract.mjs.map +1 -0
  245. package/dist/capture/publish/handler.d.mts +28 -0
  246. package/dist/capture/publish/handler.mjs +62 -0
  247. package/dist/capture/publish/handler.mjs.map +1 -0
  248. package/dist/capture/publish/index.d.mts +10 -0
  249. package/dist/capture/publish/limits.d.mts +37 -0
  250. package/dist/capture/publish/limits.mjs +39 -0
  251. package/dist/capture/publish/limits.mjs.map +1 -0
  252. package/dist/capture/publish/published.d.mts +15 -0
  253. package/dist/capture/publish/published.mjs +18 -0
  254. package/dist/capture/publish/published.mjs.map +1 -0
  255. package/dist/capture/publish/run.d.mts +33 -0
  256. package/dist/capture/publish/run.mjs +30 -0
  257. package/dist/capture/publish/run.mjs.map +1 -0
  258. package/dist/capture/rediff.d.mts +1 -1
  259. package/dist/capture/rediff.mjs +4 -3
  260. package/dist/capture/rediff.mjs.map +1 -1
  261. package/dist/capture/render.mjs +53 -0
  262. package/dist/capture/render.mjs.map +1 -0
  263. package/dist/capture/scratch.d.mts +44 -0
  264. package/dist/capture/scratch.mjs +138 -0
  265. package/dist/capture/scratch.mjs.map +1 -0
  266. package/dist/capture/shard-plan.mjs +128 -0
  267. package/dist/capture/shard-plan.mjs.map +1 -0
  268. package/dist/capture/shards/context.mjs +51 -0
  269. package/dist/capture/shards/context.mjs.map +1 -0
  270. package/dist/capture/shards/execute.mjs +174 -0
  271. package/dist/capture/shards/execute.mjs.map +1 -0
  272. package/dist/capture/shards/fan-out.mjs +126 -0
  273. package/dist/capture/shards/fan-out.mjs.map +1 -0
  274. package/dist/capture/shards/history.mjs +35 -0
  275. package/dist/capture/shards/history.mjs.map +1 -0
  276. package/dist/capture/shards/reduce.mjs +97 -0
  277. package/dist/capture/shards/reduce.mjs.map +1 -0
  278. package/dist/capture/sizing.mjs +1 -11
  279. package/dist/capture/sizing.mjs.map +1 -1
  280. package/dist/capture/skip-checks.mjs +2 -65
  281. package/dist/capture/skip-checks.mjs.map +1 -1
  282. package/dist/capture/statics.d.mts +7 -8
  283. package/dist/capture/statics.mjs +25 -71
  284. package/dist/capture/statics.mjs.map +1 -1
  285. package/dist/capture/storybook-urls.d.mts +40 -0
  286. package/dist/capture/storybook-urls.mjs +41 -0
  287. package/dist/capture/storybook-urls.mjs.map +1 -0
  288. package/dist/capture/storybook.d.mts +12 -4
  289. package/dist/capture/storybook.mjs +56 -16
  290. package/dist/capture/storybook.mjs.map +1 -1
  291. package/dist/capture/target.mjs +21 -0
  292. package/dist/capture/target.mjs.map +1 -0
  293. package/dist/capture/targets.d.mts +25 -0
  294. package/dist/capture/targets.mjs +32 -0
  295. package/dist/capture/targets.mjs.map +1 -0
  296. package/dist/capture/tests.d.mts +37 -0
  297. package/dist/capture/tests.mjs +126 -0
  298. package/dist/capture/tests.mjs.map +1 -0
  299. package/dist/capture/token-chunks.mjs +74 -0
  300. package/dist/capture/token-chunks.mjs.map +1 -0
  301. package/dist/capture.d.mts +60 -7
  302. package/dist/capture.mjs +48 -3
  303. package/dist/config.d.mts +121 -12
  304. package/dist/config.mjs +9 -6
  305. package/dist/config.mjs.map +1 -1
  306. package/dist/diff.d.mts +9 -3
  307. package/dist/diff.mjs +16 -3
  308. package/dist/diff.mjs.map +1 -0
  309. package/dist/index.d.mts +1 -1
  310. package/dist/insights/budget.d.mts +20 -0
  311. package/dist/insights/budget.mjs +33 -0
  312. package/dist/insights/budget.mjs.map +1 -0
  313. package/dist/insights/envelope.d.mts +59 -0
  314. package/dist/insights/envelope.mjs +60 -0
  315. package/dist/insights/envelope.mjs.map +1 -0
  316. package/dist/insights/evidence-health.d.mts +32 -0
  317. package/dist/insights/evidence-health.mjs +28 -0
  318. package/dist/insights/evidence-health.mjs.map +1 -0
  319. package/dist/insights/evidence.d.mts +55 -0
  320. package/dist/insights/evidence.mjs +64 -0
  321. package/dist/insights/evidence.mjs.map +1 -0
  322. package/dist/insights/hash.d.mts +18 -0
  323. package/dist/insights/hash.mjs +21 -0
  324. package/dist/insights/hash.mjs.map +1 -0
  325. package/dist/insights/limits.d.mts +24 -0
  326. package/dist/insights/limits.mjs +24 -0
  327. package/dist/insights/limits.mjs.map +1 -0
  328. package/dist/insights/prompts/v1.d.mts +13 -0
  329. package/dist/insights/prompts/v1.mjs +34 -0
  330. package/dist/insights/prompts/v1.mjs.map +1 -0
  331. package/dist/insights/redact.d.mts +9 -0
  332. package/dist/insights/redact.mjs +24 -0
  333. package/dist/insights/redact.mjs.map +1 -0
  334. package/dist/insights/text.mjs +44 -0
  335. package/dist/insights/text.mjs.map +1 -0
  336. package/dist/insights.d.mts +11 -0
  337. package/dist/insights.mjs +12 -0
  338. package/dist/logger.mjs +17 -1
  339. package/dist/logger.mjs.map +1 -1
  340. package/dist/models/ai-budget-alert.d.mts +23 -0
  341. package/dist/models/ai-budget-alert.mjs +38 -0
  342. package/dist/models/ai-budget-alert.mjs.map +1 -0
  343. package/dist/models/ai-usage.d.mts +40 -0
  344. package/dist/models/ai-usage.mjs +61 -0
  345. package/dist/models/ai-usage.mjs.map +1 -0
  346. package/dist/models/baseline-archive.d.mts +84 -0
  347. package/dist/models/baseline-archive.mjs +106 -0
  348. package/dist/models/baseline-archive.mjs.map +1 -0
  349. package/dist/models/baseline-export.d.mts +19 -0
  350. package/dist/models/baseline-export.mjs +136 -0
  351. package/dist/models/baseline-export.mjs.map +1 -0
  352. package/dist/models/baseline-import.mjs +118 -0
  353. package/dist/models/baseline-import.mjs.map +1 -0
  354. package/dist/models/baseline-transfer.d.mts +49 -0
  355. package/dist/models/baseline-transfer.mjs +121 -0
  356. package/dist/models/baseline-transfer.mjs.map +1 -0
  357. package/dist/models/baseline.d.mts +32 -5
  358. package/dist/models/baseline.mjs +65 -20
  359. package/dist/models/baseline.mjs.map +1 -1
  360. package/dist/models/build-component.d.mts +28 -0
  361. package/dist/models/build-component.mjs +55 -0
  362. package/dist/models/build-component.mjs.map +1 -0
  363. package/dist/models/build-review-status.d.mts +29 -0
  364. package/dist/models/build-review-status.mjs +37 -0
  365. package/dist/models/build-review-status.mjs.map +1 -0
  366. package/dist/models/build-review.d.mts +28 -0
  367. package/dist/models/build-review.mjs +54 -0
  368. package/dist/models/build-review.mjs.map +1 -0
  369. package/dist/models/build.d.mts +25 -3
  370. package/dist/models/build.mjs +50 -11
  371. package/dist/models/build.mjs.map +1 -1
  372. package/dist/models/capture-attempt.d.mts +7 -1
  373. package/dist/models/capture-attempt.mjs +29 -4
  374. package/dist/models/capture-attempt.mjs.map +1 -1
  375. package/dist/models/capture-job.d.mts +106 -0
  376. package/dist/models/capture-job.mjs +288 -0
  377. package/dist/models/capture-job.mjs.map +1 -0
  378. package/dist/models/capture-log.d.mts +1 -1
  379. package/dist/models/capture-log.mjs +4 -3
  380. package/dist/models/capture-log.mjs.map +1 -1
  381. package/dist/models/capture-shard.d.mts +62 -0
  382. package/dist/models/capture-shard.mjs +190 -0
  383. package/dist/models/capture-shard.mjs.map +1 -0
  384. package/dist/models/changeset.d.mts +48 -0
  385. package/dist/models/changeset.mjs +81 -0
  386. package/dist/models/changeset.mjs.map +1 -0
  387. package/dist/models/columns.mjs +19 -0
  388. package/dist/models/columns.mjs.map +1 -0
  389. package/dist/models/comment.d.mts +4 -1
  390. package/dist/models/comment.mjs +7 -4
  391. package/dist/models/comment.mjs.map +1 -1
  392. package/dist/models/component.d.mts +43 -0
  393. package/dist/models/component.mjs +60 -0
  394. package/dist/models/component.mjs.map +1 -0
  395. package/dist/models/connections.d.mts +41 -0
  396. package/dist/models/connections.mjs +112 -0
  397. package/dist/models/connections.mjs.map +1 -0
  398. package/dist/models/content-ref.d.mts +1 -1
  399. package/dist/models/content-ref.mjs +1 -1
  400. package/dist/models/content-ref.mjs.map +1 -1
  401. package/dist/models/credentials.mjs +12 -2
  402. package/dist/models/credentials.mjs.map +1 -1
  403. package/dist/models/design-credentials.d.mts +41 -0
  404. package/dist/models/design-credentials.mjs +86 -0
  405. package/dist/models/design-credentials.mjs.map +1 -0
  406. package/dist/models/design-health.d.mts +16 -0
  407. package/dist/models/design-health.mjs +25 -0
  408. package/dist/models/design-health.mjs.map +1 -0
  409. package/dist/models/design-nodes.d.mts +83 -0
  410. package/dist/models/design-nodes.mjs +222 -0
  411. package/dist/models/design-nodes.mjs.map +1 -0
  412. package/dist/models/design-renders.mjs +74 -0
  413. package/dist/models/design-renders.mjs.map +1 -0
  414. package/dist/models/design-resolve.d.mts +29 -0
  415. package/dist/models/design-resolve.mjs +41 -0
  416. package/dist/models/design-resolve.mjs.map +1 -0
  417. package/dist/models/design.d.mts +39 -0
  418. package/dist/models/design.mjs +90 -0
  419. package/dist/models/design.mjs.map +1 -0
  420. package/dist/models/inherited-ratio.d.mts +21 -0
  421. package/dist/models/inherited-ratio.mjs +22 -0
  422. package/dist/models/inherited-ratio.mjs.map +1 -0
  423. package/dist/models/insight.d.mts +70 -0
  424. package/dist/models/insight.mjs +162 -0
  425. package/dist/models/insight.mjs.map +1 -0
  426. package/dist/models/label.d.mts +4 -2
  427. package/dist/models/label.mjs +25 -10
  428. package/dist/models/label.mjs.map +1 -1
  429. package/dist/models/member.d.mts +1 -1
  430. package/dist/models/member.mjs +5 -4
  431. package/dist/models/member.mjs.map +1 -1
  432. package/dist/models/notification-channel.d.mts +37 -3
  433. package/dist/models/notification-channel.mjs +88 -9
  434. package/dist/models/notification-channel.mjs.map +1 -1
  435. package/dist/models/notification-subscription.d.mts +1 -2
  436. package/dist/models/notification-subscription.mjs +7 -9
  437. package/dist/models/notification-subscription.mjs.map +1 -1
  438. package/dist/models/project-group-mapping.d.mts +6 -1
  439. package/dist/models/project-group-mapping.mjs +14 -4
  440. package/dist/models/project-group-mapping.mjs.map +1 -1
  441. package/dist/models/project.d.mts +9 -1
  442. package/dist/models/project.mjs +9 -4
  443. package/dist/models/project.mjs.map +1 -1
  444. package/dist/models/public-origin.d.mts +63 -0
  445. package/dist/models/public-origin.mjs +114 -0
  446. package/dist/models/public-origin.mjs.map +1 -0
  447. package/dist/models/review-switch-off.d.mts +14 -0
  448. package/dist/models/review-switch-off.mjs +26 -0
  449. package/dist/models/review-switch-off.mjs.map +1 -0
  450. package/dist/models/site-settings.d.mts +23 -0
  451. package/dist/models/site-settings.mjs +48 -0
  452. package/dist/models/site-settings.mjs.map +1 -0
  453. package/dist/models/snapshot.d.mts +36 -1
  454. package/dist/models/snapshot.mjs +50 -3
  455. package/dist/models/snapshot.mjs.map +1 -1
  456. package/dist/models/token.d.mts +13 -1
  457. package/dist/models/token.mjs +23 -5
  458. package/dist/models/token.mjs.map +1 -1
  459. package/dist/models/usage.d.mts +76 -0
  460. package/dist/models/usage.mjs +149 -0
  461. package/dist/models/usage.mjs.map +1 -0
  462. package/dist/models/user.d.mts +1 -1
  463. package/dist/models/user.mjs +4 -3
  464. package/dist/models/user.mjs.map +1 -1
  465. package/dist/models/webhook.d.mts +15 -1
  466. package/dist/models/webhook.mjs +33 -4
  467. package/dist/models/webhook.mjs.map +1 -1
  468. package/dist/models.d.mts +26 -3
  469. package/dist/models.mjs +27 -5
  470. package/dist/options.d.mts +18 -0
  471. package/dist/options.mjs +90 -0
  472. package/dist/options.mjs.map +1 -0
  473. package/dist/paths.d.mts +19 -4
  474. package/dist/paths.mjs +51 -10
  475. package/dist/paths.mjs.map +1 -1
  476. package/dist/retention/ai.d.mts +18 -0
  477. package/dist/retention/ai.mjs +23 -0
  478. package/dist/retention/ai.mjs.map +1 -0
  479. package/dist/retention/pending.mjs +47 -0
  480. package/dist/retention/pending.mjs.map +1 -0
  481. package/dist/retention/screenshot-sweep.d.mts +51 -0
  482. package/dist/retention/screenshot-sweep.mjs +128 -0
  483. package/dist/retention/screenshot-sweep.mjs.map +1 -0
  484. package/dist/retention.d.mts +16 -2
  485. package/dist/retention.mjs +71 -14
  486. package/dist/retention.mjs.map +1 -1
  487. package/dist/schema/baseline.d.mts +6 -0
  488. package/dist/schema/build-component.d.mts +25 -0
  489. package/dist/schema/build-review.d.mts +17 -0
  490. package/dist/schema/build.d.mts +7 -0
  491. package/dist/schema/capture-job.d.mts +60 -0
  492. package/dist/schema/capture-shard.d.mts +35 -0
  493. package/dist/schema/comment.d.mts +2 -0
  494. package/dist/schema/connection.d.mts +24 -0
  495. package/dist/schema/design.d.mts +58 -0
  496. package/dist/schema/insight.d.mts +54 -0
  497. package/dist/schema/notification.d.mts +4 -0
  498. package/dist/schema/project.d.mts +21 -0
  499. package/dist/schema/site-setting.d.mts +21 -0
  500. package/dist/schema/snapshot.d.mts +12 -0
  501. package/dist/schema/usage.d.mts +30 -0
  502. package/dist/schema.d.mts +11 -3
  503. package/dist/serverless.d.mts +21 -0
  504. package/dist/serverless.mjs +37 -0
  505. package/dist/serverless.mjs.map +1 -0
  506. package/dist/test-helpers/create-project.d.mts +1 -1
  507. package/dist/test-helpers/deferred.d.mts +39 -0
  508. package/dist/test-helpers/deferred.mjs +61 -0
  509. package/dist/test-helpers/deferred.mjs.map +1 -0
  510. package/dist/test-helpers/design-contract.d.mts +21 -0
  511. package/dist/test-helpers/design-contract.mjs +40 -0
  512. package/dist/test-helpers/design-contract.mjs.map +1 -0
  513. package/dist/test-helpers/fake-database.mjs +14 -2
  514. package/dist/test-helpers/fake-database.mjs.map +1 -1
  515. package/dist/test-helpers/fake-design.d.mts +43 -0
  516. package/dist/test-helpers/fake-design.mjs +114 -0
  517. package/dist/test-helpers/fake-design.mjs.map +1 -0
  518. package/dist/test-helpers/fake-notifier.d.mts +21 -0
  519. package/dist/test-helpers/fake-notifier.mjs +44 -0
  520. package/dist/test-helpers/fake-notifier.mjs.map +1 -0
  521. package/dist/test-helpers/fake-tables.mjs +255 -67
  522. package/dist/test-helpers/fake-tables.mjs.map +1 -1
  523. package/dist/test-helpers/manual-clock.d.mts +27 -0
  524. package/dist/test-helpers/manual-clock.mjs +56 -0
  525. package/dist/test-helpers/manual-clock.mjs.map +1 -0
  526. package/dist/test-helpers/notifier-contract.d.mts +7 -1
  527. package/dist/test-helpers/notifier-contract.mjs +32 -3
  528. package/dist/test-helpers/notifier-contract.mjs.map +1 -1
  529. package/dist/test-helpers/queue-contract.d.mts +16 -2
  530. package/dist/test-helpers/queue-contract.mjs +195 -1
  531. package/dist/test-helpers/queue-contract.mjs.map +1 -1
  532. package/dist/test-helpers/queue-multi-instance.d.mts +20 -0
  533. package/dist/test-helpers/queue-multi-instance.mjs +206 -0
  534. package/dist/test-helpers/queue-multi-instance.mjs.map +1 -0
  535. package/dist/test-helpers/runner-contract.mjs +11 -0
  536. package/dist/test-helpers/runner-contract.mjs.map +1 -1
  537. package/dist/test-helpers/sql-chunks.mjs +23 -0
  538. package/dist/test-helpers/sql-chunks.mjs.map +1 -1
  539. package/dist/test-helpers/storage-contract.mjs +1 -1
  540. package/dist/test-helpers.d.mts +9 -4
  541. package/dist/test-helpers.mjs +8 -3
  542. package/dist/types.d.mts +14 -3
  543. package/dist/types.mjs +21 -4
  544. package/dist/types.mjs.map +1 -1
  545. package/dist/urls.d.mts +4 -0
  546. package/dist/urls.mjs +11 -2
  547. package/dist/urls.mjs.map +1 -1
  548. package/dist/utils/adaptive-concurrency.d.mts +66 -0
  549. package/dist/utils/adaptive-concurrency.mjs +149 -0
  550. package/dist/utils/adaptive-concurrency.mjs.map +1 -0
  551. package/dist/utils/connect-retry.d.mts +26 -0
  552. package/dist/utils/connect-retry.mjs +43 -0
  553. package/dist/utils/connect-retry.mjs.map +1 -0
  554. package/dist/utils/environment.d.mts +19 -0
  555. package/dist/utils/environment.mjs +59 -0
  556. package/dist/utils/environment.mjs.map +1 -0
  557. package/dist/utils/http.d.mts +17 -1
  558. package/dist/utils/http.mjs +22 -3
  559. package/dist/utils/http.mjs.map +1 -1
  560. package/dist/utils/map-adaptive.d.mts +15 -0
  561. package/dist/utils/map-adaptive.mjs +72 -0
  562. package/dist/utils/map-adaptive.mjs.map +1 -0
  563. package/dist/utils/map-limit.d.mts +10 -0
  564. package/dist/utils/map-limit.mjs +32 -0
  565. package/dist/utils/map-limit.mjs.map +1 -0
  566. package/dist/utils/page-limiter.d.mts +37 -0
  567. package/dist/utils/page-limiter.mjs +56 -0
  568. package/dist/utils/page-limiter.mjs.map +1 -0
  569. package/dist/utils/public-origin.d.mts +20 -0
  570. package/dist/utils/public-origin.mjs +75 -0
  571. package/dist/utils/public-origin.mjs.map +1 -0
  572. package/dist/utils/redact.mjs +3 -0
  573. package/dist/utils/redact.mjs.map +1 -1
  574. package/dist/utils/remote-environment.d.mts +39 -0
  575. package/dist/utils/remote-environment.mjs +114 -0
  576. package/dist/utils/remote-environment.mjs.map +1 -0
  577. package/dist/utils/resources.d.mts +64 -0
  578. package/dist/utils/resources.mjs +122 -0
  579. package/dist/utils/resources.mjs.map +1 -0
  580. package/dist/utils/screenshot-store.d.mts +15 -0
  581. package/dist/utils/screenshot-store.mjs +41 -0
  582. package/dist/utils/screenshot-store.mjs.map +1 -0
  583. package/dist/utils/session-gate.d.mts +22 -0
  584. package/dist/utils/session-gate.mjs +53 -0
  585. package/dist/utils/session-gate.mjs.map +1 -0
  586. package/dist/utils/storybook-token.d.mts +81 -0
  587. package/dist/utils/storybook-token.mjs +134 -0
  588. package/dist/utils/storybook-token.mjs.map +1 -0
  589. package/dist/utils/storybook-url.d.mts +15 -0
  590. package/dist/utils/storybook-url.mjs +21 -0
  591. package/dist/utils/storybook-url.mjs.map +1 -0
  592. package/dist/utils/timing.mjs +1 -1
  593. package/dist/utils/timing.mjs.map +1 -1
  594. package/dist/utils/zip-read.mjs +129 -0
  595. package/dist/utils/zip-read.mjs.map +1 -0
  596. package/dist/utils/zip-write.d.mts +18 -0
  597. package/dist/utils/zip-write.mjs +188 -0
  598. package/dist/utils/zip-write.mjs.map +1 -0
  599. package/dist/utils.d.mts +17 -3
  600. package/dist/utils.mjs +18 -4
  601. package/package.json +32 -20
  602. package/dist/adapter/capture-runner.mjs.map +0 -1
  603. package/dist/adapter/git-host/comments.d.mts +0 -41
  604. package/dist/adapter/git-host/comments.mjs +0 -54
  605. package/dist/adapter/git-host/comments.mjs.map +0 -1
  606. package/dist/adapter/git-host/helpers.d.mts +0 -13
  607. package/dist/adapter/git-host/helpers.mjs +0 -30
  608. package/dist/adapter/git-host/helpers.mjs.map +0 -1
  609. package/dist/adapter/git-host.d.mts +0 -42
  610. package/dist/capture/queue.d.mts +0 -38
  611. package/dist/capture/queue.mjs +0 -98
  612. package/dist/capture/queue.mjs.map +0 -1
  613. package/dist/capture/recovery.d.mts +0 -21
  614. package/dist/capture/recovery.mjs +0 -83
  615. package/dist/capture/recovery.mjs.map +0 -1
  616. package/dist/capture/status-fanout.mjs +0 -72
  617. package/dist/capture/status-fanout.mjs.map +0 -1
  618. package/dist/diff/engine.d.mts +0 -16
  619. package/dist/diff/engine.mjs +0 -87
  620. package/dist/diff/engine.mjs.map +0 -1
  621. package/dist/diff/options.d.mts +0 -42
  622. package/dist/diff/options.mjs +0 -15
  623. package/dist/diff/options.mjs.map +0 -1
  624. package/dist/models/status-config.d.mts +0 -38
  625. package/dist/models/status-config.mjs +0 -63
  626. package/dist/models/status-config.mjs.map +0 -1
  627. package/dist/schema/status-config.d.mts +0 -16
  628. package/dist/test-helpers/git-contract.d.mts +0 -11
  629. package/dist/test-helpers/git-contract.mjs +0 -34
  630. package/dist/test-helpers/git-contract.mjs.map +0 -1
  631. /package/dist/adapter/{git-host.mjs → oauth.mjs} +0 -0
@@ -0,0 +1,30 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { removeShardScratch } from "../scratch.mjs";
5
+ import { extractStorybookToScratch, persistStorybookStatics } from "../statics.mjs";
6
+ //#region src/capture/publish/run.ts
7
+ /** Scratch id publishing uses, so it never shares a directory with the build's capture. */
8
+ const PUBLISH_SCRATCH_ID = "publish";
9
+ /**
10
+ * Extract `storybook.zip` of a build and persist its statics. The manifest is written last, so a
11
+ * build serves only once complete and a repeat run rewrites the same keys.
12
+ *
13
+ * @throws {ZipLimitError} When the zip breaks a cap or tries to escape the target.
14
+ */
15
+ async function publishStoredZip(options, target) {
16
+ const { storage, scratchDir, logger } = options;
17
+ const { projectId, buildId } = target;
18
+ try {
19
+ const dir = await extractStorybookToScratch(storage, scratchDir, projectId, buildId, PUBLISH_SCRATCH_ID, options.limits);
20
+ options.touch?.();
21
+ await persistStorybookStatics(storage, dir, projectId, buildId, options.db);
22
+ options.touch?.();
23
+ } finally {
24
+ await removeShardScratch(scratchDir, projectId, buildId, PUBLISH_SCRATCH_ID, logger);
25
+ }
26
+ }
27
+ //#endregion
28
+ export { PUBLISH_SCRATCH_ID, publishStoredZip };
29
+
30
+ //# sourceMappingURL=run.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.mjs","names":[],"sources":["../../../src/capture/publish/run.ts"],"sourcesContent":["/** Publish a build's stored zip: extract with the caps, persist statics and manifest, clean the scratch. */\nimport type { DatabaseAdapter } from \"../../adapters/database.ts\";\nimport type { StorageAdapter } from \"../../adapters/storage.ts\";\nimport type { Logger } from \"../../logger.ts\";\nimport { removeShardScratch } from \"../scratch.ts\";\nimport { extractStorybookToScratch, persistStorybookStatics } from \"../statics.ts\";\nimport type { ExtractionLimits } from \"./limits.ts\";\n\n/** Scratch id publishing uses, so it never shares a directory with the build's capture. */\nexport const PUBLISH_SCRATCH_ID = \"publish\";\n\n/** What {@link publishStoredZip} works with. */\nexport interface PublishZipOptions {\n storage: StorageAdapter;\n scratchDir: string;\n limits: ExtractionLimits;\n /** Records the persisted content in `content_refs`, so retention knows who still uses it. */\n db?: DatabaseAdapter | undefined;\n logger: Logger;\n /** Called between stages (renews a job's lease). */\n touch?: () => void;\n}\n\n/**\n * Extract `storybook.zip` of a build and persist its statics. The manifest is written last, so a\n * build serves only once complete and a repeat run rewrites the same keys.\n *\n * @throws {ZipLimitError} When the zip breaks a cap or tries to escape the target.\n */\nexport async function publishStoredZip(\n options: PublishZipOptions,\n target: { projectId: string; buildId: string },\n): Promise<void> {\n const { storage, scratchDir, logger } = options;\n const { projectId, buildId } = target;\n try {\n const dir = await extractStorybookToScratch(\n storage,\n scratchDir,\n projectId,\n buildId,\n PUBLISH_SCRATCH_ID,\n options.limits,\n );\n options.touch?.();\n await persistStorybookStatics(storage, dir, projectId, buildId, options.db);\n options.touch?.();\n } finally {\n await removeShardScratch(scratchDir, projectId, buildId, PUBLISH_SCRATCH_ID, logger);\n }\n}\n"],"mappings":";;;;;;;AASA,MAAa,qBAAqB;;;;;;;AAoBlC,eAAsB,iBACpB,SACA,QACe;CACf,MAAM,EAAE,SAAS,YAAY,WAAW;CACxC,MAAM,EAAE,WAAW,YAAY;CAC/B,IAAI;EACF,MAAM,MAAM,MAAM,0BAChB,SACA,YACA,WACA,SACA,oBACA,QAAQ,MACV;EACA,QAAQ,QAAQ;EAChB,MAAM,wBAAwB,SAAS,KAAK,WAAW,SAAS,QAAQ,EAAE;EAC1E,QAAQ,QAAQ;CAClB,UAAU;EACR,MAAM,mBAAmB,YAAY,WAAW,SAAS,oBAAoB,MAAM;CACrF;AACF"}
@@ -1,8 +1,8 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { DatabaseAdapter } from "../adapter/database.mjs";
4
- import { StorageAdapter } from "../adapter/storage.mjs";
5
4
  import { Logger } from "../logger.mjs";
5
+ import { StorageAdapter } from "../adapter/storage.mjs";
6
6
  import { Build } from "../schema/build.mjs";
7
7
  import { Project } from "../schema/project.mjs";
8
8
  //#region src/capture/rediff.d.ts
@@ -5,6 +5,7 @@ import { diffPath } from "../paths.mjs";
5
5
  import { BaselineModel } from "../models/baseline.mjs";
6
6
  import { NO_BASELINE } from "../models/baseline-status.mjs";
7
7
  import { SnapshotModel } from "../models/snapshot.mjs";
8
+ import { mapLimit } from "../utils/map-limit.mjs";
8
9
  import { diffStoredScreenshots, writeDiffOverlay } from "./diff-record.mjs";
9
10
  import { diffStatus } from "./pipeline.mjs";
10
11
  //#region src/capture/rediff.ts
@@ -27,7 +28,7 @@ const REDIFFABLE_SNAPSHOT_STATUSES = /* @__PURE__ */ new Set([
27
28
  */
28
29
  async function rediffBuild(deps, project, build) {
29
30
  const rows = (await new SnapshotModel(deps.db).listByBuild(build.id)).filter((row) => !row.inherited && REDIFFABLE_SNAPSHOT_STATUSES.has(row.status));
30
- const outcomes = await Promise.all(rows.map(async (row) => await rediffSnapshot(deps, project, build, row)));
31
+ const outcomes = await mapLimit(rows, 4, async (row) => await rediffSnapshot(deps, project, build, row));
31
32
  const result = {
32
33
  rediffed: outcomes.filter((outcome) => outcome !== "missing").length,
33
34
  statusChanged: outcomes.filter((outcome) => outcome === "changed").length,
@@ -41,7 +42,7 @@ async function rediffBuild(deps, project, build) {
41
42
  }
42
43
  async function rediffSnapshot(deps, project, build, row) {
43
44
  if (!await deps.storage.exists(row.screenshotPath)) return "missing";
44
- const baseline = await new BaselineModel(deps.db).resolve(project.id, row.storyId, row.viewportName, build.gitBranch, project.gitDefaultBranch);
45
+ const baseline = await new BaselineModel(deps.db).resolve(project.id, row.storyId, row.viewportName, build.gitBranch, project.gitDefaultBranch, row.browser);
45
46
  const patch = baseline ? await diffPatch(deps, project, build, row, baseline) : noBaselinePatch();
46
47
  await new SnapshotModel(deps.db).update(row.id, patch);
47
48
  return patch.status === row.status ? "same" : "changed";
@@ -59,7 +60,7 @@ function noBaselinePatch() {
59
60
  }
60
61
  async function diffPatch(deps, project, build, row, baseline) {
61
62
  const result = await diffStoredScreenshots(deps.storage, project, baseline.screenshotPath, row.screenshotPath);
62
- const target = diffPath(project.id, build.id, row.storyId, row.viewportName);
63
+ const target = diffPath(project.id, build.id, row.storyId, row.viewportName, row.browser);
63
64
  const overlay = await writeDiffOverlay(deps.storage, target, result, result.passed);
64
65
  return {
65
66
  status: diffStatus(result.passed, false),
@@ -1 +1 @@
1
- {"version":3,"file":"rediff.mjs","names":[],"sources":["../../src/capture/rediff.ts"],"sourcesContent":["/** Recompute a build's diffs against the baselines that apply now, without re-rendering. */\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { NO_BASELINE } from \"../models/baseline-status.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport type { Baseline } from \"../schema/baseline.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport type { Snapshot } from \"../schema/snapshot.ts\";\nimport { diffPath } from \"../utils/paths.ts\";\nimport { diffStoredScreenshots, writeDiffOverlay } from \"./diff-record.ts\";\nimport { diffStatus } from \"./pipeline.ts\";\n\n/** Dependencies for {@link rediffBuild}. */\nexport interface RediffDeps {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n logger?: Logger;\n}\n\n/** Outcome of a re-diff run. */\nexport interface RediffResult {\n /** Snapshots diffed against their current baseline. */\n rediffed: number;\n /** Snapshots whose status changed (for example `changed` to `unchanged`). */\n statusChanged: number;\n /** Snapshot ids skipped because their stored screenshot is gone. */\n missingScreenshots: string[];\n}\n\n/** Build statuses a re-diff may run in; earlier or failed builds have no complete snapshot set. */\nexport const REDIFFABLE_BUILD_STATUSES: ReadonlySet<string> = new Set([\n \"reviewing\",\n \"approved\",\n \"rejected\",\n]);\n\nconst REDIFFABLE_SNAPSHOT_STATUSES: ReadonlySet<string> = new Set([\"new\", \"changed\", \"unchanged\"]);\n\n/**\n * Re-diff the build's undecided snapshots against the baselines that apply today.\n *\n * Approved and rejected snapshots are decisions and stay untouched; inherited snapshots were never\n * rendered. The caller refreshes the build rollup afterwards.\n */\nexport async function rediffBuild(\n deps: RediffDeps,\n project: Project,\n build: Build,\n): Promise<RediffResult> {\n const snapshots = new SnapshotModel(deps.db);\n const rows = (await snapshots.listByBuild(build.id)).filter(\n (row) => !row.inherited && REDIFFABLE_SNAPSHOT_STATUSES.has(row.status),\n );\n const outcomes = await Promise.all(\n rows.map(async (row) => await rediffSnapshot(deps, project, build, row)),\n );\n const result: RediffResult = {\n rediffed: outcomes.filter((outcome) => outcome !== \"missing\").length,\n statusChanged: outcomes.filter((outcome) => outcome === \"changed\").length,\n missingScreenshots: rows\n .filter((_row, index) => outcomes[index] === \"missing\")\n .map((row) => row.id),\n };\n deps.logger?.info({ buildId: build.id, ...result }, \"build re-diffed against current baselines\");\n return result;\n}\n\ntype Outcome = \"same\" | \"changed\" | \"missing\";\n\nasync function rediffSnapshot(\n deps: RediffDeps,\n project: Project,\n build: Build,\n row: Snapshot,\n): Promise<Outcome> {\n if (!(await deps.storage.exists(row.screenshotPath))) {\n return \"missing\";\n }\n const baseline = await new BaselineModel(deps.db).resolve(\n project.id,\n row.storyId,\n row.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n );\n const patch = baseline ? await diffPatch(deps, project, build, row, baseline) : noBaselinePatch();\n await new SnapshotModel(deps.db).update(row.id, patch);\n return patch.status === row.status ? \"same\" : \"changed\";\n}\n\nfunction noBaselinePatch(): Partial<Snapshot> {\n return {\n status: \"new\",\n diffPath: null,\n diffPixels: null,\n diffRatio: null,\n diffPassed: null,\n baselineId: null,\n baselineVersion: NO_BASELINE,\n };\n}\n\nasync function diffPatch(\n deps: RediffDeps,\n project: Project,\n build: Build,\n row: Snapshot,\n baseline: Baseline,\n): Promise<Partial<Snapshot>> {\n const result = await diffStoredScreenshots(\n deps.storage,\n project,\n baseline.screenshotPath,\n row.screenshotPath,\n );\n const target = diffPath(project.id, build.id, row.storyId, row.viewportName);\n const overlay = await writeDiffOverlay(deps.storage, target, result, result.passed);\n return {\n status: diffStatus(result.passed, false),\n diffPath: overlay,\n diffPixels: result.diffPixels,\n diffRatio: result.diffRatio,\n diffPassed: result.passed,\n baselineId: baseline.id,\n baselineVersion: baseline.updatedAt,\n };\n}\n"],"mappings":";;;;;;;;;;;AAiCA,MAAa,4CAAiD,IAAI,IAAI;CACpE;CACA;CACA;AACF,CAAC;AAED,MAAM,+CAAoD,IAAI,IAAI;CAAC;CAAO;CAAW;AAAW,CAAC;;;;;;;AAQjG,eAAsB,YACpB,MACA,SACA,OACuB;CAEvB,MAAM,QAAQ,MAAM,IADE,cAAc,KAAK,EACb,CAAC,CAAC,YAAY,MAAM,EAAE,EAAA,CAAG,QAClD,QAAQ,CAAC,IAAI,aAAa,6BAA6B,IAAI,IAAI,MAAM,CACxE;CACA,MAAM,WAAW,MAAM,QAAQ,IAC7B,KAAK,IAAI,OAAO,QAAQ,MAAM,eAAe,MAAM,SAAS,OAAO,GAAG,CAAC,CACzE;CACA,MAAM,SAAuB;EAC3B,UAAU,SAAS,QAAQ,YAAY,YAAY,SAAS,CAAC,CAAC;EAC9D,eAAe,SAAS,QAAQ,YAAY,YAAY,SAAS,CAAC,CAAC;EACnE,oBAAoB,KACjB,QAAQ,MAAM,UAAU,SAAS,WAAW,SAAS,CAAC,CACtD,KAAK,QAAQ,IAAI,EAAE;CACxB;CACA,KAAK,QAAQ,KAAK;EAAE,SAAS,MAAM;EAAI,GAAG;CAAO,GAAG,2CAA2C;CAC/F,OAAO;AACT;AAIA,eAAe,eACb,MACA,SACA,OACA,KACkB;CAClB,IAAI,CAAE,MAAM,KAAK,QAAQ,OAAO,IAAI,cAAc,GAChD,OAAO;CAET,MAAM,WAAW,MAAM,IAAI,cAAc,KAAK,EAAE,CAAC,CAAC,QAChD,QAAQ,IACR,IAAI,SACJ,IAAI,cACJ,MAAM,WACN,QAAQ,gBACV;CACA,MAAM,QAAQ,WAAW,MAAM,UAAU,MAAM,SAAS,OAAO,KAAK,QAAQ,IAAI,gBAAgB;CAChG,MAAM,IAAI,cAAc,KAAK,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI,KAAK;CACrD,OAAO,MAAM,WAAW,IAAI,SAAS,SAAS;AAChD;AAEA,SAAS,kBAAqC;CAC5C,OAAO;EACL,QAAQ;EACR,UAAU;EACV,YAAY;EACZ,WAAW;EACX,YAAY;EACZ,YAAY;EACZ,iBAAiB;CACnB;AACF;AAEA,eAAe,UACb,MACA,SACA,OACA,KACA,UAC4B;CAC5B,MAAM,SAAS,MAAM,sBACnB,KAAK,SACL,SACA,SAAS,gBACT,IAAI,cACN;CACA,MAAM,SAAS,SAAS,QAAQ,IAAI,MAAM,IAAI,IAAI,SAAS,IAAI,YAAY;CAC3E,MAAM,UAAU,MAAM,iBAAiB,KAAK,SAAS,QAAQ,QAAQ,OAAO,MAAM;CAClF,OAAO;EACL,QAAQ,WAAW,OAAO,QAAQ,KAAK;EACvC,UAAU;EACV,YAAY,OAAO;EACnB,WAAW,OAAO;EAClB,YAAY,OAAO;EACnB,YAAY,SAAS;EACrB,iBAAiB,SAAS;CAC5B;AACF"}
1
+ {"version":3,"file":"rediff.mjs","names":[],"sources":["../../src/capture/rediff.ts"],"sourcesContent":["/** Recompute a build's diffs against the baselines that apply now, without re-rendering. */\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { NO_BASELINE } from \"../models/baseline-status.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport type { Baseline } from \"../schema/baseline.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport type { Snapshot } from \"../schema/snapshot.ts\";\nimport { mapLimit } from \"../utils/map-limit.ts\";\nimport { diffPath } from \"../utils/paths.ts\";\nimport { diffStoredScreenshots, writeDiffOverlay } from \"./diff-record.ts\";\nimport { diffStatus, PERSIST_CONCURRENCY } from \"./pipeline.ts\";\n\n/** Dependencies for {@link rediffBuild}. */\nexport interface RediffDeps {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n logger?: Logger;\n}\n\n/** Outcome of a re-diff run. */\nexport interface RediffResult {\n /** Snapshots diffed against their current baseline. */\n rediffed: number;\n /** Snapshots whose status changed (for example `changed` to `unchanged`). */\n statusChanged: number;\n /** Snapshot ids skipped because their stored screenshot is gone. */\n missingScreenshots: string[];\n}\n\n/** Build statuses a re-diff may run in; earlier or failed builds have no complete snapshot set. */\nexport const REDIFFABLE_BUILD_STATUSES: ReadonlySet<string> = new Set([\n \"reviewing\",\n \"approved\",\n \"rejected\",\n]);\n\nconst REDIFFABLE_SNAPSHOT_STATUSES: ReadonlySet<string> = new Set([\"new\", \"changed\", \"unchanged\"]);\n\n/**\n * Re-diff the build's undecided snapshots against the baselines that apply today.\n *\n * Approved and rejected snapshots are decisions and stay untouched; inherited snapshots were never\n * rendered. The caller refreshes the build rollup afterwards.\n */\nexport async function rediffBuild(\n deps: RediffDeps,\n project: Project,\n build: Build,\n): Promise<RediffResult> {\n const snapshots = new SnapshotModel(deps.db);\n const rows = (await snapshots.listByBuild(build.id)).filter(\n (row) => !row.inherited && REDIFFABLE_SNAPSHOT_STATUSES.has(row.status),\n );\n const outcomes = await mapLimit(\n rows,\n PERSIST_CONCURRENCY,\n async (row) => await rediffSnapshot(deps, project, build, row),\n );\n const result: RediffResult = {\n rediffed: outcomes.filter((outcome) => outcome !== \"missing\").length,\n statusChanged: outcomes.filter((outcome) => outcome === \"changed\").length,\n missingScreenshots: rows\n .filter((_row, index) => outcomes[index] === \"missing\")\n .map((row) => row.id),\n };\n deps.logger?.info({ buildId: build.id, ...result }, \"build re-diffed against current baselines\");\n return result;\n}\n\ntype Outcome = \"same\" | \"changed\" | \"missing\";\n\nasync function rediffSnapshot(\n deps: RediffDeps,\n project: Project,\n build: Build,\n row: Snapshot,\n): Promise<Outcome> {\n if (!(await deps.storage.exists(row.screenshotPath))) {\n return \"missing\";\n }\n const baseline = await new BaselineModel(deps.db).resolve(\n project.id,\n row.storyId,\n row.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n row.browser,\n );\n const patch = baseline ? await diffPatch(deps, project, build, row, baseline) : noBaselinePatch();\n await new SnapshotModel(deps.db).update(row.id, patch);\n return patch.status === row.status ? \"same\" : \"changed\";\n}\n\nfunction noBaselinePatch(): Partial<Snapshot> {\n return {\n status: \"new\",\n diffPath: null,\n diffPixels: null,\n diffRatio: null,\n diffPassed: null,\n baselineId: null,\n baselineVersion: NO_BASELINE,\n };\n}\n\nasync function diffPatch(\n deps: RediffDeps,\n project: Project,\n build: Build,\n row: Snapshot,\n baseline: Baseline,\n): Promise<Partial<Snapshot>> {\n const result = await diffStoredScreenshots(\n deps.storage,\n project,\n baseline.screenshotPath,\n row.screenshotPath,\n );\n const target = diffPath(project.id, build.id, row.storyId, row.viewportName, row.browser);\n const overlay = await writeDiffOverlay(deps.storage, target, result, result.passed);\n return {\n status: diffStatus(result.passed, false),\n diffPath: overlay,\n diffPixels: result.diffPixels,\n diffRatio: result.diffRatio,\n diffPassed: result.passed,\n baselineId: baseline.id,\n baselineVersion: baseline.updatedAt,\n };\n}\n"],"mappings":";;;;;;;;;;;;AAkCA,MAAa,4CAAiD,IAAI,IAAI;CACpE;CACA;CACA;AACF,CAAC;AAED,MAAM,+CAAoD,IAAI,IAAI;CAAC;CAAO;CAAW;AAAW,CAAC;;;;;;;AAQjG,eAAsB,YACpB,MACA,SACA,OACuB;CAEvB,MAAM,QAAQ,MAAM,IADE,cAAc,KAAK,EACb,CAAC,CAAC,YAAY,MAAM,EAAE,EAAA,CAAG,QAClD,QAAQ,CAAC,IAAI,aAAa,6BAA6B,IAAI,IAAI,MAAM,CACxE;CACA,MAAM,WAAW,MAAM,SACrB,MAAA,GAEA,OAAO,QAAQ,MAAM,eAAe,MAAM,SAAS,OAAO,GAAG,CAC/D;CACA,MAAM,SAAuB;EAC3B,UAAU,SAAS,QAAQ,YAAY,YAAY,SAAS,CAAC,CAAC;EAC9D,eAAe,SAAS,QAAQ,YAAY,YAAY,SAAS,CAAC,CAAC;EACnE,oBAAoB,KACjB,QAAQ,MAAM,UAAU,SAAS,WAAW,SAAS,CAAC,CACtD,KAAK,QAAQ,IAAI,EAAE;CACxB;CACA,KAAK,QAAQ,KAAK;EAAE,SAAS,MAAM;EAAI,GAAG;CAAO,GAAG,2CAA2C;CAC/F,OAAO;AACT;AAIA,eAAe,eACb,MACA,SACA,OACA,KACkB;CAClB,IAAI,CAAE,MAAM,KAAK,QAAQ,OAAO,IAAI,cAAc,GAChD,OAAO;CAET,MAAM,WAAW,MAAM,IAAI,cAAc,KAAK,EAAE,CAAC,CAAC,QAChD,QAAQ,IACR,IAAI,SACJ,IAAI,cACJ,MAAM,WACN,QAAQ,kBACR,IAAI,OACN;CACA,MAAM,QAAQ,WAAW,MAAM,UAAU,MAAM,SAAS,OAAO,KAAK,QAAQ,IAAI,gBAAgB;CAChG,MAAM,IAAI,cAAc,KAAK,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI,KAAK;CACrD,OAAO,MAAM,WAAW,IAAI,SAAS,SAAS;AAChD;AAEA,SAAS,kBAAqC;CAC5C,OAAO;EACL,QAAQ;EACR,UAAU;EACV,YAAY;EACZ,WAAW;EACX,YAAY;EACZ,YAAY;EACZ,iBAAiB;CACnB;AACF;AAEA,eAAe,UACb,MACA,SACA,OACA,KACA,UAC4B;CAC5B,MAAM,SAAS,MAAM,sBACnB,KAAK,SACL,SACA,SAAS,gBACT,IAAI,cACN;CACA,MAAM,SAAS,SAAS,QAAQ,IAAI,MAAM,IAAI,IAAI,SAAS,IAAI,cAAc,IAAI,OAAO;CACxF,MAAM,UAAU,MAAM,iBAAiB,KAAK,SAAS,QAAQ,QAAQ,OAAO,MAAM;CAClF,OAAO;EACL,QAAQ,WAAW,OAAO,QAAQ,KAAK;EACvC,UAAU;EACV,YAAY,OAAO;EACnB,WAAW,OAAO;EAClB,YAAY,OAAO;EACnB,YAAY,SAAS;EACrB,iBAAiB,SAAS;CAC5B;AACF"}
@@ -0,0 +1,53 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import "../utils/storybook-token.mjs";
5
+ import { redactSecrets } from "../utils/redact.mjs";
6
+ //#region src/capture/render.ts
7
+ /** Default budget for one render call before the orchestrator cancels it. */
8
+ const RENDER_TIMEOUT_MS = 9e5;
9
+ /**
10
+ * Budget for one render call: an explicit setting wins; otherwise the default
11
+ * floor, raised to a per-capture allowance for large matrices. Bounded page
12
+ * concurrency on small hosts makes big builds legitimately take longer than 15 minutes.
13
+ */
14
+ function renderBudgetMs(captureCount, explicitMs) {
15
+ return explicitMs ?? Math.max(9e5, captureCount * 15e3);
16
+ }
17
+ /** Failure error text cap (persisted rows and logs stay bounded). */
18
+ const FAILURE_ERROR_MAX = 500;
19
+ /** Cap failure error text so one chatty runner cannot flood rows and logs. */
20
+ function capFailures(result) {
21
+ return {
22
+ ...result,
23
+ captures: result.captures,
24
+ failures: result.failures.map((failure) => ({
25
+ ...failure,
26
+ error: redactSecrets(failure.error).slice(0, FAILURE_ERROR_MAX)
27
+ }))
28
+ };
29
+ }
30
+ /** Render with a timeout; cancels the runner when the budget expires. */
31
+ async function renderWithTimeout(runner, request, timeoutMs) {
32
+ let timedOut = false;
33
+ let timer;
34
+ const timeout = new Promise((_, reject) => {
35
+ timer = setTimeout(() => {
36
+ timedOut = true;
37
+ reject(/* @__PURE__ */ new Error(`Render timed out after ${timeoutMs}ms`));
38
+ }, timeoutMs);
39
+ });
40
+ timer?.unref?.();
41
+ try {
42
+ return capFailures(await Promise.race([runner.render(request), timeout]));
43
+ } catch (error) {
44
+ if (timedOut) await runner.cancel(request.buildId, request.runId).catch(() => {});
45
+ throw error;
46
+ } finally {
47
+ clearTimeout(timer);
48
+ }
49
+ }
50
+ //#endregion
51
+ export { RENDER_TIMEOUT_MS, renderBudgetMs, renderWithTimeout };
52
+
53
+ //# sourceMappingURL=render.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.mjs","names":[],"sources":["../../src/capture/render.ts"],"sourcesContent":["/** Render one engine's pass with a budget; shared by the orchestrator's browser loop. */\nimport type { Logger } from \"pino\";\nimport type {\n CaptureTarget,\n CaptureRunner,\n RenderResult,\n RenderedSnapshot,\n} from \"../adapters/capture-runner.ts\";\nimport { redactSecrets } from \"../utils/redact.ts\";\nimport { PER_CAPTURE_BUDGET_MS } from \"../utils/storybook-token.ts\";\n\n/** Default budget for one render call before the orchestrator cancels it. */\nexport const RENDER_TIMEOUT_MS = 15 * 60_000;\n\n/**\n * Budget for one render call: an explicit setting wins; otherwise the default\n * floor, raised to a per-capture allowance for large matrices. Bounded page\n * concurrency on small hosts makes big builds legitimately take longer than 15 minutes.\n */\nexport function renderBudgetMs(captureCount: number, explicitMs?: number): number {\n return explicitMs ?? Math.max(RENDER_TIMEOUT_MS, captureCount * PER_CAPTURE_BUDGET_MS);\n}\n\n/** Failure error text cap (persisted rows and logs stay bounded). */\nconst FAILURE_ERROR_MAX = 500;\n\n/** Render input assembled by the orchestrator for one build. */\nexport interface RenderRequest {\n buildId: string;\n /** This render's cancel handle within the build (see `CaptureRunner.render`). */\n runId?: string | undefined;\n storybookDir: string;\n stories: Parameters<CaptureRunner[\"render\"]>[0][\"stories\"];\n viewports: Parameters<CaptureRunner[\"render\"]>[0][\"viewports\"];\n logger: Logger | undefined;\n executePlay: boolean;\n playTimeoutMs: number;\n runA11y: boolean;\n target: CaptureTarget;\n /** The signed URL a `reach: \"url\"` target opens (never logged); absent for every other target. */\n storybookUrl?: string | undefined;\n onCapture?: (capture: RenderedSnapshot) => Promise<void>;\n}\n\n/** Cap failure error text so one chatty runner cannot flood rows and logs. */\nfunction capFailures(result: RenderResult): RenderResult {\n return {\n ...result,\n captures: result.captures,\n failures: result.failures.map((failure) => ({\n ...failure,\n // A runner's error can quote the URL it failed to open, which carries a signed token.\n error: redactSecrets(failure.error).slice(0, FAILURE_ERROR_MAX),\n })),\n };\n}\n\n/** Render with a timeout; cancels the runner when the budget expires. */\nexport async function renderWithTimeout(\n runner: CaptureRunner,\n request: RenderRequest,\n timeoutMs: number,\n): Promise<RenderResult> {\n let timedOut = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n timedOut = true;\n reject(new Error(`Render timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n });\n timer?.unref?.();\n try {\n return capFailures(await Promise.race([runner.render(request), timeout]));\n } catch (error) {\n if (timedOut) {\n // Only this render: a sibling shard of the same build may be rendering in this process.\n await runner.cancel(request.buildId, request.runId).catch(() => {});\n }\n throw error;\n } finally {\n clearTimeout(timer);\n }\n}\n"],"mappings":";;;;;;;AAYA,MAAa,oBAAoB;;;;;;AAOjC,SAAgB,eAAe,cAAsB,YAA6B;CAChF,OAAO,cAAc,KAAK,IAAA,KAAuB,eAAA,IAAoC;AACvF;;AAGA,MAAM,oBAAoB;;AAqB1B,SAAS,YAAY,QAAoC;CACvD,OAAO;EACL,GAAG;EACH,UAAU,OAAO;EACjB,UAAU,OAAO,SAAS,KAAK,aAAa;GAC1C,GAAG;GAEH,OAAO,cAAc,QAAQ,KAAK,CAAC,CAAC,MAAM,GAAG,iBAAiB;EAChE,EAAE;CACJ;AACF;;AAGA,eAAsB,kBACpB,QACA,SACA,WACuB;CACvB,IAAI,WAAW;CACf,IAAI;CACJ,MAAM,UAAU,IAAI,SAAgB,GAAG,WAAW;EAChD,QAAQ,iBAAiB;GACvB,WAAW;GACX,uBAAO,IAAI,MAAM,0BAA0B,UAAU,GAAG,CAAC;EAC3D,GAAG,SAAS;CACd,CAAC;CACD,OAAO,QAAQ;CACf,IAAI;EACF,OAAO,YAAY,MAAM,QAAQ,KAAK,CAAC,OAAO,OAAO,OAAO,GAAG,OAAO,CAAC,CAAC;CAC1E,SAAS,OAAO;EACd,IAAI,UAEF,MAAM,OAAO,OAAO,QAAQ,SAAS,QAAQ,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC;EAEpE,MAAM;CACR,UAAU;EACR,aAAa,KAAK;CACpB;AACF"}
@@ -0,0 +1,44 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { Logger } from "../logger.mjs";
4
+ //#region src/capture/scratch.d.ts
5
+ /**
6
+ * Subtree of `scratchDir` that holds extracted Storybooks.
7
+ *
8
+ * `scratchDir` is often also the local storage root (dev server, generated servers), where
9
+ * `<project>/builds/<build>/...` holds screenshots and published statics. Keeping scratch in its
10
+ * own subtree means removing it can never touch a storage key.
11
+ */
12
+ declare const SCRATCH_DIRNAME = ".storyshelf-scratch";
13
+ /** Scratch dirs untouched for this long are assumed orphaned by a crash (no capture runs this long). */
14
+ declare const SCRATCH_MAX_AGE_MS: number;
15
+ /** Scratch directory owned by one build: `<scratchDir>/.storyshelf-scratch/<projectId>/<buildId>`. */
16
+ declare function buildScratchDir(scratchDir: string, projectId: string, buildId: string): string;
17
+ /**
18
+ * Remove the scratch of the build's own job (the plan, or the whole build when it is not sharded)
19
+ * and the build directory too if nothing else is left in it.
20
+ *
21
+ * Shards live in their own `shards/` directory beside `job/`, so this can never reach one: the plan
22
+ * hands its shards to the queue before it cleans up, and a worker may already be extracting.
23
+ */
24
+ declare function removeJobScratch(scratchDir: string, projectId: string, buildId: string, logger?: Logger): Promise<void>;
25
+ /** Remove a build's scratch directory. Best effort: a failure is logged, never thrown. */
26
+ declare function removeBuildScratch(scratchDir: string, projectId: string, buildId: string, logger?: Logger): Promise<void>;
27
+ /** Options for {@link sweepOrphanedScratch}. */
28
+ interface ScratchSweepOptions {
29
+ /** Remove build scratch dirs not modified for this long (default {@link SCRATCH_MAX_AGE_MS}). */
30
+ maxAgeMs?: number;
31
+ /** Clock override for tests. */
32
+ now?: number;
33
+ logger?: Logger;
34
+ }
35
+ /**
36
+ * Boot-time sweep for scratch dirs a crash left behind. Age-gated rather than wholesale so a
37
+ * second process sharing the same `scratchDir` (server plus worker) keeps its live captures.
38
+ *
39
+ * @returns How many build scratch directories were removed.
40
+ */
41
+ declare function sweepOrphanedScratch(scratchDir: string, options?: ScratchSweepOptions): Promise<number>;
42
+ //#endregion
43
+ export { SCRATCH_DIRNAME, SCRATCH_MAX_AGE_MS, ScratchSweepOptions, buildScratchDir, removeBuildScratch, removeJobScratch, sweepOrphanedScratch };
44
+ //# sourceMappingURL=scratch.d.mts.map
@@ -0,0 +1,138 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { join } from "node:path";
5
+ import { readdir, rm, rmdir, stat } from "node:fs/promises";
6
+ //#region src/capture/scratch.ts
7
+ /** Per-build scratch directories: where they live, removing them, and sweeping crash leftovers. */
8
+ /**
9
+ * Subtree of `scratchDir` that holds extracted Storybooks.
10
+ *
11
+ * `scratchDir` is often also the local storage root (dev server, generated servers), where
12
+ * `<project>/builds/<build>/...` holds screenshots and published statics. Keeping scratch in its
13
+ * own subtree means removing it can never touch a storage key.
14
+ */
15
+ const SCRATCH_DIRNAME = ".storyshelf-scratch";
16
+ /** Scratch dirs untouched for this long are assumed orphaned by a crash (no capture runs this long). */
17
+ const SCRATCH_MAX_AGE_MS = 216e5;
18
+ /** Directory inside a build's scratch directory that holds one subdirectory per shard. */
19
+ const SHARDS_DIRNAME = "shards";
20
+ /** Directory inside a build's scratch directory for the build's own job, apart from its shards. */
21
+ const JOB_DIRNAME = "job";
22
+ /** Scratch directory owned by one build: `<scratchDir>/.storyshelf-scratch/<projectId>/<buildId>`. */
23
+ function buildScratchDir(scratchDir, projectId, buildId) {
24
+ return join(scratchDir, SCRATCH_DIRNAME, projectId, buildId);
25
+ }
26
+ /** Scratch directory of the build's own job (plan or whole build): `<build>/job`, beside `shards/`. */
27
+ function jobScratchDir(scratchDir, projectId, buildId) {
28
+ return join(buildScratchDir(scratchDir, projectId, buildId), JOB_DIRNAME);
29
+ }
30
+ /** Scratch directory of one shard of a build, inside the build's: shards of a build can share a host. */
31
+ function shardScratchDir(scratchDir, projectId, buildId, shardId) {
32
+ return join(buildScratchDir(scratchDir, projectId, buildId), SHARDS_DIRNAME, shardId);
33
+ }
34
+ /**
35
+ * Remove one shard's scratch directory, leaving the build's other shards alone, then the build's
36
+ * directories if that left them empty: whichever shard finishes last tidies up. Best effort.
37
+ */
38
+ async function removeShardScratch(scratchDir, projectId, buildId, shardId, logger) {
39
+ try {
40
+ await rm(shardScratchDir(scratchDir, projectId, buildId, shardId), {
41
+ recursive: true,
42
+ force: true
43
+ });
44
+ await pruneBuildScratch(scratchDir, projectId, buildId);
45
+ } catch (error) {
46
+ logger?.warn({
47
+ err: error,
48
+ buildId,
49
+ shardId
50
+ }, "failed to remove shard scratch directory");
51
+ }
52
+ }
53
+ /**
54
+ * Remove the scratch of the build's own job (the plan, or the whole build when it is not sharded)
55
+ * and the build directory too if nothing else is left in it.
56
+ *
57
+ * Shards live in their own `shards/` directory beside `job/`, so this can never reach one: the plan
58
+ * hands its shards to the queue before it cleans up, and a worker may already be extracting.
59
+ */
60
+ async function removeJobScratch(scratchDir, projectId, buildId, logger) {
61
+ try {
62
+ await rm(jobScratchDir(scratchDir, projectId, buildId), {
63
+ recursive: true,
64
+ force: true
65
+ });
66
+ await pruneBuildScratch(scratchDir, projectId, buildId);
67
+ } catch (error) {
68
+ logger?.warn({
69
+ err: error,
70
+ buildId
71
+ }, "failed to remove job scratch directory");
72
+ }
73
+ }
74
+ /**
75
+ * Remove the build's scratch directories that are empty, and nothing that is not. A shard that is
76
+ * still running (a redelivered duplicate of one that already finished) keeps its directory; its own
77
+ * cleanup, or the boot sweep, removes it later. `rmdir` fails on a non-empty directory, which is
78
+ * the point.
79
+ */
80
+ async function pruneBuildScratch(scratchDir, projectId, buildId) {
81
+ const buildDir = buildScratchDir(scratchDir, projectId, buildId);
82
+ await rmdir(join(buildDir, SHARDS_DIRNAME)).catch(ignore);
83
+ await rmdir(buildDir).catch(ignore);
84
+ }
85
+ /** Remove a build's scratch directory. Best effort: a failure is logged, never thrown. */
86
+ async function removeBuildScratch(scratchDir, projectId, buildId, logger) {
87
+ try {
88
+ await rm(buildScratchDir(scratchDir, projectId, buildId), {
89
+ recursive: true,
90
+ force: true
91
+ });
92
+ } catch (error) {
93
+ logger?.warn({
94
+ err: error,
95
+ buildId
96
+ }, "failed to remove build scratch directory");
97
+ }
98
+ }
99
+ /**
100
+ * Boot-time sweep for scratch dirs a crash left behind. Age-gated rather than wholesale so a
101
+ * second process sharing the same `scratchDir` (server plus worker) keeps its live captures.
102
+ *
103
+ * @returns How many build scratch directories were removed.
104
+ */
105
+ async function sweepOrphanedScratch(scratchDir, options = {}) {
106
+ const root = join(scratchDir, SCRATCH_DIRNAME);
107
+ const cutoff = (options.now ?? Date.now()) - (options.maxAgeMs ?? 216e5);
108
+ const projects = await readdir(root).catch(() => []);
109
+ const total = (await Promise.all(projects.map(async (projectId) => await sweepProject(join(root, projectId), cutoff)))).reduce((sum, count) => sum + count, 0);
110
+ if (total > 0) options.logger?.info({ removed: total }, "removed orphaned capture scratch directories");
111
+ return total;
112
+ }
113
+ async function sweepProject(projectDir, cutoff) {
114
+ const builds = await readdir(projectDir).catch(() => []);
115
+ const results = await Promise.all(builds.map(async (buildId) => await sweepBuild(join(projectDir, buildId), cutoff)));
116
+ if ((await readdir(projectDir).catch(() => ["x"])).length === 0) await rm(projectDir, {
117
+ recursive: true,
118
+ force: true
119
+ }).catch(ignore);
120
+ return results.filter(Boolean).length;
121
+ }
122
+ async function sweepBuild(buildDir, cutoff) {
123
+ try {
124
+ if ((await stat(buildDir)).mtimeMs >= cutoff) return false;
125
+ await rm(buildDir, {
126
+ recursive: true,
127
+ force: true
128
+ });
129
+ return true;
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+ function ignore() {}
135
+ //#endregion
136
+ export { SCRATCH_DIRNAME, SCRATCH_MAX_AGE_MS, buildScratchDir, jobScratchDir, pruneBuildScratch, removeBuildScratch, removeJobScratch, removeShardScratch, shardScratchDir, sweepOrphanedScratch };
137
+
138
+ //# sourceMappingURL=scratch.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scratch.mjs","names":[],"sources":["../../src/capture/scratch.ts"],"sourcesContent":["/** Per-build scratch directories: where they live, removing them, and sweeping crash leftovers. */\nimport { readdir, rm, rmdir, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { Logger } from \"../logger.ts\";\n\n/**\n * Subtree of `scratchDir` that holds extracted Storybooks.\n *\n * `scratchDir` is often also the local storage root (dev server, generated servers), where\n * `<project>/builds/<build>/...` holds screenshots and published statics. Keeping scratch in its\n * own subtree means removing it can never touch a storage key.\n */\nexport const SCRATCH_DIRNAME = \".storyshelf-scratch\";\n\n/** Scratch dirs untouched for this long are assumed orphaned by a crash (no capture runs this long). */\nexport const SCRATCH_MAX_AGE_MS = 6 * 60 * 60 * 1000;\n\n/** Directory inside a build's scratch directory that holds one subdirectory per shard. */\nconst SHARDS_DIRNAME = \"shards\";\n\n/** Directory inside a build's scratch directory for the build's own job, apart from its shards. */\nconst JOB_DIRNAME = \"job\";\n\n/** Scratch directory owned by one build: `<scratchDir>/.storyshelf-scratch/<projectId>/<buildId>`. */\nexport function buildScratchDir(scratchDir: string, projectId: string, buildId: string): string {\n return join(scratchDir, SCRATCH_DIRNAME, projectId, buildId);\n}\n\n/** Scratch directory of the build's own job (plan or whole build): `<build>/job`, beside `shards/`. */\nexport function jobScratchDir(scratchDir: string, projectId: string, buildId: string): string {\n return join(buildScratchDir(scratchDir, projectId, buildId), JOB_DIRNAME);\n}\n\n/** Scratch directory of one shard of a build, inside the build's: shards of a build can share a host. */\nexport function shardScratchDir(\n scratchDir: string,\n projectId: string,\n buildId: string,\n shardId: string,\n): string {\n return join(buildScratchDir(scratchDir, projectId, buildId), SHARDS_DIRNAME, shardId);\n}\n\n/**\n * Remove one shard's scratch directory, leaving the build's other shards alone, then the build's\n * directories if that left them empty: whichever shard finishes last tidies up. Best effort.\n */\nexport async function removeShardScratch(\n scratchDir: string,\n projectId: string,\n buildId: string,\n shardId: string,\n logger?: Logger,\n): Promise<void> {\n try {\n await rm(shardScratchDir(scratchDir, projectId, buildId, shardId), {\n recursive: true,\n force: true,\n });\n await pruneBuildScratch(scratchDir, projectId, buildId);\n } catch (error) {\n logger?.warn({ err: error, buildId, shardId }, \"failed to remove shard scratch directory\");\n }\n}\n\n/**\n * Remove the scratch of the build's own job (the plan, or the whole build when it is not sharded)\n * and the build directory too if nothing else is left in it.\n *\n * Shards live in their own `shards/` directory beside `job/`, so this can never reach one: the plan\n * hands its shards to the queue before it cleans up, and a worker may already be extracting.\n */\nexport async function removeJobScratch(\n scratchDir: string,\n projectId: string,\n buildId: string,\n logger?: Logger,\n): Promise<void> {\n try {\n await rm(jobScratchDir(scratchDir, projectId, buildId), { recursive: true, force: true });\n await pruneBuildScratch(scratchDir, projectId, buildId);\n } catch (error) {\n logger?.warn({ err: error, buildId }, \"failed to remove job scratch directory\");\n }\n}\n\n/**\n * Remove the build's scratch directories that are empty, and nothing that is not. A shard that is\n * still running (a redelivered duplicate of one that already finished) keeps its directory; its own\n * cleanup, or the boot sweep, removes it later. `rmdir` fails on a non-empty directory, which is\n * the point.\n */\nexport async function pruneBuildScratch(\n scratchDir: string,\n projectId: string,\n buildId: string,\n): Promise<void> {\n const buildDir = buildScratchDir(scratchDir, projectId, buildId);\n await rmdir(join(buildDir, SHARDS_DIRNAME)).catch(ignore);\n await rmdir(buildDir).catch(ignore);\n}\n\n/** Remove a build's scratch directory. Best effort: a failure is logged, never thrown. */\nexport async function removeBuildScratch(\n scratchDir: string,\n projectId: string,\n buildId: string,\n logger?: Logger,\n): Promise<void> {\n try {\n await rm(buildScratchDir(scratchDir, projectId, buildId), { recursive: true, force: true });\n } catch (error) {\n logger?.warn({ err: error, buildId }, \"failed to remove build scratch directory\");\n }\n}\n\n/** Options for {@link sweepOrphanedScratch}. */\nexport interface ScratchSweepOptions {\n /** Remove build scratch dirs not modified for this long (default {@link SCRATCH_MAX_AGE_MS}). */\n maxAgeMs?: number;\n /** Clock override for tests. */\n now?: number;\n logger?: Logger;\n}\n\n/**\n * Boot-time sweep for scratch dirs a crash left behind. Age-gated rather than wholesale so a\n * second process sharing the same `scratchDir` (server plus worker) keeps its live captures.\n *\n * @returns How many build scratch directories were removed.\n */\nexport async function sweepOrphanedScratch(\n scratchDir: string,\n options: ScratchSweepOptions = {},\n): Promise<number> {\n const root = join(scratchDir, SCRATCH_DIRNAME);\n const cutoff = (options.now ?? Date.now()) - (options.maxAgeMs ?? SCRATCH_MAX_AGE_MS);\n const projects = await readdir(root).catch(() => [] as string[]);\n const removed = await Promise.all(\n projects.map(async (projectId) => await sweepProject(join(root, projectId), cutoff)),\n );\n const total = removed.reduce((sum, count) => sum + count, 0);\n if (total > 0) {\n options.logger?.info({ removed: total }, \"removed orphaned capture scratch directories\");\n }\n return total;\n}\n\nasync function sweepProject(projectDir: string, cutoff: number): Promise<number> {\n const builds = await readdir(projectDir).catch(() => [] as string[]);\n const results = await Promise.all(\n builds.map(async (buildId) => await sweepBuild(join(projectDir, buildId), cutoff)),\n );\n // Drop the project dir once empty; `rmdir` semantics via a non-recursive remove that fails if not.\n if ((await readdir(projectDir).catch(() => [\"x\"])).length === 0) {\n await rm(projectDir, { recursive: true, force: true }).catch(ignore);\n }\n return results.filter(Boolean).length;\n}\n\nasync function sweepBuild(buildDir: string, cutoff: number): Promise<boolean> {\n try {\n if ((await stat(buildDir)).mtimeMs >= cutoff) {\n return false;\n }\n await rm(buildDir, { recursive: true, force: true });\n return true;\n } catch {\n return false;\n }\n}\n\nfunction ignore(): void {}\n"],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/capture/shard-plan.ts
5
+ /**
6
+ * Split the work into shards of about `size` captures, keeping a story's engines together so
7
+ * a newly enabled engine can still bootstrap from its counterpart in the same shard.
8
+ *
9
+ * Returns `[]` when sharding is off or the build is too small to be worth it (up to twice the
10
+ * shard size runs as one shard, exactly as before).
11
+ *
12
+ * Without timings the slices are contiguous runs of stories in their original order. Given
13
+ * `weightOf`, the stories are packed longest-first into shards of equal capture capacity and the
14
+ * shards come back heaviest first, so the slow work starts early and no shard straggles.
15
+ *
16
+ * @param work - Per-engine render lists, primary engine first.
17
+ * @param capturesOf - Captures one story produces on one engine (its viewport count).
18
+ * @param size - Target captures per shard; undefined or below 1 turns sharding off.
19
+ * @param weightOf - What a story cost last time, in milliseconds; unknown for a story returns undefined.
20
+ */
21
+ function planShards(work, capturesOf, size, weightOf) {
22
+ if (!size || !worthSharding(work, capturesOf, size)) return [];
23
+ const costs = storyCosts(work, capturesOf);
24
+ const weights = weightOf ? weighted(costs, weightOf) : void 0;
25
+ return (weights ? packLongestFirst(costs, weights, size) : sliceByCost(costs, size)).map((ids) => restrictTo(work, new Set(ids)));
26
+ }
27
+ /**
28
+ * Whether {@link planShards} would split this work at all: sharding is on (`size` of at least 1)
29
+ * and the work is more than twice the shard size. Lets the plan stage skip work only an ordering needs.
30
+ */
31
+ function worthSharding(work, capturesOf, size) {
32
+ return size !== void 0 && size >= 1 && totalCaptures(work, capturesOf) > size * 2;
33
+ }
34
+ /** Total captures the work produces. */
35
+ function totalCaptures(work, capturesOf) {
36
+ return [...storyCosts(work, capturesOf).values()].reduce((sum, cost) => sum + cost, 0);
37
+ }
38
+ /** Captures per story id, summed over the engines that render it, in first-seen order. */
39
+ function storyCosts(work, capturesOf) {
40
+ const costs = /* @__PURE__ */ new Map();
41
+ for (const engine of work) for (const story of engine.stories) costs.set(story.id, (costs.get(story.id) ?? 0) + capturesOf(story));
42
+ return costs;
43
+ }
44
+ /**
45
+ * What each story is expected to cost in milliseconds. A story nothing is known about is priced at
46
+ * the average cost of one capture, so a new story is treated as ordinary rather than free.
47
+ */
48
+ function weighted(costs, weightOf) {
49
+ const known = [...costs.keys()].flatMap((id) => {
50
+ const weight = weightOf(id);
51
+ return typeof weight === "number" && weight > 0 ? [[id, weight]] : [];
52
+ });
53
+ if (known.length === 0) return;
54
+ const knownCaptures = known.reduce((sum, [id]) => sum + (costs.get(id) ?? 0), 0);
55
+ const perCapture = known.reduce((sum, [, weight]) => sum + weight, 0) / Math.max(knownCaptures, 1);
56
+ const weights = new Map(known);
57
+ for (const [id, captures] of costs) if (!weights.has(id)) weights.set(id, perCapture * captures);
58
+ return weights;
59
+ }
60
+ /**
61
+ * Longest-processing-time packing: the dearest stories are placed first, each into the shard that
62
+ * has the least work so far and still has room for its captures. Shards come back heaviest first.
63
+ */
64
+ function packLongestFirst(costs, weights, size) {
65
+ const count = Math.max(1, Math.ceil(sumOf(costs) / size));
66
+ const bins = Array.from({ length: count }, newBin);
67
+ for (const id of byWeightDescending(costs, weights)) {
68
+ const captures = costs.get(id) ?? 0;
69
+ const bin = binFor(bins, captures, size);
70
+ bin.ids.push(id);
71
+ bin.captures += captures;
72
+ bin.weight += weights.get(id) ?? 0;
73
+ }
74
+ return bins.filter((bin) => bin.ids.length > 0).toSorted((left, right) => right.weight - left.weight).map((bin) => bin.ids);
75
+ }
76
+ /** Story ids dearest first; equal cost keeps the order they were planned in. */
77
+ function byWeightDescending(costs, weights) {
78
+ return [...costs.keys()].toSorted((left, right) => (weights.get(right) ?? 0) - (weights.get(left) ?? 0));
79
+ }
80
+ function newBin() {
81
+ return {
82
+ ids: [],
83
+ captures: 0,
84
+ weight: 0
85
+ };
86
+ }
87
+ /**
88
+ * The shard with the least work that still has room for `captures`. When none has room the last
89
+ * stories did not fit anywhere, so a shard is added: one more beats overfilling one, just as an
90
+ * oversized story gets a shard of its own when there are no timings.
91
+ */
92
+ function binFor(bins, captures, size) {
93
+ const [lightest] = bins.filter((bin) => bin.captures + captures <= size).toSorted((left, right) => left.weight - right.weight);
94
+ if (lightest) return lightest;
95
+ const fresh = newBin();
96
+ bins.push(fresh);
97
+ return fresh;
98
+ }
99
+ function sumOf(costs) {
100
+ return [...costs.values()].reduce((sum, cost) => sum + cost, 0);
101
+ }
102
+ /** Contiguous runs of ids whose cost stays within `size` (a single oversized story gets its own). */
103
+ function sliceByCost(costs, size) {
104
+ const slices = [];
105
+ for (const id of costs.keys()) {
106
+ const cost = costs.get(id) ?? 0;
107
+ const last = slices.at(-1);
108
+ if (last && last.used + cost <= size) {
109
+ last.ids.push(id);
110
+ last.used += cost;
111
+ } else slices.push({
112
+ ids: [id],
113
+ used: cost
114
+ });
115
+ }
116
+ return slices.map((slice) => slice.ids);
117
+ }
118
+ /** The work restricted to some stories; engines left with nothing to render are dropped. */
119
+ function restrictTo(work, ids) {
120
+ return work.map((engine) => ({
121
+ browser: engine.browser,
122
+ stories: engine.stories.filter((story) => ids.has(story.id))
123
+ })).filter((engine) => engine.stories.length > 0);
124
+ }
125
+ //#endregion
126
+ export { planShards, totalCaptures, worthSharding };
127
+
128
+ //# sourceMappingURL=shard-plan.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shard-plan.mjs","names":[],"sources":["../../src/capture/shard-plan.ts"],"sourcesContent":["/** Splitting a build's render work into shards (ADR 0005 phase 5). Pure: no I/O, no clock. */\nimport type { CaptureTarget } from \"../adapters/capture-runner.ts\";\nimport type { StoryEntry, Viewport } from \"./adapter.ts\";\n\n/** What one engine has to render this build (after affected-capture partitioning). */\nexport interface EngineWork {\n browser: CaptureTarget;\n stories: StoryEntry[];\n}\n\n/** One render shard: a slice of stories, with the engines each of them is rendered on. */\nexport interface ShardPayload {\n /** Engines of the slice, primary first; each lists only the stories it renders. */\n browsers: EngineWork[];\n /** Viewports the stories resolve against. */\n viewports: Viewport[];\n /** Engines with no baseline yet when the plan was made (fixed at plan time, not per shard). */\n fresh: string[];\n}\n\n/** Settings of the plan itself, kept on the plan row. */\nexport interface PlanPayload {\n browsers: string[];\n fresh: string[];\n /** Captures per shard the plan aimed for. */\n shardSize: number;\n /** Captures (story x viewport x engine) the plan covers. */\n total: number;\n}\n\n/** Outcome of one render shard, kept on its row. */\nexport interface ShardResult {\n failedStoryIds: string[];\n flakyStoryIds: string[];\n a11yStoryIds: string[];\n captured: number;\n durationMs: number;\n}\n\n/** Outcome of the plan stage: every story of the build, and stories whose inherited snapshot failed. */\nexport interface PlanResult {\n storyIds: string[];\n failedStoryIds: string[];\n}\n\n/**\n * Split the work into shards of about `size` captures, keeping a story's engines together so\n * a newly enabled engine can still bootstrap from its counterpart in the same shard.\n *\n * Returns `[]` when sharding is off or the build is too small to be worth it (up to twice the\n * shard size runs as one shard, exactly as before).\n *\n * Without timings the slices are contiguous runs of stories in their original order. Given\n * `weightOf`, the stories are packed longest-first into shards of equal capture capacity and the\n * shards come back heaviest first, so the slow work starts early and no shard straggles.\n *\n * @param work - Per-engine render lists, primary engine first.\n * @param capturesOf - Captures one story produces on one engine (its viewport count).\n * @param size - Target captures per shard; undefined or below 1 turns sharding off.\n * @param weightOf - What a story cost last time, in milliseconds; unknown for a story returns undefined.\n */\nexport function planShards(\n work: readonly EngineWork[],\n capturesOf: (story: StoryEntry) => number,\n size: number | undefined,\n weightOf?: (storyId: string) => number | undefined,\n): EngineWork[][] {\n if (!size || !worthSharding(work, capturesOf, size)) {\n return [];\n }\n const costs = storyCosts(work, capturesOf);\n const weights = weightOf ? weighted(costs, weightOf) : undefined;\n const slices = weights ? packLongestFirst(costs, weights, size) : sliceByCost(costs, size);\n return slices.map((ids) => restrictTo(work, new Set(ids)));\n}\n\n/**\n * Whether {@link planShards} would split this work at all: sharding is on (`size` of at least 1)\n * and the work is more than twice the shard size. Lets the plan stage skip work only an ordering needs.\n */\nexport function worthSharding(\n work: readonly EngineWork[],\n capturesOf: (story: StoryEntry) => number,\n size: number | undefined,\n): boolean {\n return size !== undefined && size >= 1 && totalCaptures(work, capturesOf) > size * 2;\n}\n\n/** Total captures the work produces. */\nexport function totalCaptures(\n work: readonly EngineWork[],\n capturesOf: (story: StoryEntry) => number,\n): number {\n return [...storyCosts(work, capturesOf).values()].reduce((sum, cost) => sum + cost, 0);\n}\n\n/** Captures per story id, summed over the engines that render it, in first-seen order. */\nfunction storyCosts(\n work: readonly EngineWork[],\n capturesOf: (story: StoryEntry) => number,\n): Map<string, number> {\n const costs = new Map<string, number>();\n for (const engine of work) {\n for (const story of engine.stories) {\n costs.set(story.id, (costs.get(story.id) ?? 0) + capturesOf(story));\n }\n }\n return costs;\n}\n\n/**\n * What each story is expected to cost in milliseconds. A story nothing is known about is priced at\n * the average cost of one capture, so a new story is treated as ordinary rather than free.\n */\nfunction weighted(\n costs: Map<string, number>,\n weightOf: (storyId: string) => number | undefined,\n): Map<string, number> | undefined {\n const known = [...costs.keys()].flatMap((id) => {\n const weight = weightOf(id);\n return typeof weight === \"number\" && weight > 0 ? [[id, weight] as const] : [];\n });\n if (known.length === 0) {\n return undefined;\n }\n const knownCaptures = known.reduce((sum, [id]) => sum + (costs.get(id) ?? 0), 0);\n const perCapture =\n known.reduce((sum, [, weight]) => sum + weight, 0) / Math.max(knownCaptures, 1);\n const weights = new Map(known);\n for (const [id, captures] of costs) {\n if (!weights.has(id)) {\n weights.set(id, perCapture * captures);\n }\n }\n return weights;\n}\n\n/**\n * Longest-processing-time packing: the dearest stories are placed first, each into the shard that\n * has the least work so far and still has room for its captures. Shards come back heaviest first.\n */\nfunction packLongestFirst(\n costs: Map<string, number>,\n weights: Map<string, number>,\n size: number,\n): string[][] {\n const count = Math.max(1, Math.ceil(sumOf(costs) / size));\n const bins: Bin[] = Array.from({ length: count }, newBin);\n for (const id of byWeightDescending(costs, weights)) {\n const captures = costs.get(id) ?? 0;\n const bin = binFor(bins, captures, size);\n bin.ids.push(id);\n bin.captures += captures;\n bin.weight += weights.get(id) ?? 0;\n }\n return bins\n .filter((bin) => bin.ids.length > 0)\n .toSorted((left, right) => right.weight - left.weight)\n .map((bin) => bin.ids);\n}\n\n/** Story ids dearest first; equal cost keeps the order they were planned in. */\nfunction byWeightDescending(costs: Map<string, number>, weights: Map<string, number>): string[] {\n return [...costs.keys()].toSorted(\n (left, right) => (weights.get(right) ?? 0) - (weights.get(left) ?? 0),\n );\n}\n\n/** One shard being filled: the stories in it, their captures and what they are expected to cost. */\ninterface Bin {\n ids: string[];\n captures: number;\n weight: number;\n}\n\nfunction newBin(): Bin {\n return { ids: [], captures: 0, weight: 0 };\n}\n\n/**\n * The shard with the least work that still has room for `captures`. When none has room the last\n * stories did not fit anywhere, so a shard is added: one more beats overfilling one, just as an\n * oversized story gets a shard of its own when there are no timings.\n */\nfunction binFor(bins: Bin[], captures: number, size: number): Bin {\n const withRoom = bins.filter((bin) => bin.captures + captures <= size);\n const [lightest] = withRoom.toSorted((left, right) => left.weight - right.weight);\n if (lightest) {\n return lightest;\n }\n const fresh = newBin();\n bins.push(fresh);\n return fresh;\n}\n\nfunction sumOf(costs: Map<string, number>): number {\n return [...costs.values()].reduce((sum, cost) => sum + cost, 0);\n}\n\n/** Contiguous runs of ids whose cost stays within `size` (a single oversized story gets its own). */\nfunction sliceByCost(costs: Map<string, number>, size: number): string[][] {\n const slices: { ids: string[]; used: number }[] = [];\n for (const id of costs.keys()) {\n const cost = costs.get(id) ?? 0;\n const last = slices.at(-1);\n if (last && last.used + cost <= size) {\n last.ids.push(id);\n last.used += cost;\n } else {\n slices.push({ ids: [id], used: cost });\n }\n }\n return slices.map((slice) => slice.ids);\n}\n\n/** The work restricted to some stories; engines left with nothing to render are dropped. */\nfunction restrictTo(work: readonly EngineWork[], ids: ReadonlySet<string>): EngineWork[] {\n return work\n .map((engine) => ({\n browser: engine.browser,\n stories: engine.stories.filter((story) => ids.has(story.id)),\n }))\n .filter((engine) => engine.stories.length > 0);\n}\n"],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { storybookUrlSource } from "../storybook-urls.mjs";
5
+ //#region src/capture/shards/context.ts
6
+ /** A capture context for the persist and finalize steps of one stage. */
7
+ function contextOf(target, parts) {
8
+ const { options } = target;
9
+ return {
10
+ db: options.db,
11
+ tables: options.tables,
12
+ storage: options.storage,
13
+ project: target.project,
14
+ build: target.build,
15
+ viewports: parts.viewports,
16
+ browsers: parts.browsers,
17
+ captures: parts.captures ?? [],
18
+ inherited: parts.inherited ?? [],
19
+ ...parts.freshBrowsers ? { freshBrowsers: parts.freshBrowsers } : {},
20
+ progress: parts.progress,
21
+ ...options.logger ? { logger: options.logger } : {},
22
+ ...options.secret ? { secret: options.secret } : {}
23
+ };
24
+ }
25
+ /** The matrix input for rendering `stories` from `storybookDir` on `browsers`. */
26
+ function matrixInputOf(target, parts) {
27
+ const { options } = target;
28
+ return {
29
+ db: options.db,
30
+ storage: options.storage,
31
+ tables: options.tables,
32
+ runner: options.runner,
33
+ build: target.build,
34
+ project: target.project,
35
+ stories: parts.stories,
36
+ viewports: parts.viewports,
37
+ storybookDir: parts.storybookDir,
38
+ browsers: parts.browsers,
39
+ affectedPaths: parts.affectedPaths,
40
+ renderTimeoutMs: options.renderTimeoutMs,
41
+ storybookUrl: storybookUrlSource(options, target),
42
+ logger: parts.logger ?? options.logger,
43
+ recordLog: parts.recordLog,
44
+ progress: parts.progress,
45
+ runId: parts.runId
46
+ };
47
+ }
48
+ //#endregion
49
+ export { contextOf, matrixInputOf };
50
+
51
+ //# sourceMappingURL=context.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.mjs","names":[],"sources":["../../../src/capture/shards/context.ts"],"sourcesContent":["/** What every stage of a sharded capture works against. */\nimport type { Logger } from \"pino\";\nimport type { CaptureTarget } from \"../../adapters/capture-runner.ts\";\nimport type { Build } from \"../../schema/build.ts\";\nimport type { Project } from \"../../schema/project.ts\";\nimport type { Viewport } from \"../adapter.ts\";\nimport type { MatrixInput } from \"../matrix.ts\";\nimport type { CaptureJobOptions } from \"../orchestrator.ts\";\nimport type { CaptureContext } from \"../pipeline.ts\";\nimport type { CaptureProgress } from \"../progress.ts\";\nimport { storybookUrlSource } from \"../storybook-urls.ts\";\n\n/** The build being captured and the adapters to capture it with. */\nexport interface ShardTarget {\n options: CaptureJobOptions;\n build: Build;\n project: Project;\n}\n\n/** A capture context for the persist and finalize steps of one stage. */\nexport function contextOf(\n target: ShardTarget,\n parts: {\n viewports: Viewport[];\n browsers: CaptureTarget[];\n captures?: CaptureContext[\"captures\"];\n inherited?: NonNullable<CaptureContext[\"inherited\"]>;\n freshBrowsers?: ReadonlySet<string>;\n progress?: CaptureProgress | undefined;\n },\n): CaptureContext {\n const { options } = target;\n return {\n db: options.db,\n tables: options.tables,\n storage: options.storage,\n project: target.project,\n build: target.build,\n viewports: parts.viewports,\n browsers: parts.browsers,\n captures: parts.captures ?? [],\n inherited: parts.inherited ?? [],\n ...(parts.freshBrowsers ? { freshBrowsers: parts.freshBrowsers } : {}),\n progress: parts.progress,\n ...(options.logger ? { logger: options.logger } : {}),\n ...(options.secret ? { secret: options.secret } : {}),\n };\n}\n\n/** The matrix input for rendering `stories` from `storybookDir` on `browsers`. */\nexport function matrixInputOf(\n target: ShardTarget,\n parts: {\n stories: MatrixInput[\"stories\"];\n viewports: Viewport[];\n browsers: CaptureTarget[];\n storybookDir: string;\n affectedPaths: string[] | null;\n logger?: Logger | undefined;\n recordLog?: MatrixInput[\"recordLog\"];\n progress?: CaptureProgress | undefined;\n runId?: string | undefined;\n },\n): MatrixInput {\n const { options } = target;\n return {\n db: options.db,\n storage: options.storage,\n tables: options.tables,\n runner: options.runner,\n build: target.build,\n project: target.project,\n stories: parts.stories,\n viewports: parts.viewports,\n storybookDir: parts.storybookDir,\n browsers: parts.browsers,\n affectedPaths: parts.affectedPaths,\n renderTimeoutMs: options.renderTimeoutMs,\n storybookUrl: storybookUrlSource(options, target),\n logger: parts.logger ?? options.logger,\n recordLog: parts.recordLog,\n progress: parts.progress,\n runId: parts.runId,\n };\n}\n"],"mappings":";;;;;;AAoBA,SAAgB,UACd,QACA,OAQgB;CAChB,MAAM,EAAE,YAAY;CACpB,OAAO;EACL,IAAI,QAAQ;EACZ,QAAQ,QAAQ;EAChB,SAAS,QAAQ;EACjB,SAAS,OAAO;EAChB,OAAO,OAAO;EACd,WAAW,MAAM;EACjB,UAAU,MAAM;EAChB,UAAU,MAAM,YAAY,CAAC;EAC7B,WAAW,MAAM,aAAa,CAAC;EAC/B,GAAI,MAAM,gBAAgB,EAAE,eAAe,MAAM,cAAc,IAAI,CAAC;EACpE,UAAU,MAAM;EAChB,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;EACnD,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;CACrD;AACF;;AAGA,SAAgB,cACd,QACA,OAWa;CACb,MAAM,EAAE,YAAY;CACpB,OAAO;EACL,IAAI,QAAQ;EACZ,SAAS,QAAQ;EACjB,QAAQ,QAAQ;EAChB,QAAQ,QAAQ;EAChB,OAAO,OAAO;EACd,SAAS,OAAO;EAChB,SAAS,MAAM;EACf,WAAW,MAAM;EACjB,cAAc,MAAM;EACpB,UAAU,MAAM;EAChB,eAAe,MAAM;EACrB,iBAAiB,QAAQ;EACzB,cAAc,mBAAmB,SAAS,MAAM;EAChD,QAAQ,MAAM,UAAU,QAAQ;EAChC,WAAW,MAAM;EACjB,UAAU,MAAM;EAChB,OAAO,MAAM;CACf;AACF"}