@storyshelf/core 0.3.2 → 0.5.3

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 (393) hide show
  1. package/README.md +38 -31
  2. package/dist/adapter/capture-queue.d.mts +29 -1
  3. package/dist/adapter/capture-runner.d.mts +18 -2
  4. package/dist/adapter/capture-runner.mjs +12 -1
  5. package/dist/adapter/capture-runner.mjs.map +1 -0
  6. package/dist/adapter/database.d.mts +67 -17
  7. package/dist/adapter/database.mjs +22 -55
  8. package/dist/adapter/database.mjs.map +1 -1
  9. package/dist/adapter/email-sender.d.mts +27 -0
  10. package/dist/adapter/email-sender.mjs +4 -0
  11. package/dist/adapter/metadata.d.mts +21 -15
  12. package/dist/adapter/notifier.d.mts +7 -0
  13. package/dist/adapter/notifier.mjs +6 -0
  14. package/dist/adapter/setup.d.mts +61 -0
  15. package/dist/adapter/setup.mjs +29 -0
  16. package/dist/adapter/setup.mjs.map +1 -0
  17. package/dist/adapter/webhook-events.d.mts +29 -0
  18. package/dist/adapter/webhook-events.mjs +83 -0
  19. package/dist/adapter/webhook-events.mjs.map +1 -0
  20. package/dist/adapters/notifier/events.d.mts +26 -0
  21. package/dist/adapters/notifier/events.mjs +66 -0
  22. package/dist/adapters/notifier/events.mjs.map +1 -0
  23. package/dist/adapters/notifier/format.d.mts +14 -0
  24. package/dist/adapters/notifier/format.mjs +96 -0
  25. package/dist/adapters/notifier/format.mjs.map +1 -0
  26. package/dist/adapters/notifier/provider.d.mts +32 -0
  27. package/dist/adapters/notifier/types.d.mts +56 -0
  28. package/dist/adapters/setup-runner.d.mts +23 -0
  29. package/dist/adapters/setup-runner.mjs +149 -0
  30. package/dist/adapters/setup-runner.mjs.map +1 -0
  31. package/dist/adapters/validate.d.mts +30 -0
  32. package/dist/adapters/validate.mjs +313 -0
  33. package/dist/adapters/validate.mjs.map +1 -0
  34. package/dist/auth.d.mts +94 -0
  35. package/dist/auth.mjs +4 -0
  36. package/dist/brand-packs.d.mts +16 -0
  37. package/dist/brand-packs.mjs +130 -0
  38. package/dist/brand-packs.mjs.map +1 -0
  39. package/dist/capture/adapter.d.mts +51 -1
  40. package/dist/capture/adapter.mjs.map +1 -1
  41. package/dist/capture/affected.d.mts +16 -0
  42. package/dist/capture/affected.mjs +64 -0
  43. package/dist/capture/affected.mjs.map +1 -0
  44. package/dist/capture/diff-record.mjs +30 -0
  45. package/dist/capture/diff-record.mjs.map +1 -0
  46. package/dist/capture/dispatch.d.mts +36 -0
  47. package/dist/capture/dispatch.mjs +88 -31
  48. package/dist/capture/dispatch.mjs.map +1 -1
  49. package/dist/capture/guards.mjs +31 -0
  50. package/dist/capture/guards.mjs.map +1 -0
  51. package/dist/capture/orchestrator.d.mts +41 -6
  52. package/dist/capture/orchestrator.mjs +151 -28
  53. package/dist/capture/orchestrator.mjs.map +1 -1
  54. package/dist/capture/pipeline.d.mts +30 -16
  55. package/dist/capture/pipeline.mjs +143 -39
  56. package/dist/capture/pipeline.mjs.map +1 -1
  57. package/dist/capture/recovery.d.mts +21 -0
  58. package/dist/capture/recovery.mjs +83 -0
  59. package/dist/capture/recovery.mjs.map +1 -0
  60. package/dist/capture/rediff.d.mts +35 -0
  61. package/dist/capture/rediff.mjs +77 -0
  62. package/dist/capture/rediff.mjs.map +1 -0
  63. package/dist/capture/sizing.d.mts +58 -0
  64. package/dist/capture/sizing.mjs +119 -0
  65. package/dist/capture/sizing.mjs.map +1 -0
  66. package/dist/capture/skip-checks.mjs +3 -3
  67. package/dist/capture/skip-checks.mjs.map +1 -1
  68. package/dist/capture/statics.d.mts +38 -0
  69. package/dist/capture/statics.mjs +38 -4
  70. package/dist/capture/statics.mjs.map +1 -1
  71. package/dist/capture/status-fanout.mjs +7 -2
  72. package/dist/capture/status-fanout.mjs.map +1 -1
  73. package/dist/capture/storybook.d.mts +31 -2
  74. package/dist/capture/storybook.mjs +77 -2
  75. package/dist/capture/storybook.mjs.map +1 -1
  76. package/dist/capture.d.mts +10 -4
  77. package/dist/capture.mjs +7 -2
  78. package/dist/config.d.mts +121 -4
  79. package/dist/config.mjs +55 -8
  80. package/dist/config.mjs.map +1 -1
  81. package/dist/index.d.mts +5 -56
  82. package/dist/index.mjs +4 -284
  83. package/dist/logger.d.mts +26 -17
  84. package/dist/logger.mjs +10 -5
  85. package/dist/logger.mjs.map +1 -1
  86. package/dist/models/baseline-status.d.mts +29 -0
  87. package/dist/models/baseline-status.mjs +26 -0
  88. package/dist/models/baseline-status.mjs.map +1 -0
  89. package/dist/models/baseline.d.mts +33 -0
  90. package/dist/models/baseline.mjs +35 -13
  91. package/dist/models/baseline.mjs.map +1 -1
  92. package/dist/models/build.d.mts +87 -0
  93. package/dist/models/build.mjs +83 -35
  94. package/dist/models/build.mjs.map +1 -1
  95. package/dist/models/capture-attempt.d.mts +40 -0
  96. package/dist/models/capture-attempt.mjs +85 -0
  97. package/dist/models/capture-attempt.mjs.map +1 -0
  98. package/dist/models/capture-log.d.mts +21 -0
  99. package/dist/models/capture-log.mjs +44 -0
  100. package/dist/models/capture-log.mjs.map +1 -0
  101. package/dist/models/comment.d.mts +29 -0
  102. package/dist/models/comment.mjs +11 -8
  103. package/dist/models/comment.mjs.map +1 -1
  104. package/dist/models/content-ref.d.mts +21 -0
  105. package/dist/models/content-ref.mjs +49 -0
  106. package/dist/models/content-ref.mjs.map +1 -0
  107. package/dist/models/credentials.d.mts +36 -0
  108. package/dist/models/credentials.mjs +103 -0
  109. package/dist/models/credentials.mjs.map +1 -0
  110. package/dist/models/label.d.mts +58 -0
  111. package/dist/models/label.mjs +32 -27
  112. package/dist/models/label.mjs.map +1 -1
  113. package/dist/models/member.d.mts +34 -0
  114. package/dist/models/member.mjs +20 -11
  115. package/dist/models/member.mjs.map +1 -1
  116. package/dist/models/notification-channel.d.mts +52 -0
  117. package/dist/models/notification-channel.mjs +93 -0
  118. package/dist/models/notification-channel.mjs.map +1 -0
  119. package/dist/models/notification-subscription.d.mts +44 -0
  120. package/dist/models/notification-subscription.mjs +89 -0
  121. package/dist/models/notification-subscription.mjs.map +1 -0
  122. package/dist/models/project-group-mapping.d.mts +30 -0
  123. package/dist/models/project-group-mapping.mjs +46 -0
  124. package/dist/models/project-group-mapping.mjs.map +1 -0
  125. package/dist/models/project.d.mts +54 -0
  126. package/dist/models/project.mjs +15 -11
  127. package/dist/models/project.mjs.map +1 -1
  128. package/dist/models/snapshot.d.mts +58 -0
  129. package/dist/models/snapshot.mjs +13 -7
  130. package/dist/models/snapshot.mjs.map +1 -1
  131. package/dist/models/status-config.d.mts +38 -0
  132. package/dist/models/status-config.mjs +12 -11
  133. package/dist/models/status-config.mjs.map +1 -1
  134. package/dist/models/token.d.mts +44 -0
  135. package/dist/models/token.mjs +18 -15
  136. package/dist/models/token.mjs.map +1 -1
  137. package/dist/models/user.d.mts +42 -0
  138. package/dist/models/user.mjs +65 -0
  139. package/dist/models/user.mjs.map +1 -0
  140. package/dist/models/webhook.d.mts +50 -0
  141. package/dist/models/webhook.mjs +12 -10
  142. package/dist/models/webhook.mjs.map +1 -1
  143. package/dist/models.d.mts +22 -0
  144. package/dist/models.mjs +23 -0
  145. package/dist/paths.mjs +29 -0
  146. package/dist/paths.mjs.map +1 -1
  147. package/dist/retention.d.mts +45 -0
  148. package/dist/retention.mjs +145 -0
  149. package/dist/retention.mjs.map +1 -0
  150. package/dist/schema/baseline.d.mts +1 -0
  151. package/dist/schema/build.d.mts +8 -0
  152. package/dist/schema/capture-attempt.d.mts +38 -0
  153. package/dist/schema/comment.d.mts +1 -1
  154. package/dist/schema/content-ref.d.mts +13 -0
  155. package/dist/schema/member.d.mts +2 -0
  156. package/dist/schema/notification.d.mts +35 -0
  157. package/dist/schema/project-group-mapping.d.mts +15 -0
  158. package/dist/schema/project.d.mts +4 -0
  159. package/dist/schema/snapshot.d.mts +7 -0
  160. package/dist/schema/token.d.mts +2 -0
  161. package/dist/schema/user.d.mts +11 -1
  162. package/dist/schema.d.mts +9 -46
  163. package/dist/schema.mjs +1 -55
  164. package/dist/test-helpers/create-project.d.mts +15 -0
  165. package/dist/test-helpers/create-project.mjs +28 -0
  166. package/dist/test-helpers/create-project.mjs.map +1 -0
  167. package/dist/test-helpers/database-contract.d.mts +13 -0
  168. package/dist/test-helpers/database-contract.mjs +58 -0
  169. package/dist/test-helpers/database-contract.mjs.map +1 -0
  170. package/dist/test-helpers/fake-adapters.d.mts +14 -0
  171. package/dist/test-helpers/fake-adapters.mjs +20 -0
  172. package/dist/test-helpers/fake-adapters.mjs.map +1 -0
  173. package/dist/test-helpers/fake-database.d.mts +11 -0
  174. package/dist/test-helpers/fake-database.mjs +104 -0
  175. package/dist/test-helpers/fake-database.mjs.map +1 -0
  176. package/dist/test-helpers/fake-storage.d.mts +14 -0
  177. package/dist/test-helpers/fake-storage.mjs +49 -0
  178. package/dist/test-helpers/fake-storage.mjs.map +1 -0
  179. package/dist/test-helpers/fake-tables.mjs +276 -0
  180. package/dist/test-helpers/fake-tables.mjs.map +1 -0
  181. package/dist/test-helpers/git-contract.d.mts +11 -0
  182. package/dist/test-helpers/git-contract.mjs +34 -0
  183. package/dist/test-helpers/git-contract.mjs.map +1 -0
  184. package/dist/test-helpers/notifier-contract.d.mts +11 -0
  185. package/dist/test-helpers/notifier-contract.mjs +34 -0
  186. package/dist/test-helpers/notifier-contract.mjs.map +1 -0
  187. package/dist/test-helpers/queue-contract.d.mts +11 -0
  188. package/dist/test-helpers/queue-contract.mjs +64 -0
  189. package/dist/test-helpers/queue-contract.mjs.map +1 -0
  190. package/dist/test-helpers/runner-contract.d.mts +11 -0
  191. package/dist/test-helpers/runner-contract.mjs +32 -0
  192. package/dist/test-helpers/runner-contract.mjs.map +1 -0
  193. package/dist/test-helpers/sql-chunks.mjs +138 -0
  194. package/dist/test-helpers/sql-chunks.mjs.map +1 -0
  195. package/dist/test-helpers/storage-contract.d.mts +11 -0
  196. package/dist/test-helpers/storage-contract.mjs +87 -0
  197. package/dist/test-helpers/storage-contract.mjs.map +1 -0
  198. package/dist/test-helpers.d.mts +13 -0
  199. package/dist/test-helpers.mjs +14 -0
  200. package/dist/tracing.mjs +63 -0
  201. package/dist/tracing.mjs.map +1 -0
  202. package/dist/types.d.mts +38 -5
  203. package/dist/types.mjs +18 -5
  204. package/dist/types.mjs.map +1 -1
  205. package/dist/urls.d.mts +12 -1
  206. package/dist/urls.mjs +22 -3
  207. package/dist/urls.mjs.map +1 -1
  208. package/dist/utils/encrypt.d.mts +48 -0
  209. package/dist/utils/encrypt.mjs +67 -13
  210. package/dist/utils/encrypt.mjs.map +1 -1
  211. package/dist/utils/hash.d.mts +17 -0
  212. package/dist/utils/hash.mjs +8 -2
  213. package/dist/utils/hash.mjs.map +1 -1
  214. package/dist/utils/http.d.mts +58 -0
  215. package/dist/utils/http.mjs +147 -0
  216. package/dist/utils/http.mjs.map +1 -0
  217. package/dist/utils/mime.d.mts +9 -0
  218. package/dist/utils/mime.mjs +33 -0
  219. package/dist/utils/mime.mjs.map +1 -0
  220. package/dist/utils/path-security.d.mts +12 -0
  221. package/dist/utils/path-security.mjs +24 -0
  222. package/dist/utils/path-security.mjs.map +1 -0
  223. package/dist/utils/redact.d.mts +17 -0
  224. package/dist/utils/redact.mjs +42 -0
  225. package/dist/utils/redact.mjs.map +1 -0
  226. package/dist/utils/timing.d.mts +19 -0
  227. package/dist/utils/timing.mjs +46 -0
  228. package/dist/utils/timing.mjs.map +1 -0
  229. package/dist/utils/ulid.d.mts +10 -0
  230. package/dist/utils/ulid.mjs +8 -8
  231. package/dist/utils/ulid.mjs.map +1 -1
  232. package/dist/utils.d.mts +12 -0
  233. package/dist/utils.mjs +13 -0
  234. package/package.json +69 -28
  235. package/dist/adapter/auth.d.mts +0 -50
  236. package/dist/adapter/auth.mjs +0 -10
  237. package/dist/adapter/auth.mjs.map +0 -1
  238. package/dist/adapter/init.d.mts +0 -58
  239. package/dist/adapter/init.mjs +0 -93
  240. package/dist/adapter/init.mjs.map +0 -1
  241. package/dist/adapters/webhook-events.mjs +0 -55
  242. package/dist/adapters/webhook-events.mjs.map +0 -1
  243. package/dist/ddl.d.mts +0 -8
  244. package/dist/ddl.mjs +0 -158
  245. package/dist/ddl.mjs.map +0 -1
  246. package/dist/index.mjs.map +0 -1
  247. package/dist/middleware/auth-gate.mjs +0 -36
  248. package/dist/middleware/auth-gate.mjs.map +0 -1
  249. package/dist/middleware/csrf.mjs +0 -40
  250. package/dist/middleware/csrf.mjs.map +0 -1
  251. package/dist/middleware/rate-limit.mjs +0 -39
  252. package/dist/middleware/rate-limit.mjs.map +0 -1
  253. package/dist/middleware/request-log.mjs +0 -28
  254. package/dist/middleware/request-log.mjs.map +0 -1
  255. package/dist/middleware/store-scope.mjs +0 -29
  256. package/dist/middleware/store-scope.mjs.map +0 -1
  257. package/dist/pages/build-detail.mjs +0 -339
  258. package/dist/pages/build-detail.mjs.map +0 -1
  259. package/dist/pages/build-diff-comments.mjs +0 -113
  260. package/dist/pages/build-diff-comments.mjs.map +0 -1
  261. package/dist/pages/build-diff-header.mjs +0 -163
  262. package/dist/pages/build-diff-header.mjs.map +0 -1
  263. package/dist/pages/build-diff-nav.mjs +0 -66
  264. package/dist/pages/build-diff-nav.mjs.map +0 -1
  265. package/dist/pages/build-diff-viewer.mjs +0 -197
  266. package/dist/pages/build-diff-viewer.mjs.map +0 -1
  267. package/dist/pages/build-diff.mjs +0 -90
  268. package/dist/pages/build-diff.mjs.map +0 -1
  269. package/dist/pages/compute-jobs.mjs +0 -198
  270. package/dist/pages/compute-jobs.mjs.map +0 -1
  271. package/dist/pages/label-detail.mjs +0 -228
  272. package/dist/pages/label-detail.mjs.map +0 -1
  273. package/dist/pages/login.mjs +0 -78
  274. package/dist/pages/login.mjs.map +0 -1
  275. package/dist/pages/project-builds.mjs +0 -231
  276. package/dist/pages/project-builds.mjs.map +0 -1
  277. package/dist/pages/project-create.mjs +0 -169
  278. package/dist/pages/project-create.mjs.map +0 -1
  279. package/dist/pages/project-settings.mjs +0 -102
  280. package/dist/pages/project-settings.mjs.map +0 -1
  281. package/dist/pages/projects.mjs +0 -184
  282. package/dist/pages/projects.mjs.map +0 -1
  283. package/dist/pages/root.mjs +0 -94
  284. package/dist/pages/root.mjs.map +0 -1
  285. package/dist/pages/settings-general.mjs +0 -192
  286. package/dist/pages/settings-general.mjs.map +0 -1
  287. package/dist/pages/settings-labels.mjs +0 -150
  288. package/dist/pages/settings-labels.mjs.map +0 -1
  289. package/dist/pages/settings-members.mjs +0 -125
  290. package/dist/pages/settings-members.mjs.map +0 -1
  291. package/dist/pages/settings-status.mjs +0 -152
  292. package/dist/pages/settings-status.mjs.map +0 -1
  293. package/dist/pages/settings-tests.mjs +0 -149
  294. package/dist/pages/settings-tests.mjs.map +0 -1
  295. package/dist/pages/settings-tokens.mjs +0 -96
  296. package/dist/pages/settings-tokens.mjs.map +0 -1
  297. package/dist/pages/settings-webhooks.mjs +0 -150
  298. package/dist/pages/settings-webhooks.mjs.map +0 -1
  299. package/dist/pages/storybook.mjs +0 -96
  300. package/dist/pages/storybook.mjs.map +0 -1
  301. package/dist/retention/purge.mjs +0 -70
  302. package/dist/retention/purge.mjs.map +0 -1
  303. package/dist/routers/admin.mjs +0 -42
  304. package/dist/routers/admin.mjs.map +0 -1
  305. package/dist/routers/assets.mjs +0 -25
  306. package/dist/routers/assets.mjs.map +0 -1
  307. package/dist/routers/auth.mjs +0 -78
  308. package/dist/routers/auth.mjs.map +0 -1
  309. package/dist/routers/builds.handlers.mjs +0 -184
  310. package/dist/routers/builds.handlers.mjs.map +0 -1
  311. package/dist/routers/builds.mjs +0 -185
  312. package/dist/routers/builds.mjs.map +0 -1
  313. package/dist/routers/comments.mjs +0 -90
  314. package/dist/routers/comments.mjs.map +0 -1
  315. package/dist/routers/health.mjs +0 -137
  316. package/dist/routers/health.mjs.map +0 -1
  317. package/dist/routers/helpers.mjs +0 -82
  318. package/dist/routers/helpers.mjs.map +0 -1
  319. package/dist/routers/htmx.mjs +0 -20
  320. package/dist/routers/htmx.mjs.map +0 -1
  321. package/dist/routers/labels.mjs +0 -110
  322. package/dist/routers/labels.mjs.map +0 -1
  323. package/dist/routers/media.mjs +0 -55
  324. package/dist/routers/media.mjs.map +0 -1
  325. package/dist/routers/members.mjs +0 -102
  326. package/dist/routers/members.mjs.map +0 -1
  327. package/dist/routers/projects.mjs +0 -122
  328. package/dist/routers/projects.mjs.map +0 -1
  329. package/dist/routers/schemas.mjs +0 -224
  330. package/dist/routers/schemas.mjs.map +0 -1
  331. package/dist/routers/settings-general.mjs +0 -95
  332. package/dist/routers/settings-general.mjs.map +0 -1
  333. package/dist/routers/settings-labels.mjs +0 -49
  334. package/dist/routers/settings-labels.mjs.map +0 -1
  335. package/dist/routers/settings-members.mjs +0 -30
  336. package/dist/routers/settings-members.mjs.map +0 -1
  337. package/dist/routers/settings-status.mjs +0 -73
  338. package/dist/routers/settings-status.mjs.map +0 -1
  339. package/dist/routers/settings-tokens.mjs +0 -40
  340. package/dist/routers/settings-tokens.mjs.map +0 -1
  341. package/dist/routers/settings-webhooks.mjs +0 -63
  342. package/dist/routers/settings-webhooks.mjs.map +0 -1
  343. package/dist/routers/settings.handlers.mjs +0 -93
  344. package/dist/routers/settings.handlers.mjs.map +0 -1
  345. package/dist/routers/settings.mjs +0 -23
  346. package/dist/routers/settings.mjs.map +0 -1
  347. package/dist/routers/snapshots.mjs +0 -144
  348. package/dist/routers/snapshots.mjs.map +0 -1
  349. package/dist/routers/status-configs.mjs +0 -101
  350. package/dist/routers/status-configs.mjs.map +0 -1
  351. package/dist/routers/storybook.mjs +0 -120
  352. package/dist/routers/storybook.mjs.map +0 -1
  353. package/dist/routers/tokens.mjs +0 -81
  354. package/dist/routers/tokens.mjs.map +0 -1
  355. package/dist/routers/ui.mjs +0 -165
  356. package/dist/routers/ui.mjs.map +0 -1
  357. package/dist/routers/webhooks.mjs +0 -90
  358. package/dist/routers/webhooks.mjs.map +0 -1
  359. package/dist/schema/baseline.mjs +0 -22
  360. package/dist/schema/baseline.mjs.map +0 -1
  361. package/dist/schema/build.mjs +0 -29
  362. package/dist/schema/build.mjs.map +0 -1
  363. package/dist/schema/comment.mjs +0 -26
  364. package/dist/schema/comment.mjs.map +0 -1
  365. package/dist/schema/label.mjs +0 -30
  366. package/dist/schema/label.mjs.map +0 -1
  367. package/dist/schema/member.mjs +0 -19
  368. package/dist/schema/member.mjs.map +0 -1
  369. package/dist/schema/project.mjs +0 -25
  370. package/dist/schema/project.mjs.map +0 -1
  371. package/dist/schema/snapshot.mjs +0 -34
  372. package/dist/schema/snapshot.mjs.map +0 -1
  373. package/dist/schema/status-config.mjs +0 -20
  374. package/dist/schema/status-config.mjs.map +0 -1
  375. package/dist/schema/token.mjs +0 -19
  376. package/dist/schema/token.mjs.map +0 -1
  377. package/dist/schema/user.mjs +0 -19
  378. package/dist/schema/user.mjs.map +0 -1
  379. package/dist/schema/webhook.mjs +0 -20
  380. package/dist/schema/webhook.mjs.map +0 -1
  381. package/dist/schema.mjs.map +0 -1
  382. package/dist/store.mjs +0 -20
  383. package/dist/store.mjs.map +0 -1
  384. package/dist/ui/document.mjs +0 -348
  385. package/dist/ui/document.mjs.map +0 -1
  386. package/dist/ui/feedback.mjs +0 -24
  387. package/dist/ui/feedback.mjs.map +0 -1
  388. package/dist/ui/forms.mjs +0 -117
  389. package/dist/ui/forms.mjs.map +0 -1
  390. package/dist/ui/styles.mjs +0 -174
  391. package/dist/ui/styles.mjs.map +0 -1
  392. package/dist/ui/theme.mjs +0 -44
  393. package/dist/ui/theme.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrator.mjs","names":[],"sources":["../../src/capture/orchestrator.ts"],"sourcesContent":["import type { Logger } from \"pino\";\nimport type { CaptureRunner } from \"../adapters/capture-runner.ts\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { DEFAULT_VIEWPORTS, isDisabledStory, isFlakyStory } from \"./adapter.ts\";\nimport type { Viewport } from \"./adapter.ts\";\nimport { persistCapture } from \"./pipeline.ts\";\nimport { extractStorybookToScratch, persistStorybookStatics } from \"./statics.ts\";\nimport { StorybookAdapter } from \"./storybook.ts\";\n\n/** Inputs for running a capture job against a Storybook build. */\nexport interface CaptureJobOptions {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n runner: CaptureRunner;\n scratchDir: string;\n viewports?: Viewport[];\n logger?: Logger;\n /** Server secret for decrypting webhook secrets at send time. */\n secret?: string | undefined;\n}\n/**\n * Run the full capture for a build: extract, render, persist, and finalize.\n *\n * @param input - Build id plus the originating request id.\n * @param options - Adapters, scratch dir, viewports, and logger.\n */\nexport async function executeCaptureJob(\n input: { buildId: string; reqId?: string },\n options: CaptureJobOptions,\n): Promise<void> {\n const builds = new BuildModel(options.db);\n const { build, project } = await loadTarget(options, input.buildId);\n const logger = options.logger?.child({ buildId: input.buildId, reqId: input.reqId });\n await builds.setStatus(build.id, \"capturing\");\n\n const startTime = performance.now();\n let extractedDir: string | undefined;\n try {\n const extractStart = performance.now();\n extractedDir = await extractStorybookToScratch(\n options.storage,\n options.scratchDir,\n project.id,\n build.id,\n );\n const extractDuration = performance.now() - extractStart;\n logger?.info({ durationMs: Math.round(extractDuration) }, \"storybook extracted\");\n\n // Persist the extracted statics to storage so the published Storybook\n // (`storybookDir`) can be served after the scratch dir is cleaned up.\n const staticsStart = performance.now();\n await persistStorybookStatics(options.storage, extractedDir, project.id, build.id);\n logger?.info(\n { durationMs: Math.round(performance.now() - staticsStart) },\n \"storybook statics persisted\",\n );\n\n const adapter = new StorybookAdapter();\n const discovered = await adapter.discover(extractedDir);\n const stories = discovered.filter((s) => !isDisabledStory(s));\n const viewports = options.viewports ?? DEFAULT_VIEWPORTS;\n\n const renderStart = performance.now();\n const result = await options.runner.render({\n buildId: build.id,\n storybookDir: extractedDir,\n stories,\n viewports,\n logger,\n executePlay: project.executePlay ?? false,\n playTimeoutMs: project.playTimeoutMs ?? 10_000,\n });\n const renderDuration = performance.now() - renderStart;\n logger?.info(\n { durationMs: Math.round(renderDuration), storyCount: stories.length },\n \"stories rendered\",\n );\n\n const flakyStoryIds = new Set(stories.filter((s) => isFlakyStory(s)).map((s) => s.id));\n const blockingFailed = new Set<string>();\n const flakyFailed = new Set<string>();\n for (const f of result.failures) {\n if (flakyStoryIds.has(f.storyId)) flakyFailed.add(f.storyId);\n else blockingFailed.add(f.storyId);\n }\n\n const persistStart = performance.now();\n await persistCapture(\n {\n db: options.db,\n storage: options.storage,\n project,\n build,\n viewports,\n captures: result.captures,\n logger,\n secret: options.secret,\n },\n blockingFailed,\n flakyFailed,\n );\n const persistDuration = performance.now() - persistStart;\n logger?.info({ durationMs: Math.round(persistDuration) }, \"capture persisted\");\n\n const totalDuration = performance.now() - startTime;\n logger?.info({ durationMs: Math.round(totalDuration) }, \"capture completed\");\n } catch (error) {\n const totalDuration = performance.now() - startTime;\n logger?.error({ durationMs: Math.round(totalDuration), err: error }, \"capture failed\");\n await builds.setStatus(build.id, \"failed\").catch((markError: unknown) => {\n logger?.error({ err: markError }, \"failed to mark build failed after capture error\");\n });\n throw error;\n }\n}\n\nasync function loadTarget(\n options: CaptureJobOptions,\n buildId: string,\n): Promise<{ build: Build; project: Project }> {\n const build = await new BuildModel(options.db).get(buildId);\n if (!build) {\n throw new Error(`Build not found: ${buildId}`);\n }\n const project = await new ProjectModel(options.db).get(build.projectId);\n if (!project) {\n throw new Error(`Project not found: ${build.projectId}`);\n }\n return { build, project };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"orchestrator.mjs","names":[],"sources":["../../src/capture/orchestrator.ts"],"sourcesContent":["import type { Logger } from \"pino\";\nimport type { CaptureRunner, RenderResult } from \"../adapters/capture-runner.ts\";\nimport type { BrowserName } from \"../adapters/capture-runner.ts\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { BuildModel, type BuildTables } from \"../models/build.ts\";\nimport {\n emitAttemptLog,\n type AttemptLogRecorder,\n type CaptureAttemptTables,\n} from \"../models/capture-attempt.ts\";\nimport type { CaptureLogTables } from \"../models/capture-log.ts\";\nimport { ProjectModel, type ProjectTables } from \"../models/project.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { parentContext, withSpan } from \"../tracing.ts\";\nimport type { SecretInput } from \"../utils/encrypt.ts\";\nimport { roundedTimings, runWithTimings, timed } from \"../utils/timing.ts\";\nimport { DEFAULT_VIEWPORTS, isDisabledStory, isFlakyStory } from \"./adapter.ts\";\nimport type { Viewport } from \"./adapter.ts\";\nimport { partitionAffectedStories } from \"./affected.ts\";\nimport { persistCapture, type PipelineTables } from \"./pipeline.ts\";\nimport { resolveViewports } from \"./sizing.ts\";\nimport { extractStorybookToScratch, persistStorybookStatics } from \"./statics.ts\";\nimport { StorybookAdapter } from \"./storybook.ts\";\n\n/** Table handles required by the orchestrator. */\nexport type OrchestratorTables = BuildTables &\n ProjectTables &\n PipelineTables &\n CaptureAttemptTables &\n CaptureLogTables;\n\n/** Inputs for running a capture job against a Storybook build. */\nexport interface CaptureJobOptions {\n db: DatabaseAdapter;\n tables: OrchestratorTables;\n storage: StorageAdapter;\n runner: CaptureRunner;\n scratchDir: string;\n viewports?: Viewport[];\n logger?: Logger;\n /** Server secret for decrypting webhook secrets at send time. */\n secret?: SecretInput;\n /** Render budget override (tests use a short fuse). */\n renderTimeoutMs?: number | undefined;\n}\n\n/** Default budget for one render call before the orchestrator cancels it. */\nexport const RENDER_TIMEOUT_MS = 15 * 60_000;\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. */\ninterface RenderRequest {\n buildId: string;\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 browser: BrowserName;\n}\n\n/** Cap failure error text so one chatty runner cannot flood rows and logs. */\nfunction capFailures(result: RenderResult): RenderResult {\n return {\n captures: result.captures,\n failures: result.failures.map((failure) => ({\n ...failure,\n error:\n failure.error.length > FAILURE_ERROR_MAX\n ? failure.error.slice(0, FAILURE_ERROR_MAX)\n : failure.error,\n })),\n };\n}\n\n/** Render with a timeout; cancels the runner when the budget expires. */\nasync 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 await runner.cancel(request.buildId).catch(() => {});\n }\n throw error;\n } finally {\n clearTimeout(timer);\n }\n}\n\n/** Pointer to the attempt row owning this run's log history. */\nexport interface CaptureAttemptRef {\n id: string;\n attemptNo: number;\n projectId: string;\n}\n\n/** Inputs for running a capture job against a Storybook build. */\nexport interface CaptureJobInput {\n buildId: string;\n reqId?: string;\n /**\n * W3C `traceparent` from the enqueueing request or queue payload.\n * Continues the inbound trace instead of starting a root span.\n */\n traceparent?: string;\n /** Attempt row owning this run; when omitted no per-attempt logs are stored. */\n attempt?: CaptureAttemptRef;\n /** Recorder mirroring phase logs into the attempt's log history. */\n recordLog?: AttemptLogRecorder;\n}\n/**\n * Run the full capture for a build: extract, render, persist, and finalize.\n *\n * The run executes inside a `capture.job` span (continuing `input.traceparent`\n * when present) with one child span per pipeline phase.\n *\n * @param input - Build id plus the originating request id.\n * @param options - Adapters, scratch dir, viewports, and logger.\n * @returns Story and blocking-failure counts for the attempt row.\n */\nexport async function executeCaptureJob(\n input: CaptureJobInput,\n options: CaptureJobOptions,\n): Promise<{ storyCount: number; failedCount: number }> {\n const builds = new BuildModel(options.db, options.tables);\n const { build, project } = await loadTarget(options, input.buildId);\n return await withSpan(\n \"capture.job\",\n async () => {\n const { result, timings } = await runWithTimings(\n async () => await runCapturePhases(input, options, builds, build, project),\n );\n options.logger\n ?.child({ buildId: build.id, ...(input.reqId ? { reqId: input.reqId } : {}) })\n ?.info({ timings: roundedTimings(timings) }, \"capture timings\");\n return result;\n },\n {\n \"storyshelf.build_id\": build.id,\n ...(input.reqId ? { \"storyshelf.req_id\": input.reqId } : {}),\n ...(input.attempt ? { \"storyshelf.attempt_no\": input.attempt.attemptNo } : {}),\n },\n parentContext(input.traceparent),\n );\n}\n\n/** Execute the phase pipeline inside the `capture.job` span. */\nasync function runCapturePhases(\n input: CaptureJobInput,\n options: CaptureJobOptions,\n builds: BuildModel,\n build: Build,\n project: Project,\n): Promise<{ storyCount: number; failedCount: number }> {\n const logger = options.logger?.child({\n buildId: input.buildId,\n reqId: input.reqId,\n ...(input.attempt ? { attemptNo: input.attempt.attemptNo } : {}),\n });\n await builds.setStatus(build.id, \"capturing\");\n\n const startTime = performance.now();\n let extractedDir: string | undefined;\n try {\n const extractStart = performance.now();\n extractedDir = await timed(\n \"capture.extract\",\n async () =>\n await withSpan(\"capture.extract\", async () => {\n return await extractStorybookToScratch(\n options.storage,\n options.scratchDir,\n project.id,\n build.id,\n );\n }),\n );\n const extractDuration = performance.now() - extractStart;\n logger?.info({ durationMs: Math.round(extractDuration) }, \"storybook extracted\");\n await emitAttemptLog(input.recordLog, logger, \"info\", \"storybook extracted\", {\n durationMs: Math.round(extractDuration),\n });\n\n // Persist the extracted statics to storage so the published Storybook\n // (`storybookDir`) can be served after the scratch dir is cleaned up.\n // Copy to a const: closures below need the narrowed string type.\n const storybookDir: string = extractedDir;\n const staticsStart = performance.now();\n await timed(\n \"capture.persist-statics\",\n async () =>\n await withSpan(\"capture.persist-statics\", async () => {\n await persistStorybookStatics(options.storage, storybookDir, project.id, build.id);\n }),\n );\n logger?.info(\n { durationMs: Math.round(performance.now() - staticsStart) },\n \"storybook statics persisted\",\n );\n await emitAttemptLog(input.recordLog, logger, \"info\", \"storybook statics persisted\", {\n durationMs: Math.round(performance.now() - staticsStart),\n });\n\n const adapter = new StorybookAdapter();\n const discovered = await adapter.discover(extractedDir);\n const stories = discovered.filter((s) => !isDisabledStory(s));\n // Resolve viewports via single helper (project JSON → server override → default)\n const rawViewports = (project as unknown as { viewports?: string | null }).viewports;\n const viewports = resolveViewports(rawViewports, DEFAULT_VIEWPORTS, options.viewports);\n const browser = (project as unknown as { browser?: string }).browser as BrowserName | undefined;\n\n const partition = await partitionAffectedStories({\n db: options.db,\n tables: options.tables,\n projectId: project.id,\n branch: build.gitBranch,\n defaultBranch: project.gitDefaultBranch,\n stories,\n viewports,\n affectedPaths: affectedPathsFor(build),\n });\n logger?.info(\n {\n rendered: partition.render.length,\n inherited: partition.inherited.length,\n total: stories.length,\n },\n \"affected capture partitioned\",\n );\n await emitAttemptLog(input.recordLog, logger, \"info\", \"affected capture partitioned\", {\n rendered: partition.render.length,\n inherited: partition.inherited.length,\n total: stories.length,\n });\n\n const renderStart = performance.now();\n const renderTimeoutMs = options.renderTimeoutMs ?? RENDER_TIMEOUT_MS;\n const result = await timed(\n \"capture.render\",\n async () =>\n await withSpan(\n \"capture.render\",\n async () => {\n if (partition.render.length === 0) {\n return { captures: [], failures: [] };\n }\n return await renderWithTimeout(\n options.runner,\n {\n buildId: build.id,\n storybookDir,\n stories: partition.render,\n viewports,\n logger,\n executePlay: project.executePlay ?? false,\n playTimeoutMs: project.playTimeoutMs ?? 10_000,\n runA11y: project.runA11y ?? false,\n browser: browser ?? \"chromium\",\n },\n renderTimeoutMs,\n );\n },\n { \"storyshelf.render_count\": partition.render.length },\n ),\n );\n const renderDuration = performance.now() - renderStart;\n logger?.info(\n { durationMs: Math.round(renderDuration), storyCount: stories.length },\n \"stories rendered\",\n );\n await emitAttemptLog(input.recordLog, logger, \"info\", \"stories rendered\", {\n durationMs: Math.round(renderDuration),\n storyCount: stories.length,\n });\n\n const flakyStoryIds = new Set(stories.filter((s) => isFlakyStory(s)).map((s) => s.id));\n const blockingFailed = new Set<string>();\n const flakyFailed = new Set<string>();\n const a11yFailed = new Set<string>();\n for (const f of result.failures) {\n if (f.error.startsWith(\"a11y:\")) a11yFailed.add(f.storyId);\n else if (flakyStoryIds.has(f.storyId)) flakyFailed.add(f.storyId);\n else blockingFailed.add(f.storyId);\n }\n\n const persistStart = performance.now();\n await timed(\n \"capture.persist\",\n async () =>\n await withSpan(\"capture.persist\", async () => {\n await persistCapture(\n {\n db: options.db,\n tables: options.tables,\n storage: options.storage,\n project,\n build,\n viewports,\n captures: result.captures,\n inherited: partition.inherited,\n logger,\n recordLog: input.recordLog,\n secret: options.secret,\n },\n blockingFailed,\n flakyFailed,\n a11yFailed,\n );\n }),\n );\n const persistDuration = performance.now() - persistStart;\n logger?.info({ durationMs: Math.round(persistDuration) }, \"capture persisted\");\n await emitAttemptLog(input.recordLog, logger, \"info\", \"capture persisted\", {\n durationMs: Math.round(persistDuration),\n });\n\n const totalDuration = performance.now() - startTime;\n logger?.info({ durationMs: Math.round(totalDuration) }, \"capture completed\");\n await emitAttemptLog(input.recordLog, logger, \"info\", \"capture completed\", {\n durationMs: Math.round(totalDuration),\n });\n return { storyCount: stories.length, failedCount: blockingFailed.size };\n } catch (error) {\n const totalDuration = performance.now() - startTime;\n logger?.error({ durationMs: Math.round(totalDuration), err: error }, \"capture failed\");\n await emitAttemptLog(input.recordLog, logger, \"error\", \"capture failed\", {\n durationMs: Math.round(totalDuration),\n });\n await builds.setStatus(build.id, \"failed\").catch((markError: unknown) => {\n logger?.error({ err: markError }, \"failed to mark build failed after capture error\");\n });\n throw error;\n }\n}\n\n/**\n * Resolve the affected import paths for a build, or `null` for full capture\n * (affected capture disabled, or no computation was posted).\n */\nfunction affectedPathsFor(build: Build): string[] | null {\n if (!build.affectedOnly) {\n return null;\n }\n return BuildModel.affectedPaths(build);\n}\n\nasync function loadTarget(\n options: CaptureJobOptions,\n buildId: string,\n): Promise<{ build: Build; project: Project }> {\n const build = await new BuildModel(options.db, options.tables).get(buildId);\n if (!build) {\n throw new Error(`Build not found: ${buildId}`);\n }\n const project = await new ProjectModel(options.db, options.tables).get(build.projectId);\n if (!project) {\n throw new Error(`Project not found: ${build.projectId}`);\n }\n return { build, project };\n}\n"],"mappings":"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"}
@@ -4,14 +4,38 @@ import { Viewport } from "./adapter.mjs";
4
4
  import { RenderedSnapshot } from "../adapter/capture-runner.mjs";
5
5
  import { DatabaseAdapter } from "../adapter/database.mjs";
6
6
  import { StorageAdapter } from "../adapter/storage.mjs";
7
+ import { SecretInput } from "../utils/encrypt.mjs";
7
8
  import { Build } from "../schema/build.mjs";
9
+ import { BuildTables } from "../models/build.mjs";
10
+ import { AttemptLogRecorder } from "../models/capture-attempt.mjs";
8
11
  import { Project } from "../schema/project.mjs";
12
+ import { BaselineTables } from "../models/baseline.mjs";
13
+ import { SnapshotTables } from "../models/snapshot.mjs";
14
+ import { InheritedStory } from "./affected.mjs";
9
15
  import { Logger } from "pino";
10
16
  //#region src/capture/pipeline.d.ts
17
+ /**
18
+ * Persist a completed capture run: write screenshots, diff against the branch
19
+ * baseline, create snapshots, and finalize the build.
20
+ *
21
+ * Rendering is performed by a pure `CaptureRunner`; this function owns only
22
+ * storage writes and record keeping, so it can run against any renderer.
23
+ *
24
+ * @param ctx - Capture context.
25
+ * @param renderFailedStoryIds - Story ids that the renderer could not capture
26
+ * (blocking failures). They force the build to `failed`.
27
+ * @param flakyFailedStoryIds - Story ids that failed but are marked flaky
28
+ * (non-blocking). They are logged as warnings and do not block the build.
29
+ */
30
+ declare function persistCapture(ctx: CaptureContext, renderFailedStoryIds?: ReadonlySet<string>, flakyFailedStoryIds?: ReadonlySet<string>, a11yFailedStoryIds?: ReadonlySet<string>): Promise<void>;
31
+ /** Table handles required by the capture pipeline. */
32
+ type PipelineTables = BuildTables & BaselineTables & SnapshotTables;
11
33
  /** Persistence inputs for a completed capture run. */
12
34
  interface CaptureContext {
13
35
  /** Database adapter. */
14
36
  db: DatabaseAdapter;
37
+ /** Table handles. */
38
+ tables: PipelineTables;
15
39
  /** Storage adapter. */
16
40
  storage: StorageAdapter;
17
41
  /** The project being captured. */
@@ -22,25 +46,15 @@ interface CaptureContext {
22
46
  viewports: Viewport[];
23
47
  /** Screenshot buffers produced by the capture renderer. */
24
48
  captures: RenderedSnapshot[];
49
+ /** Stories inheriting their baseline without rendering (affected capture). */
50
+ inherited?: InheritedStory[];
25
51
  /** Optional logger for capture diagnostics. */
26
52
  logger?: Logger;
53
+ /** Optional per-attempt log recorder (mirrors logger lines into the DB). */
54
+ recordLog?: AttemptLogRecorder;
27
55
  /** Server secret for decrypting webhook secrets at send time. */
28
- secret?: string | undefined;
56
+ secret?: SecretInput;
29
57
  }
30
- /**
31
- * Persist a completed capture run: write screenshots, diff against the branch
32
- * baseline, create snapshots, and finalize the build.
33
- *
34
- * Rendering is performed by a pure `CaptureRunner`; this function owns only
35
- * storage writes and record keeping, so it can run against any renderer.
36
- *
37
- * @param ctx - Capture context.
38
- * @param renderFailedStoryIds - Story ids that the renderer could not capture
39
- * (blocking failures). They force the build to `failed`.
40
- * @param flakyFailedStoryIds - Story ids that failed but are marked flaky
41
- * (non-blocking). They are logged as warnings and do not block the build.
42
- */
43
- declare function persistCapture(ctx: CaptureContext, renderFailedStoryIds?: ReadonlySet<string>, flakyFailedStoryIds?: ReadonlySet<string>): Promise<void>;
44
58
  //#endregion
45
- export { CaptureContext, persistCapture };
59
+ export { CaptureContext, PipelineTables, persistCapture };
46
60
  //# sourceMappingURL=pipeline.d.mts.map
@@ -2,11 +2,14 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { BuildModel } from "../models/build.mjs";
5
- import { diffImages } from "../diff/engine.mjs";
6
- import { DEFAULT_DIFF_OPTIONS } from "../diff/options.mjs";
5
+ import { emitAttemptLog } from "../models/capture-attempt.mjs";
7
6
  import { diffPath, screenshotPath } from "../paths.mjs";
8
7
  import { BaselineModel } from "../models/baseline.mjs";
8
+ import { NO_BASELINE } from "../models/baseline-status.mjs";
9
9
  import { SnapshotModel } from "../models/snapshot.mjs";
10
+ import { diffStoredScreenshots, writeDiffOverlay } from "./diff-record.mjs";
11
+ import { assertScreenshotBuffer } from "./guards.mjs";
12
+ import { SIZING_DEFAULTS, infraHashFor } from "./sizing.mjs";
10
13
  //#region src/capture/pipeline.ts
11
14
  /**
12
15
  * Persist a completed capture run: write screenshots, diff against the branch
@@ -21,30 +24,104 @@ import { SnapshotModel } from "../models/snapshot.mjs";
21
24
  * @param flakyFailedStoryIds - Story ids that failed but are marked flaky
22
25
  * (non-blocking). They are logged as warnings and do not block the build.
23
26
  */
24
- async function persistCapture(ctx, renderFailedStoryIds = /* @__PURE__ */ new Set(), flakyFailedStoryIds = /* @__PURE__ */ new Set()) {
27
+ async function persistCapture(ctx, renderFailedStoryIds = /* @__PURE__ */ new Set(), flakyFailedStoryIds = /* @__PURE__ */ new Set(), a11yFailedStoryIds = /* @__PURE__ */ new Set()) {
25
28
  const failedStoryIds = new Set(renderFailedStoryIds);
26
29
  const flakyIds = new Set(flakyFailedStoryIds);
30
+ const a11yIds = new Set(a11yFailedStoryIds);
27
31
  await Promise.all(ctx.captures.map(async (capture) => {
28
32
  try {
29
33
  await persistSnapshot(ctx, capture);
30
34
  } catch (error) {
31
- if (flakyIds.has(capture.story.id)) {
32
- flakyIds.add(capture.story.id);
35
+ if (flakyIds.has(capture.story.id) || a11yIds.has(capture.story.id)) {
36
+ if (flakyIds.has(capture.story.id)) flakyIds.add(capture.story.id);
37
+ if (a11yIds.has(capture.story.id)) a11yIds.add(capture.story.id);
33
38
  ctx.logger?.warn({
34
39
  storyId: capture.story.id,
35
40
  viewport: capture.viewportName,
36
41
  err: error
37
- }, "capture failed for flaky story (non-blocking)");
42
+ }, "capture failed for flaky/a11y story (non-blocking)");
43
+ await emitAttemptLog(ctx.recordLog, ctx.logger, "warn", "capture failed for flaky/a11y story (non-blocking)", {
44
+ storyId: capture.story.id,
45
+ viewport: capture.viewportName
46
+ });
38
47
  } else failedStoryIds.add(capture.story.id);
39
48
  ctx.logger?.error({
40
49
  storyId: capture.story.id,
41
50
  viewport: capture.viewportName,
42
51
  err: error
43
52
  }, "capture failed for story");
53
+ await emitAttemptLog(ctx.recordLog, ctx.logger, "error", "capture failed for story", {
54
+ storyId: capture.story.id,
55
+ viewport: capture.viewportName
56
+ });
44
57
  }
45
58
  }));
46
- for (const id of flakyIds) if (!failedStoryIds.has(id)) ctx.logger?.warn({ storyId: id }, "flaky story failed (non-blocking)");
47
- await finalize(ctx, new Set(ctx.captures.map((c) => c.story.id)), failedStoryIds, flakyIds);
59
+ for (const id of flakyIds) if (!failedStoryIds.has(id)) {
60
+ ctx.logger?.warn({ storyId: id }, "flaky story failed (non-blocking)");
61
+ await emitAttemptLog(ctx.recordLog, ctx.logger, "warn", "flaky story failed (non-blocking)", { storyId: id });
62
+ }
63
+ for (const id of a11yIds) if (!failedStoryIds.has(id) && !flakyIds.has(id)) {
64
+ ctx.logger?.warn({ storyId: id }, "a11y violations found (non-blocking)");
65
+ await emitAttemptLog(ctx.recordLog, ctx.logger, "warn", "a11y violations found (non-blocking)", { storyId: id });
66
+ }
67
+ for (const id of await persistInherited(ctx)) failedStoryIds.add(id);
68
+ await finalize(ctx, allStoryIds(ctx), failedStoryIds, flakyIds, a11yIds);
69
+ }
70
+ /** Persist one inherited snapshot; returns the story id on failure. */
71
+ async function tryPersistInherited(ctx, inherited) {
72
+ try {
73
+ await persistInheritedSnapshot(ctx, inherited);
74
+ return null;
75
+ } catch (error) {
76
+ ctx.logger?.error({
77
+ storyId: inherited.story.id,
78
+ viewport: inherited.viewport.name,
79
+ err: error
80
+ }, "inherit failed for story");
81
+ await emitAttemptLog(ctx.recordLog, ctx.logger, "error", "inherit failed for story", {
82
+ storyId: inherited.story.id,
83
+ viewport: inherited.viewport.name
84
+ });
85
+ return inherited.story.id;
86
+ }
87
+ }
88
+ /** Persist inherited snapshots; a failure marks the story failed. */
89
+ async function persistInherited(ctx) {
90
+ const outcomes = await Promise.all((ctx.inherited ?? []).map(async (inherited) => await tryPersistInherited(ctx, inherited)));
91
+ return new Set(outcomes.filter((id) => id !== null));
92
+ }
93
+ /** Create an unchanged snapshot reusing the baseline screenshot. */
94
+ async function persistInheritedSnapshot(ctx, inherited) {
95
+ const infraHash = infraHashFor(ctx.project.browser ?? "chromium", ctx.viewports, SIZING_DEFAULTS);
96
+ const snapshots = new SnapshotModel(ctx.db, ctx.tables);
97
+ const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {
98
+ storyId: inherited.story.id,
99
+ storyName: inherited.story.name,
100
+ storyTitle: inherited.story.title,
101
+ storyImportPath: inherited.story.importPath ?? "",
102
+ viewportName: inherited.viewport.name,
103
+ viewportWidth: inherited.viewport.width,
104
+ viewportHeight: inherited.viewport.height,
105
+ screenshotPath: inherited.baseline.screenshotPath,
106
+ infraHash,
107
+ inherited: true,
108
+ baselineId: inherited.baseline.id,
109
+ baselineVersion: inherited.baseline.updatedAt
110
+ });
111
+ await snapshots.update(snapshot.id, {
112
+ status: "unchanged",
113
+ diffPixels: 0,
114
+ diffRatio: 0,
115
+ diffPassed: true,
116
+ diffPath: null,
117
+ infraHash
118
+ });
119
+ }
120
+ /** Every story covered by a capture run, rendered or inherited. */
121
+ function allStoryIds(ctx) {
122
+ const ids = new Set(ctx.captures.map((capture) => capture.story.id));
123
+ for (const inherited of ctx.inherited ?? []) ids.add(inherited.story.id);
124
+ return ids;
48
125
  }
49
126
  function viewportByName(ctx, name) {
50
127
  return ctx.viewports.find((v) => v.name === name) ?? {
@@ -54,7 +131,8 @@ function viewportByName(ctx, name) {
54
131
  };
55
132
  }
56
133
  async function persistSnapshot(ctx, capture) {
57
- const viewport = viewportByName(ctx, capture.viewportName);
134
+ assertScreenshotBuffer(capture.screenshot);
135
+ const viewport = capture.viewport ?? viewportByName(ctx, capture.viewportName);
58
136
  const screenshot = screenshotPath(ctx.project.id, ctx.build.id, capture.story.id, capture.viewportName);
59
137
  await ctx.storage.write(screenshot, capture.screenshot);
60
138
  const baseline = await resolveBaseline(ctx, capture.story.id, capture.viewportName);
@@ -62,14 +140,15 @@ async function persistSnapshot(ctx, capture) {
62
140
  await createWithoutBaseline(ctx, capture, viewport, screenshot);
63
141
  return;
64
142
  }
65
- await createWithBaseline(ctx, capture, viewport, screenshot, baseline.screenshotPath);
143
+ await createWithBaseline(ctx, capture, viewport, screenshot, baseline);
66
144
  }
67
145
  async function resolveBaseline(ctx, storyId, viewport) {
68
- return await new BaselineModel(ctx.db, ctx.storage, ctx.secret).resolve(ctx.project.id, storyId, viewport, ctx.build.gitBranch, ctx.project.gitDefaultBranch);
146
+ return await new BaselineModel(ctx.db, ctx.tables, ctx.storage, ctx.secret).resolve(ctx.project.id, storyId, viewport, ctx.build.gitBranch, ctx.project.gitDefaultBranch);
69
147
  }
70
148
  async function createWithoutBaseline(ctx, capture, viewport, screenshot) {
71
- const snapshots = new SnapshotModel(ctx.db);
149
+ const snapshots = new SnapshotModel(ctx.db, ctx.tables);
72
150
  const status = ctx.build.isDefault ? "approved" : "new";
151
+ const infraHash = infraHashFor(ctx.project.browser ?? "chromium", ctx.viewports, SIZING_DEFAULTS);
73
152
  const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {
74
153
  storyId: capture.story.id,
75
154
  storyName: capture.story.name,
@@ -78,21 +157,17 @@ async function createWithoutBaseline(ctx, capture, viewport, screenshot) {
78
157
  viewportName: capture.viewportName,
79
158
  viewportWidth: viewport.width,
80
159
  viewportHeight: viewport.height,
81
- screenshotPath: screenshot
160
+ screenshotPath: screenshot,
161
+ infraHash,
162
+ baselineVersion: NO_BASELINE
82
163
  });
83
164
  await snapshots.setStatus(snapshot.id, status);
84
- if (ctx.build.isDefault) await new BaselineModel(ctx.db, ctx.storage, ctx.secret).upsert(ctx.project.id, capture.story.id, capture.viewportName, ctx.build.gitBranch, snapshot.id, screenshot);
165
+ if (ctx.build.isDefault) await new BaselineModel(ctx.db, ctx.tables, ctx.storage, ctx.secret).upsert(ctx.project.id, capture.story.id, capture.viewportName, ctx.build.gitBranch, snapshot.id, screenshot, infraHash);
85
166
  }
86
- async function createWithBaseline(ctx, capture, viewport, screenshot, baselinePath) {
87
- const current = await ctx.storage.read(screenshot);
88
- const previous = await ctx.storage.read(baselinePath);
89
- const options = {
90
- ...DEFAULT_DIFF_OPTIONS,
91
- pixelThreshold: ctx.project.pixelThreshold,
92
- maxDiffRatio: ctx.project.maxDiffRatio
93
- };
94
- const result = diffImages(previous, current, options);
95
- const snapshots = new SnapshotModel(ctx.db);
167
+ async function createWithBaseline(ctx, capture, viewport, screenshot, baseline) {
168
+ const result = await diffStoredScreenshots(ctx.storage, ctx.project, baseline.screenshotPath, screenshot, capture.story.parameters?.diffThreshold);
169
+ const infraHash = infraHashFor(ctx.project.browser ?? "chromium", ctx.viewports, SIZING_DEFAULTS);
170
+ const snapshots = new SnapshotModel(ctx.db, ctx.tables);
96
171
  const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {
97
172
  storyId: capture.story.id,
98
173
  storyName: capture.story.name,
@@ -101,31 +176,60 @@ async function createWithBaseline(ctx, capture, viewport, screenshot, baselinePa
101
176
  viewportName: capture.viewportName,
102
177
  viewportWidth: viewport.width,
103
178
  viewportHeight: viewport.height,
104
- screenshotPath: screenshot
179
+ screenshotPath: screenshot,
180
+ infraHash,
181
+ baselineId: baseline.id,
182
+ baselineVersion: baseline.updatedAt
105
183
  });
106
- const status = result.passed ? "unchanged" : "changed";
184
+ const shouldAutomigrate = result.sizeChanged && !result.passed && ctx.project.automigrate && baseline.infraHash !== null && baseline.infraHash !== infraHash;
185
+ const status = shouldAutomigrate ? "unchanged" : diffStatus(result.passed, ctx.build.isDefault);
107
186
  const diff = diffPath(ctx.project.id, ctx.build.id, capture.story.id, capture.viewportName);
108
- if (!result.passed && result.diffImage) await ctx.storage.write(diff, result.diffImage);
187
+ const passed = shouldAutomigrate ? true : result.passed;
188
+ const pixels = shouldAutomigrate ? 0 : result.diffPixels;
189
+ const ratio = shouldAutomigrate ? 0 : result.diffRatio;
190
+ const overlay = await writeDiffOverlay(ctx.storage, diff, result, passed);
109
191
  await snapshots.update(snapshot.id, {
110
192
  status,
111
- diffPath: !result.passed && result.diffImage ? diff : null,
112
- diffPixels: result.diffPixels,
113
- diffRatio: result.diffRatio,
114
- diffPassed: result.passed
193
+ diffPath: overlay,
194
+ diffPixels: pixels,
195
+ diffRatio: ratio,
196
+ diffPassed: passed,
197
+ infraHash
115
198
  });
199
+ if (ctx.build.isDefault && (shouldAutomigrate || !passed)) await upsertDefaultBaseline(ctx, snapshot.id, capture, screenshot, infraHash);
200
+ }
201
+ /** Default-branch builds are authoritative: the captured screenshot becomes the baseline. */
202
+ async function upsertDefaultBaseline(ctx, snapshotId, capture, screenshot, infraHash) {
203
+ await new BaselineModel(ctx.db, ctx.tables, ctx.storage, ctx.secret).upsert(ctx.project.id, capture.story.id, capture.viewportName, ctx.build.gitBranch, snapshotId, screenshot, infraHash);
116
204
  }
117
- async function finalize(ctx, storyIds, failedStoryIds, flakyFailedStoryIds = /* @__PURE__ */ new Set()) {
118
- const builds = new BuildModel(ctx.db);
205
+ /** Snapshot status after diffing: default-branch changes are auto-approved, not queued for review. */
206
+ function diffStatus(passed, isDefault) {
207
+ if (passed) return "unchanged";
208
+ return isDefault ? "approved" : "changed";
209
+ }
210
+ /** A failed capture fails the build; default-branch builds otherwise auto-approve. */
211
+ function finalBuildStatus(isDefault, failedCount, hasCaptures, build) {
212
+ if (failedCount > 0) return "failed";
213
+ if (isDefault || hasCaptures && build.changedCount === 0) return "approved";
214
+ return "reviewing";
215
+ }
216
+ async function finalize(ctx, storyIds, failedStoryIds, flakyFailedStoryIds = /* @__PURE__ */ new Set(), a11yFailedStoryIds = /* @__PURE__ */ new Set()) {
217
+ const builds = new BuildModel(ctx.db, ctx.tables);
119
218
  const build = await builds.updateCounts(ctx.build.id);
120
219
  const hasCaptures = storyIds.size > 0;
121
- let status = "reviewing";
122
- if (failedStoryIds.size > 0) status = "failed";
123
- else if (hasCaptures && build.changedCount === 0) status = "approved";
124
- if (flakyFailedStoryIds.size > 0 && failedStoryIds.size === 0) ctx.logger?.warn({ flakyStoryIds: [...flakyFailedStoryIds] }, "flaky stories failed (non-blocking)");
220
+ const status = finalBuildStatus(ctx.build.isDefault, failedStoryIds.size, hasCaptures, build);
221
+ if (flakyFailedStoryIds.size > 0 && failedStoryIds.size === 0) {
222
+ ctx.logger?.warn({ flakyStoryIds: [...flakyFailedStoryIds] }, "flaky stories failed (non-blocking)");
223
+ await emitAttemptLog(ctx.recordLog, ctx.logger, "warn", "flaky stories failed (non-blocking)", { flakyStoryIds: [...flakyFailedStoryIds] });
224
+ }
225
+ if (a11yFailedStoryIds.size > 0 && failedStoryIds.size === 0) {
226
+ ctx.logger?.warn({ a11yStoryIds: [...a11yFailedStoryIds] }, "a11y violations found (non-blocking)");
227
+ await emitAttemptLog(ctx.recordLog, ctx.logger, "warn", "a11y violations found (non-blocking)", { a11yStoryIds: [...a11yFailedStoryIds] });
228
+ }
125
229
  await builds.setStatus(ctx.build.id, status);
126
- if (ctx.build.isDefault && hasCaptures) await new BaselineModel(ctx.db, ctx.storage, ctx.secret).removeOrphans(ctx.project.id, new Set(storyIds));
230
+ if (ctx.build.isDefault && hasCaptures) await new BaselineModel(ctx.db, ctx.tables, ctx.storage, ctx.secret).removeOrphans(ctx.project.id, new Set(storyIds));
127
231
  }
128
232
  //#endregion
129
- export { persistCapture };
233
+ export { diffStatus, persistCapture };
130
234
 
131
235
  //# sourceMappingURL=pipeline.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.mjs","names":[],"sources":["../../src/capture/pipeline.ts"],"sourcesContent":["import type { Logger } from \"pino\";\nimport type { RenderedSnapshot } from \"../adapters/capture-runner.ts\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { diffImages } from \"../diff/engine.ts\";\nimport { DEFAULT_DIFF_OPTIONS } from \"../diff/options.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.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 { BuildStatus } from \"../types.ts\";\nimport { diffPath, screenshotPath } from \"../utils/paths.ts\";\nimport type { Viewport } from \"./adapter.ts\";\n\n/** Persistence inputs for a completed capture run. */\nexport interface CaptureContext {\n /** Database adapter. */\n db: DatabaseAdapter;\n /** Storage adapter. */\n storage: StorageAdapter;\n /** The project being captured. */\n project: Project;\n /** The build being captured. */\n build: Build;\n /** Viewports at which stories were captured. */\n viewports: Viewport[];\n /** Screenshot buffers produced by the capture renderer. */\n captures: RenderedSnapshot[];\n /** Optional logger for capture diagnostics. */\n logger?: Logger;\n /** Server secret for decrypting webhook secrets at send time. */\n secret?: string | undefined;\n}\n\n/**\n * Persist a completed capture run: write screenshots, diff against the branch\n * baseline, create snapshots, and finalize the build.\n *\n * Rendering is performed by a pure `CaptureRunner`; this function owns only\n * storage writes and record keeping, so it can run against any renderer.\n *\n * @param ctx - Capture context.\n * @param renderFailedStoryIds - Story ids that the renderer could not capture\n * (blocking failures). They force the build to `failed`.\n * @param flakyFailedStoryIds - Story ids that failed but are marked flaky\n * (non-blocking). They are logged as warnings and do not block the build.\n */\nexport async function persistCapture(\n ctx: CaptureContext,\n renderFailedStoryIds: ReadonlySet<string> = new Set(),\n flakyFailedStoryIds: ReadonlySet<string> = new Set(),\n): Promise<void> {\n const failedStoryIds = new Set(renderFailedStoryIds);\n const flakyIds = new Set(flakyFailedStoryIds);\n await Promise.all(\n ctx.captures.map(async (capture) => {\n try {\n await persistSnapshot(ctx, capture);\n } catch (error) {\n // A failure in one story/viewport must not abort the other persists.\n // Flaky stories remain non-blocking even on persist failure.\n if (flakyIds.has(capture.story.id)) {\n flakyIds.add(capture.story.id);\n ctx.logger?.warn(\n { storyId: capture.story.id, viewport: capture.viewportName, err: error },\n \"capture failed for flaky story (non-blocking)\",\n );\n } else {\n failedStoryIds.add(capture.story.id);\n }\n ctx.logger?.error(\n { storyId: capture.story.id, viewport: capture.viewportName, err: error },\n \"capture failed for story\",\n );\n }\n }),\n );\n // Also log flaky render failures as warnings\n for (const id of flakyIds) {\n if (!failedStoryIds.has(id)) {\n ctx.logger?.warn({ storyId: id }, \"flaky story failed (non-blocking)\");\n }\n }\n await finalize(ctx, new Set(ctx.captures.map((c) => c.story.id)), failedStoryIds, flakyIds);\n}\n\nfunction viewportByName(ctx: CaptureContext, name: string): Viewport {\n return ctx.viewports.find((v) => v.name === name) ?? { name, width: 0, height: 0 };\n}\n\nasync function persistSnapshot(ctx: CaptureContext, capture: RenderedSnapshot): Promise<void> {\n const viewport = viewportByName(ctx, capture.viewportName);\n const screenshot = screenshotPath(\n ctx.project.id,\n ctx.build.id,\n capture.story.id,\n capture.viewportName,\n );\n await ctx.storage.write(screenshot, capture.screenshot);\n\n const baseline = await resolveBaseline(ctx, capture.story.id, capture.viewportName);\n if (!baseline) {\n await createWithoutBaseline(ctx, capture, viewport, screenshot);\n return;\n }\n await createWithBaseline(ctx, capture, viewport, screenshot, baseline.screenshotPath);\n}\n\nasync function resolveBaseline(\n ctx: CaptureContext,\n storyId: string,\n viewport: string,\n): Promise<Baseline | null> {\n const baselines = new BaselineModel(ctx.db, ctx.storage, ctx.secret);\n return await baselines.resolve(\n ctx.project.id,\n storyId,\n viewport,\n ctx.build.gitBranch,\n ctx.project.gitDefaultBranch,\n );\n}\n\nasync function createWithoutBaseline(\n ctx: CaptureContext,\n capture: RenderedSnapshot,\n viewport: Viewport,\n screenshot: string,\n): Promise<void> {\n const snapshots = new SnapshotModel(ctx.db);\n const status = ctx.build.isDefault ? \"approved\" : \"new\";\n const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {\n storyId: capture.story.id,\n storyName: capture.story.name,\n storyTitle: capture.story.title,\n storyImportPath: capture.story.importPath ?? \"\",\n viewportName: capture.viewportName,\n viewportWidth: viewport.width,\n viewportHeight: viewport.height,\n screenshotPath: screenshot,\n });\n await snapshots.setStatus(snapshot.id, status);\n\n if (ctx.build.isDefault) {\n const baselines = new BaselineModel(ctx.db, ctx.storage, ctx.secret);\n await baselines.upsert(\n ctx.project.id,\n capture.story.id,\n capture.viewportName,\n ctx.build.gitBranch,\n snapshot.id,\n screenshot,\n );\n }\n}\n\nasync function createWithBaseline(\n ctx: CaptureContext,\n capture: RenderedSnapshot,\n viewport: Viewport,\n screenshot: string,\n baselinePath: string,\n): Promise<void> {\n const current = await ctx.storage.read(screenshot);\n const previous = await ctx.storage.read(baselinePath);\n const options = {\n ...DEFAULT_DIFF_OPTIONS,\n pixelThreshold: ctx.project.pixelThreshold,\n maxDiffRatio: ctx.project.maxDiffRatio,\n };\n const result = diffImages(previous, current, options);\n\n const snapshots = new SnapshotModel(ctx.db);\n const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {\n storyId: capture.story.id,\n storyName: capture.story.name,\n storyTitle: capture.story.title,\n storyImportPath: capture.story.importPath ?? \"\",\n viewportName: capture.viewportName,\n viewportWidth: viewport.width,\n viewportHeight: viewport.height,\n screenshotPath: screenshot,\n });\n\n const status = result.passed ? \"unchanged\" : \"changed\";\n const diff = diffPath(ctx.project.id, ctx.build.id, capture.story.id, capture.viewportName);\n if (!result.passed && result.diffImage) {\n await ctx.storage.write(diff, result.diffImage);\n }\n await snapshots.update(snapshot.id, {\n status,\n diffPath: !result.passed && result.diffImage ? diff : null,\n diffPixels: result.diffPixels,\n diffRatio: result.diffRatio,\n diffPassed: result.passed,\n });\n}\n\nasync function finalize(\n ctx: CaptureContext,\n storyIds: ReadonlySet<string>,\n failedStoryIds: ReadonlySet<string>,\n flakyFailedStoryIds: ReadonlySet<string> = new Set(),\n): Promise<void> {\n const builds = new BuildModel(ctx.db);\n const build = await builds.updateCounts(ctx.build.id);\n const hasCaptures = storyIds.size > 0;\n let status: BuildStatus = \"reviewing\";\n if (failedStoryIds.size > 0) {\n status = \"failed\";\n } else if (hasCaptures && build.changedCount === 0) {\n status = \"approved\";\n }\n // Flaky failures do not block: log warning if any flaky stories failed\n if (flakyFailedStoryIds.size > 0 && failedStoryIds.size === 0) {\n ctx.logger?.warn(\n { flakyStoryIds: [...flakyFailedStoryIds] },\n \"flaky stories failed (non-blocking)\",\n );\n }\n await builds.setStatus(ctx.build.id, status);\n\n if (ctx.build.isDefault && hasCaptures) {\n const baselines = new BaselineModel(ctx.db, ctx.storage, ctx.secret);\n await baselines.removeOrphans(ctx.project.id, new Set(storyIds));\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAiDA,eAAsB,eACpB,KACA,uCAA4C,IAAI,IAAI,GACpD,sCAA2C,IAAI,IAAI,GACpC;CACf,MAAM,iBAAiB,IAAI,IAAI,oBAAoB;CACnD,MAAM,WAAW,IAAI,IAAI,mBAAmB;CAC5C,MAAM,QAAQ,IACZ,IAAI,SAAS,IAAI,OAAO,YAAY;EAClC,IAAI;GACF,MAAM,gBAAgB,KAAK,OAAO;EACpC,SAAS,OAAO;GAGd,IAAI,SAAS,IAAI,QAAQ,MAAM,EAAE,GAAG;IAClC,SAAS,IAAI,QAAQ,MAAM,EAAE;IAC7B,IAAI,QAAQ,KACV;KAAE,SAAS,QAAQ,MAAM;KAAI,UAAU,QAAQ;KAAc,KAAK;IAAM,GACxE,+CACF;GACF,OACE,eAAe,IAAI,QAAQ,MAAM,EAAE;GAErC,IAAI,QAAQ,MACV;IAAE,SAAS,QAAQ,MAAM;IAAI,UAAU,QAAQ;IAAc,KAAK;GAAM,GACxE,0BACF;EACF;CACF,CAAC,CACH;CAEA,KAAK,MAAM,MAAM,UACf,IAAI,CAAC,eAAe,IAAI,EAAE,GACxB,IAAI,QAAQ,KAAK,EAAE,SAAS,GAAG,GAAG,mCAAmC;CAGzE,MAAM,SAAS,KAAK,IAAI,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,gBAAgB,QAAQ;AAC5F;AAEA,SAAS,eAAe,KAAqB,MAAwB;CACnE,OAAO,IAAI,UAAU,MAAM,MAAM,EAAE,SAAS,IAAI,KAAK;EAAE;EAAM,OAAO;EAAG,QAAQ;CAAE;AACnF;AAEA,eAAe,gBAAgB,KAAqB,SAA0C;CAC5F,MAAM,WAAW,eAAe,KAAK,QAAQ,YAAY;CACzD,MAAM,aAAa,eACjB,IAAI,QAAQ,IACZ,IAAI,MAAM,IACV,QAAQ,MAAM,IACd,QAAQ,YACV;CACA,MAAM,IAAI,QAAQ,MAAM,YAAY,QAAQ,UAAU;CAEtD,MAAM,WAAW,MAAM,gBAAgB,KAAK,QAAQ,MAAM,IAAI,QAAQ,YAAY;CAClF,IAAI,CAAC,UAAU;EACb,MAAM,sBAAsB,KAAK,SAAS,UAAU,UAAU;EAC9D;CACF;CACA,MAAM,mBAAmB,KAAK,SAAS,UAAU,YAAY,SAAS,cAAc;AACtF;AAEA,eAAe,gBACb,KACA,SACA,UAC0B;CAE1B,OAAO,MAAM,IADS,cAAc,IAAI,IAAI,IAAI,SAAS,IAAI,MACxC,CAAC,CAAC,QACrB,IAAI,QAAQ,IACZ,SACA,UACA,IAAI,MAAM,WACV,IAAI,QAAQ,gBACd;AACF;AAEA,eAAe,sBACb,KACA,SACA,UACA,YACe;CACf,MAAM,YAAY,IAAI,cAAc,IAAI,EAAE;CAC1C,MAAM,SAAS,IAAI,MAAM,YAAY,aAAa;CAClD,MAAM,WAAW,MAAM,UAAU,OAAO,IAAI,QAAQ,IAAI,IAAI,MAAM,IAAI;EACpE,SAAS,QAAQ,MAAM;EACvB,WAAW,QAAQ,MAAM;EACzB,YAAY,QAAQ,MAAM;EAC1B,iBAAiB,QAAQ,MAAM,cAAc;EAC7C,cAAc,QAAQ;EACtB,eAAe,SAAS;EACxB,gBAAgB,SAAS;EACzB,gBAAgB;CAClB,CAAC;CACD,MAAM,UAAU,UAAU,SAAS,IAAI,MAAM;CAE7C,IAAI,IAAI,MAAM,WAEZ,MAAM,IADgB,cAAc,IAAI,IAAI,IAAI,SAAS,IAAI,MAC/C,CAAC,CAAC,OACd,IAAI,QAAQ,IACZ,QAAQ,MAAM,IACd,QAAQ,cACR,IAAI,MAAM,WACV,SAAS,IACT,UACF;AAEJ;AAEA,eAAe,mBACb,KACA,SACA,UACA,YACA,cACe;CACf,MAAM,UAAU,MAAM,IAAI,QAAQ,KAAK,UAAU;CACjD,MAAM,WAAW,MAAM,IAAI,QAAQ,KAAK,YAAY;CACpD,MAAM,UAAU;EACd,GAAG;EACH,gBAAgB,IAAI,QAAQ;EAC5B,cAAc,IAAI,QAAQ;CAC5B;CACA,MAAM,SAAS,WAAW,UAAU,SAAS,OAAO;CAEpD,MAAM,YAAY,IAAI,cAAc,IAAI,EAAE;CAC1C,MAAM,WAAW,MAAM,UAAU,OAAO,IAAI,QAAQ,IAAI,IAAI,MAAM,IAAI;EACpE,SAAS,QAAQ,MAAM;EACvB,WAAW,QAAQ,MAAM;EACzB,YAAY,QAAQ,MAAM;EAC1B,iBAAiB,QAAQ,MAAM,cAAc;EAC7C,cAAc,QAAQ;EACtB,eAAe,SAAS;EACxB,gBAAgB,SAAS;EACzB,gBAAgB;CAClB,CAAC;CAED,MAAM,SAAS,OAAO,SAAS,cAAc;CAC7C,MAAM,OAAO,SAAS,IAAI,QAAQ,IAAI,IAAI,MAAM,IAAI,QAAQ,MAAM,IAAI,QAAQ,YAAY;CAC1F,IAAI,CAAC,OAAO,UAAU,OAAO,WAC3B,MAAM,IAAI,QAAQ,MAAM,MAAM,OAAO,SAAS;CAEhD,MAAM,UAAU,OAAO,SAAS,IAAI;EAClC;EACA,UAAU,CAAC,OAAO,UAAU,OAAO,YAAY,OAAO;EACtD,YAAY,OAAO;EACnB,WAAW,OAAO;EAClB,YAAY,OAAO;CACrB,CAAC;AACH;AAEA,eAAe,SACb,KACA,UACA,gBACA,sCAA2C,IAAI,IAAI,GACpC;CACf,MAAM,SAAS,IAAI,WAAW,IAAI,EAAE;CACpC,MAAM,QAAQ,MAAM,OAAO,aAAa,IAAI,MAAM,EAAE;CACpD,MAAM,cAAc,SAAS,OAAO;CACpC,IAAI,SAAsB;CAC1B,IAAI,eAAe,OAAO,GACxB,SAAS;MACJ,IAAI,eAAe,MAAM,iBAAiB,GAC/C,SAAS;CAGX,IAAI,oBAAoB,OAAO,KAAK,eAAe,SAAS,GAC1D,IAAI,QAAQ,KACV,EAAE,eAAe,CAAC,GAAG,mBAAmB,EAAE,GAC1C,qCACF;CAEF,MAAM,OAAO,UAAU,IAAI,MAAM,IAAI,MAAM;CAE3C,IAAI,IAAI,MAAM,aAAa,aAEzB,MAAM,IADgB,cAAc,IAAI,IAAI,IAAI,SAAS,IAAI,MAC/C,CAAC,CAAC,cAAc,IAAI,QAAQ,IAAI,IAAI,IAAI,QAAQ,CAAC;AAEnE"}
1
+ {"version":3,"file":"pipeline.mjs","names":[],"sources":["../../src/capture/pipeline.ts"],"sourcesContent":["import type { Logger } from \"pino\";\nimport type { RenderedSnapshot } from \"../adapters/capture-runner.ts\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { NO_BASELINE } from \"../models/baseline-status.ts\";\nimport { BaselineModel, type BaselineTables } from \"../models/baseline.ts\";\nimport { BuildModel, type BuildTables } from \"../models/build.ts\";\nimport { emitAttemptLog, type AttemptLogRecorder } from \"../models/capture-attempt.ts\";\nimport { SnapshotModel, type SnapshotTables } 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 { BuildStatus, SnapshotStatus } from \"../types.ts\";\nimport type { SecretInput } from \"../utils/encrypt.ts\";\nimport { diffPath, screenshotPath } from \"../utils/paths.ts\";\nimport type { Viewport } from \"./adapter.ts\";\nimport type { InheritedStory } from \"./affected.ts\";\nimport { diffStoredScreenshots, writeDiffOverlay } from \"./diff-record.ts\";\nimport { assertScreenshotBuffer } from \"./guards.ts\";\nimport { infraHashFor, SIZING_DEFAULTS } from \"./sizing.ts\";\n\n/**\n * Persist a completed capture run: write screenshots, diff against the branch\n * baseline, create snapshots, and finalize the build.\n *\n * Rendering is performed by a pure `CaptureRunner`; this function owns only\n * storage writes and record keeping, so it can run against any renderer.\n *\n * @param ctx - Capture context.\n * @param renderFailedStoryIds - Story ids that the renderer could not capture\n * (blocking failures). They force the build to `failed`.\n * @param flakyFailedStoryIds - Story ids that failed but are marked flaky\n * (non-blocking). They are logged as warnings and do not block the build.\n */\nexport async function persistCapture(\n ctx: CaptureContext,\n renderFailedStoryIds: ReadonlySet<string> = new Set(),\n flakyFailedStoryIds: ReadonlySet<string> = new Set(),\n a11yFailedStoryIds: ReadonlySet<string> = new Set(),\n): Promise<void> {\n const failedStoryIds = new Set(renderFailedStoryIds);\n const flakyIds = new Set(flakyFailedStoryIds);\n const a11yIds = new Set(a11yFailedStoryIds);\n await Promise.all(\n ctx.captures.map(async (capture) => {\n try {\n await persistSnapshot(ctx, capture);\n } catch (error) {\n // A failure in one story/viewport must not abort the other persists.\n // Flaky and a11y stories remain non-blocking even on persist failure.\n if (flakyIds.has(capture.story.id) || a11yIds.has(capture.story.id)) {\n if (flakyIds.has(capture.story.id)) flakyIds.add(capture.story.id);\n if (a11yIds.has(capture.story.id)) a11yIds.add(capture.story.id);\n ctx.logger?.warn(\n { storyId: capture.story.id, viewport: capture.viewportName, err: error },\n \"capture failed for flaky/a11y story (non-blocking)\",\n );\n await emitAttemptLog(\n ctx.recordLog,\n ctx.logger,\n \"warn\",\n \"capture failed for flaky/a11y story (non-blocking)\",\n {\n storyId: capture.story.id,\n viewport: capture.viewportName,\n },\n );\n } else {\n failedStoryIds.add(capture.story.id);\n }\n ctx.logger?.error(\n { storyId: capture.story.id, viewport: capture.viewportName, err: error },\n \"capture failed for story\",\n );\n await emitAttemptLog(ctx.recordLog, ctx.logger, \"error\", \"capture failed for story\", {\n storyId: capture.story.id,\n viewport: capture.viewportName,\n });\n }\n }),\n );\n // Also log flaky and a11y render failures as warnings\n for (const id of flakyIds) {\n if (!failedStoryIds.has(id)) {\n ctx.logger?.warn({ storyId: id }, \"flaky story failed (non-blocking)\");\n await emitAttemptLog(ctx.recordLog, ctx.logger, \"warn\", \"flaky story failed (non-blocking)\", {\n storyId: id,\n });\n }\n }\n for (const id of a11yIds) {\n if (!failedStoryIds.has(id) && !flakyIds.has(id)) {\n ctx.logger?.warn({ storyId: id }, \"a11y violations found (non-blocking)\");\n await emitAttemptLog(\n ctx.recordLog,\n ctx.logger,\n \"warn\",\n \"a11y violations found (non-blocking)\",\n {\n storyId: id,\n },\n );\n }\n }\n for (const id of await persistInherited(ctx)) {\n failedStoryIds.add(id);\n }\n await finalize(ctx, allStoryIds(ctx), failedStoryIds, flakyIds, a11yIds);\n}\n\n/** Persist one inherited snapshot; returns the story id on failure. */\nasync function tryPersistInherited(\n ctx: CaptureContext,\n inherited: InheritedStory,\n): Promise<string | null> {\n try {\n await persistInheritedSnapshot(ctx, inherited);\n return null;\n } catch (error) {\n ctx.logger?.error(\n { storyId: inherited.story.id, viewport: inherited.viewport.name, err: error },\n \"inherit failed for story\",\n );\n await emitAttemptLog(ctx.recordLog, ctx.logger, \"error\", \"inherit failed for story\", {\n storyId: inherited.story.id,\n viewport: inherited.viewport.name,\n });\n return inherited.story.id;\n }\n}\n\n/** Persist inherited snapshots; a failure marks the story failed. */\nasync function persistInherited(ctx: CaptureContext): Promise<ReadonlySet<string>> {\n const outcomes = await Promise.all(\n (ctx.inherited ?? []).map(async (inherited) => await tryPersistInherited(ctx, inherited)),\n );\n return new Set(outcomes.filter((id): id is string => id !== null));\n}\n\n/** Create an unchanged snapshot reusing the baseline screenshot. */\nasync function persistInheritedSnapshot(\n ctx: CaptureContext,\n inherited: InheritedStory,\n): Promise<void> {\n const infraHash = infraHashFor(ctx.project.browser ?? \"chromium\", ctx.viewports, SIZING_DEFAULTS);\n const snapshots = new SnapshotModel(ctx.db, ctx.tables);\n const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {\n storyId: inherited.story.id,\n storyName: inherited.story.name,\n storyTitle: inherited.story.title,\n storyImportPath: inherited.story.importPath ?? \"\",\n viewportName: inherited.viewport.name,\n viewportWidth: inherited.viewport.width,\n viewportHeight: inherited.viewport.height,\n screenshotPath: inherited.baseline.screenshotPath,\n infraHash,\n inherited: true,\n baselineId: inherited.baseline.id,\n baselineVersion: inherited.baseline.updatedAt,\n });\n await snapshots.update(snapshot.id, {\n status: \"unchanged\",\n diffPixels: 0,\n diffRatio: 0,\n diffPassed: true,\n diffPath: null,\n infraHash,\n });\n}\n\n/** Every story covered by a capture run, rendered or inherited. */\nfunction allStoryIds(ctx: CaptureContext): ReadonlySet<string> {\n const ids = new Set(ctx.captures.map((capture) => capture.story.id));\n for (const inherited of ctx.inherited ?? []) {\n ids.add(inherited.story.id);\n }\n return ids;\n}\n\n/** Table handles required by the capture pipeline. */\nexport type PipelineTables = BuildTables & BaselineTables & SnapshotTables;\n\n/** Persistence inputs for a completed capture run. */\nexport interface CaptureContext {\n /** Database adapter. */\n db: DatabaseAdapter;\n /** Table handles. */\n tables: PipelineTables;\n /** Storage adapter. */\n storage: StorageAdapter;\n /** The project being captured. */\n project: Project;\n /** The build being captured. */\n build: Build;\n /** Viewports at which stories were captured. */\n viewports: Viewport[];\n /** Screenshot buffers produced by the capture renderer. */\n captures: RenderedSnapshot[];\n /** Stories inheriting their baseline without rendering (affected capture). */\n inherited?: InheritedStory[];\n /** Optional logger for capture diagnostics. */\n logger?: Logger;\n /** Optional per-attempt log recorder (mirrors logger lines into the DB). */\n recordLog?: AttemptLogRecorder;\n /** Server secret for decrypting webhook secrets at send time. */\n secret?: SecretInput;\n}\n\nfunction viewportByName(ctx: CaptureContext, name: string): Viewport {\n return ctx.viewports.find((v) => v.name === name) ?? { name, width: 0, height: 0 };\n}\n\nasync function persistSnapshot(ctx: CaptureContext, capture: RenderedSnapshot): Promise<void> {\n assertScreenshotBuffer(capture.screenshot);\n const viewport = capture.viewport ?? viewportByName(ctx, capture.viewportName);\n const screenshot = screenshotPath(\n ctx.project.id,\n ctx.build.id,\n capture.story.id,\n capture.viewportName,\n );\n await ctx.storage.write(screenshot, capture.screenshot);\n\n const baseline = await resolveBaseline(ctx, capture.story.id, capture.viewportName);\n if (!baseline) {\n await createWithoutBaseline(ctx, capture, viewport, screenshot);\n return;\n }\n await createWithBaseline(ctx, capture, viewport, screenshot, baseline);\n}\n\nasync function resolveBaseline(\n ctx: CaptureContext,\n storyId: string,\n viewport: string,\n): Promise<Baseline | null> {\n const baselines = new BaselineModel(ctx.db, ctx.tables, ctx.storage, ctx.secret);\n return await baselines.resolve(\n ctx.project.id,\n storyId,\n viewport,\n ctx.build.gitBranch,\n ctx.project.gitDefaultBranch,\n );\n}\n\nasync function createWithoutBaseline(\n ctx: CaptureContext,\n capture: RenderedSnapshot,\n viewport: Viewport,\n screenshot: string,\n): Promise<void> {\n const snapshots = new SnapshotModel(ctx.db, ctx.tables);\n const status = ctx.build.isDefault ? \"approved\" : \"new\";\n const infraHash = infraHashFor(ctx.project.browser ?? \"chromium\", ctx.viewports, SIZING_DEFAULTS);\n const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {\n storyId: capture.story.id,\n storyName: capture.story.name,\n storyTitle: capture.story.title,\n storyImportPath: capture.story.importPath ?? \"\",\n viewportName: capture.viewportName,\n viewportWidth: viewport.width,\n viewportHeight: viewport.height,\n screenshotPath: screenshot,\n infraHash,\n baselineVersion: NO_BASELINE,\n });\n await snapshots.setStatus(snapshot.id, status);\n\n if (ctx.build.isDefault) {\n const baselines = new BaselineModel(ctx.db, ctx.tables, ctx.storage, ctx.secret);\n await baselines.upsert(\n ctx.project.id,\n capture.story.id,\n capture.viewportName,\n ctx.build.gitBranch,\n snapshot.id,\n screenshot,\n infraHash,\n );\n }\n}\n\nasync function createWithBaseline(\n ctx: CaptureContext,\n capture: RenderedSnapshot,\n viewport: Viewport,\n screenshot: string,\n baseline: Baseline,\n): Promise<void> {\n const result = await diffStoredScreenshots(\n ctx.storage,\n ctx.project,\n baseline.screenshotPath,\n screenshot,\n capture.story.parameters?.diffThreshold,\n );\n\n const infraHash = infraHashFor(ctx.project.browser ?? \"chromium\", ctx.viewports, SIZING_DEFAULTS);\n const snapshots = new SnapshotModel(ctx.db, ctx.tables);\n const snapshot = await snapshots.create(ctx.project.id, ctx.build.id, {\n storyId: capture.story.id,\n storyName: capture.story.name,\n storyTitle: capture.story.title,\n storyImportPath: capture.story.importPath ?? \"\",\n viewportName: capture.viewportName,\n viewportWidth: viewport.width,\n viewportHeight: viewport.height,\n screenshotPath: screenshot,\n infraHash,\n baselineId: baseline.id,\n baselineVersion: baseline.updatedAt,\n });\n\n // Automigrate: infra-induced size change with project.automigrate enabled\n const shouldAutomigrate =\n result.sizeChanged &&\n !result.passed &&\n (ctx.project as unknown as { automigrate?: boolean }).automigrate &&\n baseline.infraHash !== null &&\n baseline.infraHash !== infraHash;\n\n const status = shouldAutomigrate ? \"unchanged\" : diffStatus(result.passed, ctx.build.isDefault);\n const diff = diffPath(ctx.project.id, ctx.build.id, capture.story.id, capture.viewportName);\n const passed = shouldAutomigrate ? true : result.passed;\n const pixels = shouldAutomigrate ? 0 : result.diffPixels;\n const ratio = shouldAutomigrate ? 0 : result.diffRatio;\n const overlay = await writeDiffOverlay(ctx.storage, diff, result, passed);\n await snapshots.update(snapshot.id, {\n status,\n diffPath: overlay,\n diffPixels: pixels,\n diffRatio: ratio,\n diffPassed: passed,\n infraHash,\n });\n if (ctx.build.isDefault && (shouldAutomigrate || !passed)) {\n await upsertDefaultBaseline(ctx, snapshot.id, capture, screenshot, infraHash);\n }\n}\n\n/** Default-branch builds are authoritative: the captured screenshot becomes the baseline. */\nasync function upsertDefaultBaseline(\n ctx: CaptureContext,\n snapshotId: string,\n capture: RenderedSnapshot,\n screenshot: string,\n infraHash: string,\n): Promise<void> {\n const baselines = new BaselineModel(ctx.db, ctx.tables, ctx.storage, ctx.secret);\n await baselines.upsert(\n ctx.project.id,\n capture.story.id,\n capture.viewportName,\n ctx.build.gitBranch,\n snapshotId,\n screenshot,\n infraHash,\n );\n}\n\n/** Snapshot status after diffing: default-branch changes are auto-approved, not queued for review. */\nexport function diffStatus(passed: boolean, isDefault: boolean): SnapshotStatus {\n if (passed) {\n return \"unchanged\";\n }\n return isDefault ? \"approved\" : \"changed\";\n}\n\n/** A failed capture fails the build; default-branch builds otherwise auto-approve. */\nfunction finalBuildStatus(\n isDefault: boolean,\n failedCount: number,\n hasCaptures: boolean,\n build: { changedCount: number },\n): BuildStatus {\n if (failedCount > 0) {\n return \"failed\";\n }\n if (isDefault || (hasCaptures && build.changedCount === 0)) {\n return \"approved\";\n }\n return \"reviewing\";\n}\n\nasync function finalize(\n ctx: CaptureContext,\n storyIds: ReadonlySet<string>,\n failedStoryIds: ReadonlySet<string>,\n flakyFailedStoryIds: ReadonlySet<string> = new Set(),\n a11yFailedStoryIds: ReadonlySet<string> = new Set(),\n): Promise<void> {\n const builds = new BuildModel(ctx.db, ctx.tables);\n const build = await builds.updateCounts(ctx.build.id);\n const hasCaptures = storyIds.size > 0;\n const status = finalBuildStatus(ctx.build.isDefault, failedStoryIds.size, hasCaptures, build);\n // Flaky and a11y failures do not block: log warning if any failed non-blocking\n if (flakyFailedStoryIds.size > 0 && failedStoryIds.size === 0) {\n ctx.logger?.warn(\n { flakyStoryIds: [...flakyFailedStoryIds] },\n \"flaky stories failed (non-blocking)\",\n );\n await emitAttemptLog(ctx.recordLog, ctx.logger, \"warn\", \"flaky stories failed (non-blocking)\", {\n flakyStoryIds: [...flakyFailedStoryIds],\n });\n }\n if (a11yFailedStoryIds.size > 0 && failedStoryIds.size === 0) {\n ctx.logger?.warn(\n { a11yStoryIds: [...a11yFailedStoryIds] },\n \"a11y violations found (non-blocking)\",\n );\n await emitAttemptLog(\n ctx.recordLog,\n ctx.logger,\n \"warn\",\n \"a11y violations found (non-blocking)\",\n {\n a11yStoryIds: [...a11yFailedStoryIds],\n },\n );\n }\n await builds.setStatus(ctx.build.id, status);\n\n if (ctx.build.isDefault && hasCaptures) {\n const baselines = new BaselineModel(ctx.db, ctx.tables, ctx.storage, ctx.secret);\n await baselines.removeOrphans(ctx.project.id, new Set(storyIds));\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,eAAsB,eACpB,KACA,uCAA4C,IAAI,IAAI,GACpD,sCAA2C,IAAI,IAAI,GACnD,qCAA0C,IAAI,IAAI,GACnC;CACf,MAAM,iBAAiB,IAAI,IAAI,oBAAoB;CACnD,MAAM,WAAW,IAAI,IAAI,mBAAmB;CAC5C,MAAM,UAAU,IAAI,IAAI,kBAAkB;CAC1C,MAAM,QAAQ,IACZ,IAAI,SAAS,IAAI,OAAO,YAAY;EAClC,IAAI;GACF,MAAM,gBAAgB,KAAK,OAAO;EACpC,SAAS,OAAO;GAGd,IAAI,SAAS,IAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAI,QAAQ,MAAM,EAAE,GAAG;IACnE,IAAI,SAAS,IAAI,QAAQ,MAAM,EAAE,GAAG,SAAS,IAAI,QAAQ,MAAM,EAAE;IACjE,IAAI,QAAQ,IAAI,QAAQ,MAAM,EAAE,GAAG,QAAQ,IAAI,QAAQ,MAAM,EAAE;IAC/D,IAAI,QAAQ,KACV;KAAE,SAAS,QAAQ,MAAM;KAAI,UAAU,QAAQ;KAAc,KAAK;IAAM,GACxE,oDACF;IACA,MAAM,eACJ,IAAI,WACJ,IAAI,QACJ,QACA,sDACA;KACE,SAAS,QAAQ,MAAM;KACvB,UAAU,QAAQ;IACpB,CACF;GACF,OACE,eAAe,IAAI,QAAQ,MAAM,EAAE;GAErC,IAAI,QAAQ,MACV;IAAE,SAAS,QAAQ,MAAM;IAAI,UAAU,QAAQ;IAAc,KAAK;GAAM,GACxE,0BACF;GACA,MAAM,eAAe,IAAI,WAAW,IAAI,QAAQ,SAAS,4BAA4B;IACnF,SAAS,QAAQ,MAAM;IACvB,UAAU,QAAQ;GACpB,CAAC;EACH;CACF,CAAC,CACH;CAEA,KAAK,MAAM,MAAM,UACf,IAAI,CAAC,eAAe,IAAI,EAAE,GAAG;EAC3B,IAAI,QAAQ,KAAK,EAAE,SAAS,GAAG,GAAG,mCAAmC;EACrE,MAAM,eAAe,IAAI,WAAW,IAAI,QAAQ,QAAQ,qCAAqC,EAC3F,SAAS,GACX,CAAC;CACH;CAEF,KAAK,MAAM,MAAM,SACf,IAAI,CAAC,eAAe,IAAI,EAAE,KAAK,CAAC,SAAS,IAAI,EAAE,GAAG;EAChD,IAAI,QAAQ,KAAK,EAAE,SAAS,GAAG,GAAG,sCAAsC;EACxE,MAAM,eACJ,IAAI,WACJ,IAAI,QACJ,QACA,wCACA,EACE,SAAS,GACX,CACF;CACF;CAEF,KAAK,MAAM,MAAM,MAAM,iBAAiB,GAAG,GACzC,eAAe,IAAI,EAAE;CAEvB,MAAM,SAAS,KAAK,YAAY,GAAG,GAAG,gBAAgB,UAAU,OAAO;AACzE;;AAGA,eAAe,oBACb,KACA,WACwB;CACxB,IAAI;EACF,MAAM,yBAAyB,KAAK,SAAS;EAC7C,OAAO;CACT,SAAS,OAAO;EACd,IAAI,QAAQ,MACV;GAAE,SAAS,UAAU,MAAM;GAAI,UAAU,UAAU,SAAS;GAAM,KAAK;EAAM,GAC7E,0BACF;EACA,MAAM,eAAe,IAAI,WAAW,IAAI,QAAQ,SAAS,4BAA4B;GACnF,SAAS,UAAU,MAAM;GACzB,UAAU,UAAU,SAAS;EAC/B,CAAC;EACD,OAAO,UAAU,MAAM;CACzB;AACF;;AAGA,eAAe,iBAAiB,KAAmD;CACjF,MAAM,WAAW,MAAM,QAAQ,KAC5B,IAAI,aAAa,CAAC,EAAA,CAAG,IAAI,OAAO,cAAc,MAAM,oBAAoB,KAAK,SAAS,CAAC,CAC1F;CACA,OAAO,IAAI,IAAI,SAAS,QAAQ,OAAqB,OAAO,IAAI,CAAC;AACnE;;AAGA,eAAe,yBACb,KACA,WACe;CACf,MAAM,YAAY,aAAa,IAAI,QAAQ,WAAW,YAAY,IAAI,WAAW,eAAe;CAChG,MAAM,YAAY,IAAI,cAAc,IAAI,IAAI,IAAI,MAAM;CACtD,MAAM,WAAW,MAAM,UAAU,OAAO,IAAI,QAAQ,IAAI,IAAI,MAAM,IAAI;EACpE,SAAS,UAAU,MAAM;EACzB,WAAW,UAAU,MAAM;EAC3B,YAAY,UAAU,MAAM;EAC5B,iBAAiB,UAAU,MAAM,cAAc;EAC/C,cAAc,UAAU,SAAS;EACjC,eAAe,UAAU,SAAS;EAClC,gBAAgB,UAAU,SAAS;EACnC,gBAAgB,UAAU,SAAS;EACnC;EACA,WAAW;EACX,YAAY,UAAU,SAAS;EAC/B,iBAAiB,UAAU,SAAS;CACtC,CAAC;CACD,MAAM,UAAU,OAAO,SAAS,IAAI;EAClC,QAAQ;EACR,YAAY;EACZ,WAAW;EACX,YAAY;EACZ,UAAU;EACV;CACF,CAAC;AACH;;AAGA,SAAS,YAAY,KAA0C;CAC7D,MAAM,MAAM,IAAI,IAAI,IAAI,SAAS,KAAK,YAAY,QAAQ,MAAM,EAAE,CAAC;CACnE,KAAK,MAAM,aAAa,IAAI,aAAa,CAAC,GACxC,IAAI,IAAI,UAAU,MAAM,EAAE;CAE5B,OAAO;AACT;AA+BA,SAAS,eAAe,KAAqB,MAAwB;CACnE,OAAO,IAAI,UAAU,MAAM,MAAM,EAAE,SAAS,IAAI,KAAK;EAAE;EAAM,OAAO;EAAG,QAAQ;CAAE;AACnF;AAEA,eAAe,gBAAgB,KAAqB,SAA0C;CAC5F,uBAAuB,QAAQ,UAAU;CACzC,MAAM,WAAW,QAAQ,YAAY,eAAe,KAAK,QAAQ,YAAY;CAC7E,MAAM,aAAa,eACjB,IAAI,QAAQ,IACZ,IAAI,MAAM,IACV,QAAQ,MAAM,IACd,QAAQ,YACV;CACA,MAAM,IAAI,QAAQ,MAAM,YAAY,QAAQ,UAAU;CAEtD,MAAM,WAAW,MAAM,gBAAgB,KAAK,QAAQ,MAAM,IAAI,QAAQ,YAAY;CAClF,IAAI,CAAC,UAAU;EACb,MAAM,sBAAsB,KAAK,SAAS,UAAU,UAAU;EAC9D;CACF;CACA,MAAM,mBAAmB,KAAK,SAAS,UAAU,YAAY,QAAQ;AACvE;AAEA,eAAe,gBACb,KACA,SACA,UAC0B;CAE1B,OAAO,MAAM,IADS,cAAc,IAAI,IAAI,IAAI,QAAQ,IAAI,SAAS,IAAI,MACpD,CAAC,CAAC,QACrB,IAAI,QAAQ,IACZ,SACA,UACA,IAAI,MAAM,WACV,IAAI,QAAQ,gBACd;AACF;AAEA,eAAe,sBACb,KACA,SACA,UACA,YACe;CACf,MAAM,YAAY,IAAI,cAAc,IAAI,IAAI,IAAI,MAAM;CACtD,MAAM,SAAS,IAAI,MAAM,YAAY,aAAa;CAClD,MAAM,YAAY,aAAa,IAAI,QAAQ,WAAW,YAAY,IAAI,WAAW,eAAe;CAChG,MAAM,WAAW,MAAM,UAAU,OAAO,IAAI,QAAQ,IAAI,IAAI,MAAM,IAAI;EACpE,SAAS,QAAQ,MAAM;EACvB,WAAW,QAAQ,MAAM;EACzB,YAAY,QAAQ,MAAM;EAC1B,iBAAiB,QAAQ,MAAM,cAAc;EAC7C,cAAc,QAAQ;EACtB,eAAe,SAAS;EACxB,gBAAgB,SAAS;EACzB,gBAAgB;EAChB;EACA,iBAAiB;CACnB,CAAC;CACD,MAAM,UAAU,UAAU,SAAS,IAAI,MAAM;CAE7C,IAAI,IAAI,MAAM,WAEZ,MAAM,IADgB,cAAc,IAAI,IAAI,IAAI,QAAQ,IAAI,SAAS,IAAI,MAC3D,CAAC,CAAC,OACd,IAAI,QAAQ,IACZ,QAAQ,MAAM,IACd,QAAQ,cACR,IAAI,MAAM,WACV,SAAS,IACT,YACA,SACF;AAEJ;AAEA,eAAe,mBACb,KACA,SACA,UACA,YACA,UACe;CACf,MAAM,SAAS,MAAM,sBACnB,IAAI,SACJ,IAAI,SACJ,SAAS,gBACT,YACA,QAAQ,MAAM,YAAY,aAC5B;CAEA,MAAM,YAAY,aAAa,IAAI,QAAQ,WAAW,YAAY,IAAI,WAAW,eAAe;CAChG,MAAM,YAAY,IAAI,cAAc,IAAI,IAAI,IAAI,MAAM;CACtD,MAAM,WAAW,MAAM,UAAU,OAAO,IAAI,QAAQ,IAAI,IAAI,MAAM,IAAI;EACpE,SAAS,QAAQ,MAAM;EACvB,WAAW,QAAQ,MAAM;EACzB,YAAY,QAAQ,MAAM;EAC1B,iBAAiB,QAAQ,MAAM,cAAc;EAC7C,cAAc,QAAQ;EACtB,eAAe,SAAS;EACxB,gBAAgB,SAAS;EACzB,gBAAgB;EAChB;EACA,YAAY,SAAS;EACrB,iBAAiB,SAAS;CAC5B,CAAC;CAGD,MAAM,oBACJ,OAAO,eACP,CAAC,OAAO,UACP,IAAI,QAAiD,eACtD,SAAS,cAAc,QACvB,SAAS,cAAc;CAEzB,MAAM,SAAS,oBAAoB,cAAc,WAAW,OAAO,QAAQ,IAAI,MAAM,SAAS;CAC9F,MAAM,OAAO,SAAS,IAAI,QAAQ,IAAI,IAAI,MAAM,IAAI,QAAQ,MAAM,IAAI,QAAQ,YAAY;CAC1F,MAAM,SAAS,oBAAoB,OAAO,OAAO;CACjD,MAAM,SAAS,oBAAoB,IAAI,OAAO;CAC9C,MAAM,QAAQ,oBAAoB,IAAI,OAAO;CAC7C,MAAM,UAAU,MAAM,iBAAiB,IAAI,SAAS,MAAM,QAAQ,MAAM;CACxE,MAAM,UAAU,OAAO,SAAS,IAAI;EAClC;EACA,UAAU;EACV,YAAY;EACZ,WAAW;EACX,YAAY;EACZ;CACF,CAAC;CACD,IAAI,IAAI,MAAM,cAAc,qBAAqB,CAAC,SAChD,MAAM,sBAAsB,KAAK,SAAS,IAAI,SAAS,YAAY,SAAS;AAEhF;;AAGA,eAAe,sBACb,KACA,YACA,SACA,YACA,WACe;CAEf,MAAM,IADgB,cAAc,IAAI,IAAI,IAAI,QAAQ,IAAI,SAAS,IAAI,MAC3D,CAAC,CAAC,OACd,IAAI,QAAQ,IACZ,QAAQ,MAAM,IACd,QAAQ,cACR,IAAI,MAAM,WACV,YACA,YACA,SACF;AACF;;AAGA,SAAgB,WAAW,QAAiB,WAAoC;CAC9E,IAAI,QACF,OAAO;CAET,OAAO,YAAY,aAAa;AAClC;;AAGA,SAAS,iBACP,WACA,aACA,aACA,OACa;CACb,IAAI,cAAc,GAChB,OAAO;CAET,IAAI,aAAc,eAAe,MAAM,iBAAiB,GACtD,OAAO;CAET,OAAO;AACT;AAEA,eAAe,SACb,KACA,UACA,gBACA,sCAA2C,IAAI,IAAI,GACnD,qCAA0C,IAAI,IAAI,GACnC;CACf,MAAM,SAAS,IAAI,WAAW,IAAI,IAAI,IAAI,MAAM;CAChD,MAAM,QAAQ,MAAM,OAAO,aAAa,IAAI,MAAM,EAAE;CACpD,MAAM,cAAc,SAAS,OAAO;CACpC,MAAM,SAAS,iBAAiB,IAAI,MAAM,WAAW,eAAe,MAAM,aAAa,KAAK;CAE5F,IAAI,oBAAoB,OAAO,KAAK,eAAe,SAAS,GAAG;EAC7D,IAAI,QAAQ,KACV,EAAE,eAAe,CAAC,GAAG,mBAAmB,EAAE,GAC1C,qCACF;EACA,MAAM,eAAe,IAAI,WAAW,IAAI,QAAQ,QAAQ,uCAAuC,EAC7F,eAAe,CAAC,GAAG,mBAAmB,EACxC,CAAC;CACH;CACA,IAAI,mBAAmB,OAAO,KAAK,eAAe,SAAS,GAAG;EAC5D,IAAI,QAAQ,KACV,EAAE,cAAc,CAAC,GAAG,kBAAkB,EAAE,GACxC,sCACF;EACA,MAAM,eACJ,IAAI,WACJ,IAAI,QACJ,QACA,wCACA,EACE,cAAc,CAAC,GAAG,kBAAkB,EACtC,CACF;CACF;CACA,MAAM,OAAO,UAAU,IAAI,MAAM,IAAI,MAAM;CAE3C,IAAI,IAAI,MAAM,aAAa,aAEzB,MAAM,IADgB,cAAc,IAAI,IAAI,IAAI,QAAQ,IAAI,SAAS,IAAI,MAC3D,CAAC,CAAC,cAAc,IAAI,QAAQ,IAAI,IAAI,IAAI,QAAQ,CAAC;AAEnE"}
@@ -0,0 +1,21 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { DispatchDeps } from "./dispatch.mjs";
4
+ //#region src/capture/recovery.d.ts
5
+ /** Error recorded on attempts that a restart cut short; also the requeue-once marker. */
6
+ declare const INTERRUPTED_ERROR = "Interrupted by server restart";
7
+ /** Outcome of one recovery sweep. */
8
+ interface RecoveryResult {
9
+ requeued: string[];
10
+ failed: string[];
11
+ }
12
+ /** Inputs for {@link recoverStuckCaptures}. */
13
+ interface RecoveryDeps {
14
+ deps: DispatchDeps;
15
+ enqueue: (buildId: string) => Promise<void>;
16
+ }
17
+ /** Requeue (once) or fail every build still `capturing`; call once at boot, before serving jobs. */
18
+ declare function recoverStuckCaptures(recovery: RecoveryDeps): Promise<RecoveryResult>;
19
+ //#endregion
20
+ export { INTERRUPTED_ERROR, RecoveryDeps, RecoveryResult, recoverStuckCaptures };
21
+ //# sourceMappingURL=recovery.d.mts.map
@@ -0,0 +1,83 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { BuildModel } from "../models/build.mjs";
5
+ import { CaptureAttemptModel } from "../models/capture-attempt.mjs";
6
+ import { ProjectModel } from "../models/project.mjs";
7
+ import { postStatusesForBuild } from "./status-fanout.mjs";
8
+ import { eq, getTableColumns } from "drizzle-orm";
9
+ //#region src/capture/recovery.ts
10
+ /**
11
+ * Boot-time recovery for builds left in `capturing` by a server restart.
12
+ *
13
+ * Policy: requeue once. The interrupted attempt is closed with
14
+ * {@link INTERRUPTED_ERROR}; a build with no earlier interrupted attempt goes back to `pending`
15
+ * and is re-enqueued, otherwise it is marked `failed` for manual retry. A build is failed instead
16
+ * when the run before the interrupted one was itself interrupted, so a crash loop stops after
17
+ * one requeue while manual retries stay unrestricted. In-process queues only: remote queues redeliver on their own.
18
+ */
19
+ /** Error recorded on attempts that a restart cut short; also the requeue-once marker. */
20
+ const INTERRUPTED_ERROR = "Interrupted by server restart";
21
+ /** Requeue (once) or fail every build still `capturing`; call once at boot, before serving jobs. */
22
+ async function recoverStuckCaptures(recovery) {
23
+ const { deps } = recovery;
24
+ const stuck = await deps.db.list(deps.tables.builds, { where: eq(getTableColumns(deps.tables.builds)["status"], "capturing") });
25
+ const outcomes = await Promise.all(stuck.map(async (build) => await recoverBuild(recovery, build)));
26
+ const result = {
27
+ requeued: stuck.filter((_, index) => outcomes[index]).map((build) => build.id),
28
+ failed: stuck.filter((_, index) => !outcomes[index]).map((build) => build.id)
29
+ };
30
+ if (stuck.length > 0) deps.logger.warn({
31
+ requeued: result.requeued,
32
+ failed: result.failed
33
+ }, "recovered builds interrupted by restart");
34
+ return result;
35
+ }
36
+ /** Recover one build; returns true when it was requeued. */
37
+ async function recoverBuild(recovery, build) {
38
+ const { deps } = recovery;
39
+ const attempts = new CaptureAttemptModel(deps.db, deps.tables);
40
+ const history = await attempts.listByBuild(build.id);
41
+ const open = history.filter((a) => a.status === "queued" || a.status === "running");
42
+ const lastClosed = history.findLast((a) => !open.includes(a));
43
+ await Promise.all(open.map(async (a) => await attempts.markFinished(a.id, "failed", INTERRUPTED_ERROR)));
44
+ return lastClosed?.error === "Interrupted by server restart" ? await failInterrupted(deps, build) : await requeueInterrupted(recovery, build);
45
+ }
46
+ async function failInterrupted(deps, build) {
47
+ await new BuildModel(deps.db, deps.tables).setStatus(build.id, "failed");
48
+ await postFailure(deps, build);
49
+ deps.logger.error({ buildId: build.id }, "build interrupted by restart again; marked failed for manual retry");
50
+ return false;
51
+ }
52
+ async function requeueInterrupted(recovery, build) {
53
+ const { deps } = recovery;
54
+ await new BuildModel(deps.db, deps.tables).setStatus(build.id, "pending");
55
+ await recovery.enqueue(build.id);
56
+ deps.logger.warn({ buildId: build.id }, "build interrupted by restart; requeued once");
57
+ return true;
58
+ }
59
+ /** Replace the lingering pending git status with a failure. */
60
+ async function postFailure(deps, build) {
61
+ const project = await new ProjectModel(deps.db, deps.tables).get(build.projectId);
62
+ if (!project) return;
63
+ await postStatusesForBuild({
64
+ db: deps.db,
65
+ tables: deps.tables,
66
+ project,
67
+ sha: build.gitSha,
68
+ status: "failure",
69
+ url: `/projects/${project.slug}/builds/${build.id}`,
70
+ providers: deps.gitHosts,
71
+ secret: deps.secret,
72
+ logger: deps.logger
73
+ }).catch((error) => {
74
+ deps.logger.error({
75
+ err: error,
76
+ buildId: build.id
77
+ }, "failed to post failure status");
78
+ });
79
+ }
80
+ //#endregion
81
+ export { INTERRUPTED_ERROR, recoverStuckCaptures };
82
+
83
+ //# sourceMappingURL=recovery.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery.mjs","names":[],"sources":["../../src/capture/recovery.ts"],"sourcesContent":["/**\n * Boot-time recovery for builds left in `capturing` by a server restart.\n *\n * Policy: requeue once. The interrupted attempt is closed with\n * {@link INTERRUPTED_ERROR}; a build with no earlier interrupted attempt goes back to `pending`\n * and is re-enqueued, otherwise it is marked `failed` for manual retry. A build is failed instead\n * when the run before the interrupted one was itself interrupted, so a crash loop stops after\n * one requeue while manual retries stay unrestricted. In-process queues only: remote queues redeliver on their own.\n */\nimport { eq, getTableColumns } from \"drizzle-orm\";\nimport type { SQLWrapper } from \"drizzle-orm\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { CaptureAttemptModel } from \"../models/capture-attempt.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport type { DispatchDeps } from \"./dispatch.ts\";\nimport { postStatusesForBuild } from \"./status-fanout.ts\";\n\n/** Error recorded on attempts that a restart cut short; also the requeue-once marker. */\nexport const INTERRUPTED_ERROR = \"Interrupted by server restart\";\n\n/** Outcome of one recovery sweep. */\nexport interface RecoveryResult {\n requeued: string[];\n failed: string[];\n}\n\n/** Inputs for {@link recoverStuckCaptures}. */\nexport interface RecoveryDeps {\n deps: DispatchDeps;\n enqueue: (buildId: string) => Promise<void>;\n}\n\n/** Requeue (once) or fail every build still `capturing`; call once at boot, before serving jobs. */\nexport async function recoverStuckCaptures(recovery: RecoveryDeps): Promise<RecoveryResult> {\n const { deps } = recovery;\n const stuck = (await deps.db.list(deps.tables.builds, {\n where: eq(getTableColumns(deps.tables.builds)[\"status\"] as unknown as SQLWrapper, \"capturing\"),\n })) as unknown as Build[];\n const outcomes = await Promise.all(\n stuck.map(async (build) => await recoverBuild(recovery, build)),\n );\n const result: RecoveryResult = {\n requeued: stuck.filter((_, index) => outcomes[index]).map((build) => build.id),\n failed: stuck.filter((_, index) => !outcomes[index]).map((build) => build.id),\n };\n if (stuck.length > 0) {\n deps.logger.warn(\n { requeued: result.requeued, failed: result.failed },\n \"recovered builds interrupted by restart\",\n );\n }\n return result;\n}\n\n/** Recover one build; returns true when it was requeued. */\nasync function recoverBuild(recovery: RecoveryDeps, build: Build): Promise<boolean> {\n const { deps } = recovery;\n const attempts = new CaptureAttemptModel(deps.db, deps.tables);\n const history = await attempts.listByBuild(build.id);\n const open = history.filter((a) => a.status === \"queued\" || a.status === \"running\");\n // Requeue-once: the run before the interrupted one was itself interrupted => crash loop.\n const lastClosed = history.findLast((a) => !open.includes(a));\n await Promise.all(\n open.map(async (a) => await attempts.markFinished(a.id, \"failed\", INTERRUPTED_ERROR)),\n );\n return lastClosed?.error === INTERRUPTED_ERROR\n ? await failInterrupted(deps, build)\n : await requeueInterrupted(recovery, build);\n}\n\nasync function failInterrupted(deps: DispatchDeps, build: Build): Promise<false> {\n await new BuildModel(deps.db, deps.tables).setStatus(build.id, \"failed\");\n await postFailure(deps, build);\n deps.logger.error(\n { buildId: build.id },\n \"build interrupted by restart again; marked failed for manual retry\",\n );\n return false;\n}\n\nasync function requeueInterrupted(recovery: RecoveryDeps, build: Build): Promise<true> {\n const { deps } = recovery;\n await new BuildModel(deps.db, deps.tables).setStatus(build.id, \"pending\");\n await recovery.enqueue(build.id);\n deps.logger.warn({ buildId: build.id }, \"build interrupted by restart; requeued once\");\n return true;\n}\n\n/** Replace the lingering pending git status with a failure. */\nasync function postFailure(deps: DispatchDeps, build: Build): Promise<void> {\n const project = await new ProjectModel(deps.db, deps.tables).get(build.projectId);\n if (!project) {\n return;\n }\n await postStatusesForBuild({\n db: deps.db,\n tables: deps.tables,\n project,\n sha: build.gitSha,\n status: \"failure\",\n url: `/projects/${project.slug}/builds/${build.id}`,\n providers: deps.gitHosts,\n secret: deps.secret,\n logger: deps.logger,\n }).catch((error: unknown) => {\n deps.logger.error({ err: error, buildId: build.id }, \"failed to post failure status\");\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,MAAa,oBAAoB;;AAejC,eAAsB,qBAAqB,UAAiD;CAC1F,MAAM,EAAE,SAAS;CACjB,MAAM,QAAS,MAAM,KAAK,GAAG,KAAK,KAAK,OAAO,QAAQ,EACpD,OAAO,GAAG,gBAAgB,KAAK,OAAO,MAAM,CAAC,CAAC,WAAoC,WAAW,EAC/F,CAAC;CACD,MAAM,WAAW,MAAM,QAAQ,IAC7B,MAAM,IAAI,OAAO,UAAU,MAAM,aAAa,UAAU,KAAK,CAAC,CAChE;CACA,MAAM,SAAyB;EAC7B,UAAU,MAAM,QAAQ,GAAG,UAAU,SAAS,MAAM,CAAC,CAAC,KAAK,UAAU,MAAM,EAAE;EAC7E,QAAQ,MAAM,QAAQ,GAAG,UAAU,CAAC,SAAS,MAAM,CAAC,CAAC,KAAK,UAAU,MAAM,EAAE;CAC9E;CACA,IAAI,MAAM,SAAS,GACjB,KAAK,OAAO,KACV;EAAE,UAAU,OAAO;EAAU,QAAQ,OAAO;CAAO,GACnD,yCACF;CAEF,OAAO;AACT;;AAGA,eAAe,aAAa,UAAwB,OAAgC;CAClF,MAAM,EAAE,SAAS;CACjB,MAAM,WAAW,IAAI,oBAAoB,KAAK,IAAI,KAAK,MAAM;CAC7D,MAAM,UAAU,MAAM,SAAS,YAAY,MAAM,EAAE;CACnD,MAAM,OAAO,QAAQ,QAAQ,MAAM,EAAE,WAAW,YAAY,EAAE,WAAW,SAAS;CAElF,MAAM,aAAa,QAAQ,UAAU,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC;CAC5D,MAAM,QAAQ,IACZ,KAAK,IAAI,OAAO,MAAM,MAAM,SAAS,aAAa,EAAE,IAAI,UAAU,iBAAiB,CAAC,CACtF;CACA,OAAO,YAAY,UAAA,kCACf,MAAM,gBAAgB,MAAM,KAAK,IACjC,MAAM,mBAAmB,UAAU,KAAK;AAC9C;AAEA,eAAe,gBAAgB,MAAoB,OAA8B;CAC/E,MAAM,IAAI,WAAW,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC,UAAU,MAAM,IAAI,QAAQ;CACvE,MAAM,YAAY,MAAM,KAAK;CAC7B,KAAK,OAAO,MACV,EAAE,SAAS,MAAM,GAAG,GACpB,oEACF;CACA,OAAO;AACT;AAEA,eAAe,mBAAmB,UAAwB,OAA6B;CACrF,MAAM,EAAE,SAAS;CACjB,MAAM,IAAI,WAAW,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC,UAAU,MAAM,IAAI,SAAS;CACxE,MAAM,SAAS,QAAQ,MAAM,EAAE;CAC/B,KAAK,OAAO,KAAK,EAAE,SAAS,MAAM,GAAG,GAAG,6CAA6C;CACrF,OAAO;AACT;;AAGA,eAAe,YAAY,MAAoB,OAA6B;CAC1E,MAAM,UAAU,MAAM,IAAI,aAAa,KAAK,IAAI,KAAK,MAAM,CAAC,CAAC,IAAI,MAAM,SAAS;CAChF,IAAI,CAAC,SACH;CAEF,MAAM,qBAAqB;EACzB,IAAI,KAAK;EACT,QAAQ,KAAK;EACb;EACA,KAAK,MAAM;EACX,QAAQ;EACR,KAAK,aAAa,QAAQ,KAAK,UAAU,MAAM;EAC/C,WAAW,KAAK;EAChB,QAAQ,KAAK;EACb,QAAQ,KAAK;CACf,CAAC,CAAC,CAAC,OAAO,UAAmB;EAC3B,KAAK,OAAO,MAAM;GAAE,KAAK;GAAO,SAAS,MAAM;EAAG,GAAG,+BAA+B;CACtF,CAAC;AACH"}