@storyshelf/core 0.7.0 → 0.9.0-next-d2cc082

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 (537) hide show
  1. package/dist/adapter/capture-queue.d.mts +0 -2
  2. package/dist/adapter/capture-runner.d.mts +10 -4
  3. package/dist/adapter/capture-runner.mjs +7 -2
  4. package/dist/adapter/database.d.mts +63 -3
  5. package/dist/adapter/database.mjs +4 -3
  6. package/dist/adapter/database.mjs.map +1 -1
  7. package/dist/adapter/design.d.mts +0 -2
  8. package/dist/adapter/design.mjs +1 -2
  9. package/dist/adapter/email-sender.d.mts +0 -2
  10. package/dist/adapter/metadata.d.mts +0 -2
  11. package/dist/adapter/oauth.d.mts +0 -2
  12. package/dist/adapter/setup.d.mts +0 -2
  13. package/dist/adapter/setup.mjs +1 -2
  14. package/dist/adapter/storage.d.mts +1 -3
  15. package/dist/adapter/webhook-events.d.mts +0 -2
  16. package/dist/adapter/webhook-events.mjs +12 -13
  17. package/dist/adapter/webhook-events.mjs.map +1 -1
  18. package/dist/adapters/capture-target-offers.d.mts +0 -2
  19. package/dist/adapters/capture-target-offers.mjs +1 -2
  20. package/dist/adapters/capture-target.d.mts +0 -2
  21. package/dist/adapters/capture-target.mjs +1 -2
  22. package/dist/adapters/notifier/build-comment.d.mts +0 -2
  23. package/dist/adapters/notifier/build-comment.mjs +1 -2
  24. package/dist/adapters/notifier/build-state.d.mts +0 -2
  25. package/dist/adapters/notifier/build-state.mjs +1 -2
  26. package/dist/adapters/notifier/events.d.mts +0 -2
  27. package/dist/adapters/notifier/events.mjs +1 -2
  28. package/dist/adapters/notifier/format.d.mts +0 -2
  29. package/dist/adapters/notifier/format.mjs +1 -2
  30. package/dist/adapters/notifier/provider.d.mts +0 -2
  31. package/dist/adapters/notifier/provider.mjs +1 -2
  32. package/dist/adapters/notifier/state.d.mts +0 -2
  33. package/dist/adapters/notifier/state.mjs +1 -2
  34. package/dist/adapters/notifier/types.d.mts +0 -2
  35. package/dist/adapters/remote-browser.d.mts +120 -31
  36. package/dist/adapters/remote-browser.mjs +134 -34
  37. package/dist/adapters/remote-browser.mjs.map +1 -1
  38. package/dist/adapters/remote-lease-attempt.mjs +39 -0
  39. package/dist/adapters/remote-lease-attempt.mjs.map +1 -0
  40. package/dist/adapters/remote-lease-error.d.mts +25 -0
  41. package/dist/adapters/remote-lease-error.mjs +54 -0
  42. package/dist/adapters/remote-lease-error.mjs.map +1 -0
  43. package/dist/adapters/remote-lease-held.d.mts +15 -0
  44. package/dist/adapters/remote-lease-held.mjs +86 -0
  45. package/dist/adapters/remote-lease-held.mjs.map +1 -0
  46. package/dist/adapters/remote-lease-shutdown.d.mts +12 -0
  47. package/dist/adapters/remote-lease-shutdown.mjs +32 -0
  48. package/dist/adapters/remote-lease-shutdown.mjs.map +1 -0
  49. package/dist/adapters/remote-lease.d.mts +39 -0
  50. package/dist/adapters/remote-lease.mjs +175 -0
  51. package/dist/adapters/remote-lease.mjs.map +1 -0
  52. package/dist/adapters/remote-limiter.mjs +121 -0
  53. package/dist/adapters/remote-limiter.mjs.map +1 -0
  54. package/dist/adapters/remote-pool.d.mts +56 -0
  55. package/dist/adapters/remote-pool.mjs +195 -0
  56. package/dist/adapters/remote-pool.mjs.map +1 -0
  57. package/dist/adapters/remote-sizing.d.mts +44 -0
  58. package/dist/adapters/remote-sizing.mjs +53 -0
  59. package/dist/adapters/remote-sizing.mjs.map +1 -0
  60. package/dist/adapters/render-runs.d.mts +0 -2
  61. package/dist/adapters/render-runs.mjs +1 -2
  62. package/dist/adapters/setup-runner.d.mts +0 -2
  63. package/dist/adapters/setup-runner.mjs +1 -2
  64. package/dist/adapters/sources.d.mts +0 -2
  65. package/dist/adapters/sources.mjs +1 -2
  66. package/dist/adapters/validate.d.mts +0 -2
  67. package/dist/adapters/validate.mjs +2 -2
  68. package/dist/adapters/validate.mjs.map +1 -1
  69. package/dist/ai.d.mts +0 -2
  70. package/dist/ai.mjs +1 -2
  71. package/dist/auth.d.mts +0 -2
  72. package/dist/auth.mjs +1 -2
  73. package/dist/brand-packs.d.mts +0 -2
  74. package/dist/brand-packs.mjs +0 -1
  75. package/dist/capture/a11y/axe-source.d.mts +0 -2
  76. package/dist/capture/a11y/axe-source.mjs +1 -2
  77. package/dist/capture/a11y/in-page.d.mts +1 -3
  78. package/dist/capture/a11y/in-page.mjs +3 -4
  79. package/dist/capture/a11y/in-page.mjs.map +1 -1
  80. package/dist/capture/a11y/map.d.mts +0 -2
  81. package/dist/capture/a11y/map.mjs +1 -2
  82. package/dist/capture/a11y/options.d.mts +0 -2
  83. package/dist/capture/a11y/options.mjs +1 -2
  84. package/dist/capture/adapter.d.mts +5 -2
  85. package/dist/capture/adapter.mjs +1 -2
  86. package/dist/capture/adapter.mjs.map +1 -1
  87. package/dist/capture/affected.d.mts +0 -2
  88. package/dist/capture/affected.mjs +1 -2
  89. package/dist/capture/attempt-logger.mjs +1 -2
  90. package/dist/capture/browsers.d.mts +0 -2
  91. package/dist/capture/browsers.mjs +1 -2
  92. package/dist/capture/build-state/deliver.d.mts +0 -2
  93. package/dist/capture/build-state/deliver.mjs +1 -2
  94. package/dist/capture/build-state/notify-job.d.mts +0 -2
  95. package/dist/capture/build-state/notify-job.mjs +1 -2
  96. package/dist/capture/build-state/publisher.d.mts +0 -2
  97. package/dist/capture/build-state/publisher.mjs +1 -2
  98. package/dist/capture/build-state/resume.d.mts +0 -2
  99. package/dist/capture/build-state/resume.mjs +1 -2
  100. package/dist/capture/build-state/types.d.mts +1 -3
  101. package/dist/capture/catalog/catalogue.d.mts +0 -2
  102. package/dist/capture/catalog/catalogue.mjs +1 -2
  103. package/dist/capture/catalog/components.d.mts +0 -2
  104. package/dist/capture/catalog/components.mjs +1 -2
  105. package/dist/capture/catalog/history.d.mts +0 -2
  106. package/dist/capture/catalog/history.mjs +1 -2
  107. package/dist/capture/catalog/hook.d.mts +0 -2
  108. package/dist/capture/catalog/hook.mjs +1 -2
  109. package/dist/capture/catalog/index.d.mts +2 -2
  110. package/dist/capture/catalog/load.d.mts +0 -2
  111. package/dist/capture/catalog/load.mjs +1 -2
  112. package/dist/capture/catalog/parse.d.mts +0 -2
  113. package/dist/capture/catalog/parse.mjs +1 -2
  114. package/dist/capture/catalog/read.d.mts +6 -3
  115. package/dist/capture/catalog/read.mjs +14 -8
  116. package/dist/capture/catalog/read.mjs.map +1 -1
  117. package/dist/capture/catalog/types.d.mts +0 -2
  118. package/dist/capture/comparison-reference.d.mts +27 -0
  119. package/dist/capture/comparison-reference.mjs +75 -0
  120. package/dist/capture/comparison-reference.mjs.map +1 -0
  121. package/dist/capture/consumer/capture-handler.d.mts +1 -3
  122. package/dist/capture/consumer/capture-handler.mjs +1 -2
  123. package/dist/capture/consumer/idle.d.mts +0 -2
  124. package/dist/capture/consumer/idle.mjs +1 -2
  125. package/dist/capture/consumer/index.d.mts +0 -2
  126. package/dist/capture/consumer/index.mjs +1 -2
  127. package/dist/capture/consumer/loop.mjs +1 -2
  128. package/dist/capture/consumer/metrics.d.mts +0 -2
  129. package/dist/capture/consumer/metrics.mjs +1 -2
  130. package/dist/capture/consumer/retry.d.mts +0 -2
  131. package/dist/capture/consumer/retry.mjs +1 -2
  132. package/dist/capture/consumer/run-job.mjs +1 -2
  133. package/dist/capture/consumer/startup.mjs +1 -2
  134. package/dist/capture/consumer/types.d.mts +0 -2
  135. package/dist/capture/crash-failure.d.mts +0 -2
  136. package/dist/capture/crash-failure.mjs +1 -2
  137. package/dist/capture/db-queue/cadence.d.mts +0 -2
  138. package/dist/capture/db-queue/cadence.mjs +1 -2
  139. package/dist/capture/db-queue/claim.d.mts +0 -2
  140. package/dist/capture/db-queue/claim.mjs +20 -4
  141. package/dist/capture/db-queue/claim.mjs.map +1 -1
  142. package/dist/capture/db-queue/clock.mjs +1 -2
  143. package/dist/capture/db-queue/codec.mjs +1 -2
  144. package/dist/capture/db-queue/index.d.mts +0 -2
  145. package/dist/capture/db-queue/index.mjs +3 -3
  146. package/dist/capture/db-queue/index.mjs.map +1 -1
  147. package/dist/capture/db-queue/keeper.d.mts +0 -2
  148. package/dist/capture/db-queue/keeper.mjs +1 -2
  149. package/dist/capture/db-queue/lease.mjs +1 -2
  150. package/dist/capture/db-queue/lifecycle.mjs +1 -2
  151. package/dist/capture/db-queue/operations.mjs +1 -2
  152. package/dist/capture/db-queue/poll-loop.d.mts +0 -2
  153. package/dist/capture/db-queue/poll-loop.mjs +1 -2
  154. package/dist/capture/db-queue/poll.d.mts +0 -2
  155. package/dist/capture/db-queue/poll.mjs +1 -2
  156. package/dist/capture/db-queue/timers.d.mts +0 -2
  157. package/dist/capture/db-queue/timers.mjs +1 -2
  158. package/dist/capture/db-queue/types.d.mts +0 -2
  159. package/dist/capture/db-queue/types.mjs +1 -2
  160. package/dist/capture/db-queue/types.mjs.map +1 -1
  161. package/dist/capture/db-queue/wake.mjs +1 -2
  162. package/dist/capture/design.mjs +1 -2
  163. package/dist/capture/diff-record.mjs +1 -2
  164. package/dist/capture/dispatch.d.mts +1 -3
  165. package/dist/capture/dispatch.mjs +17 -5
  166. package/dist/capture/dispatch.mjs.map +1 -1
  167. package/dist/capture/enrich.mjs +1 -2
  168. package/dist/capture/failure-sets.mjs +1 -2
  169. package/dist/capture/fingerprint.d.mts +19 -8
  170. package/dist/capture/fingerprint.mjs +43 -14
  171. package/dist/capture/fingerprint.mjs.map +1 -1
  172. package/dist/capture/forked/channel.d.mts +0 -2
  173. package/dist/capture/forked/child.d.mts +0 -2
  174. package/dist/capture/forked/child.mjs +1 -2
  175. package/dist/capture/forked/crash-tracker.mjs +1 -2
  176. package/dist/capture/forked/decision.d.mts +0 -2
  177. package/dist/capture/forked/decision.mjs +1 -2
  178. package/dist/capture/forked/exit.mjs +1 -2
  179. package/dist/capture/forked/fork.d.mts +0 -2
  180. package/dist/capture/forked/fork.mjs +1 -2
  181. package/dist/capture/forked/forked-capture.d.mts +0 -2
  182. package/dist/capture/forked/forked-capture.mjs +1 -2
  183. package/dist/capture/forked/lease-book.mjs +1 -2
  184. package/dist/capture/forked/protocol.d.mts +0 -2
  185. package/dist/capture/forked/protocol.mjs +1 -2
  186. package/dist/capture/forked/runs.mjs +1 -2
  187. package/dist/capture/guards.mjs +1 -2
  188. package/dist/capture/ignore-regions.d.mts +36 -0
  189. package/dist/capture/ignore-regions.mjs +65 -0
  190. package/dist/capture/ignore-regions.mjs.map +1 -0
  191. package/dist/capture/isolate.mjs +1 -2
  192. package/dist/capture/job-lease.d.mts +0 -2
  193. package/dist/capture/job-lease.mjs +1 -2
  194. package/dist/capture/matrix.mjs +4 -4
  195. package/dist/capture/matrix.mjs.map +1 -1
  196. package/dist/capture/modes.d.mts +0 -2
  197. package/dist/capture/modes.mjs +2 -3
  198. package/dist/capture/modes.mjs.map +1 -1
  199. package/dist/capture/orchestrator.d.mts +0 -2
  200. package/dist/capture/orchestrator.mjs +2 -3
  201. package/dist/capture/pipeline.d.mts +0 -2
  202. package/dist/capture/pipeline.mjs +44 -10
  203. package/dist/capture/pipeline.mjs.map +1 -1
  204. package/dist/capture/progress.d.mts +0 -2
  205. package/dist/capture/progress.mjs +1 -2
  206. package/dist/capture/publish/accept.d.mts +0 -2
  207. package/dist/capture/publish/accept.mjs +1 -2
  208. package/dist/capture/publish/complete.d.mts +1 -3
  209. package/dist/capture/publish/complete.mjs +1 -2
  210. package/dist/capture/publish/extract.d.mts +0 -2
  211. package/dist/capture/publish/extract.mjs +6 -5
  212. package/dist/capture/publish/extract.mjs.map +1 -1
  213. package/dist/capture/publish/handler.d.mts +1 -3
  214. package/dist/capture/publish/handler.mjs +1 -2
  215. package/dist/capture/publish/index.d.mts +2 -1
  216. package/dist/capture/publish/invalid-zip.d.mts +8 -0
  217. package/dist/capture/publish/invalid-zip.mjs +11 -0
  218. package/dist/capture/publish/invalid-zip.mjs.map +1 -0
  219. package/dist/capture/publish/limits.d.mts +0 -2
  220. package/dist/capture/publish/limits.mjs +1 -2
  221. package/dist/capture/publish/published.d.mts +0 -2
  222. package/dist/capture/publish/published.mjs +1 -2
  223. package/dist/capture/publish/run.d.mts +1 -3
  224. package/dist/capture/publish/run.mjs +1 -2
  225. package/dist/capture/rediff.d.mts +1 -3
  226. package/dist/capture/rediff.mjs +19 -6
  227. package/dist/capture/rediff.mjs.map +1 -1
  228. package/dist/capture/render.mjs +1 -2
  229. package/dist/capture/render.mjs.map +1 -1
  230. package/dist/capture/scratch.d.mts +0 -2
  231. package/dist/capture/scratch.mjs +2 -4
  232. package/dist/capture/scratch.mjs.map +1 -1
  233. package/dist/capture/shard-plan.mjs +1 -2
  234. package/dist/capture/shards/context.mjs +3 -3
  235. package/dist/capture/shards/context.mjs.map +1 -1
  236. package/dist/capture/shards/execute.mjs +4 -3
  237. package/dist/capture/shards/execute.mjs.map +1 -1
  238. package/dist/capture/shards/fan-out.mjs +1 -2
  239. package/dist/capture/shards/history.mjs +1 -2
  240. package/dist/capture/shards/reduce.mjs +1 -2
  241. package/dist/capture/sizing.d.mts +0 -2
  242. package/dist/capture/sizing.mjs +1 -2
  243. package/dist/capture/skip-checks.mjs +1 -2
  244. package/dist/capture/statics.d.mts +0 -2
  245. package/dist/capture/statics.mjs +2 -4
  246. package/dist/capture/statics.mjs.map +1 -1
  247. package/dist/capture/storybook-urls.d.mts +0 -2
  248. package/dist/capture/storybook-urls.mjs +3 -3
  249. package/dist/capture/storybook-urls.mjs.map +1 -1
  250. package/dist/capture/storybook.d.mts +0 -2
  251. package/dist/capture/storybook.mjs +8 -4
  252. package/dist/capture/storybook.mjs.map +1 -1
  253. package/dist/capture/target.mjs +1 -2
  254. package/dist/capture/targets.d.mts +0 -2
  255. package/dist/capture/targets.mjs +1 -2
  256. package/dist/capture/tests.d.mts +0 -2
  257. package/dist/capture/tests.mjs +13 -3
  258. package/dist/capture/tests.mjs.map +1 -1
  259. package/dist/capture/token-chunks.mjs +24 -7
  260. package/dist/capture/token-chunks.mjs.map +1 -1
  261. package/dist/capture.d.mts +5 -2
  262. package/dist/capture.mjs +5 -2
  263. package/dist/config.d.mts +16 -3
  264. package/dist/config.mjs +6 -3
  265. package/dist/config.mjs.map +1 -1
  266. package/dist/diff.d.mts +0 -2
  267. package/dist/diff.mjs +1 -2
  268. package/dist/index.d.mts +2 -2
  269. package/dist/index.mjs +2 -2
  270. package/dist/insights/budget.d.mts +0 -2
  271. package/dist/insights/budget.mjs +1 -2
  272. package/dist/insights/envelope.d.mts +0 -2
  273. package/dist/insights/envelope.mjs +1 -2
  274. package/dist/insights/evidence-health.d.mts +0 -2
  275. package/dist/insights/evidence-health.mjs +1 -2
  276. package/dist/insights/evidence.d.mts +0 -2
  277. package/dist/insights/evidence.mjs +1 -2
  278. package/dist/insights/hash.d.mts +0 -2
  279. package/dist/insights/hash.mjs +1 -2
  280. package/dist/insights/limits.d.mts +0 -2
  281. package/dist/insights/limits.mjs +1 -2
  282. package/dist/insights/prompts/v1.d.mts +0 -2
  283. package/dist/insights/prompts/v1.mjs +1 -2
  284. package/dist/insights/redact.d.mts +0 -2
  285. package/dist/insights/redact.mjs +1 -2
  286. package/dist/insights/text.mjs +1 -2
  287. package/dist/logger.d.mts +0 -2
  288. package/dist/logger.mjs +1 -2
  289. package/dist/models/ai-budget-alert.d.mts +0 -2
  290. package/dist/models/ai-budget-alert.mjs +1 -2
  291. package/dist/models/ai-usage.d.mts +0 -2
  292. package/dist/models/ai-usage.mjs +1 -2
  293. package/dist/models/baseline-approval.d.mts +18 -0
  294. package/dist/models/baseline-approval.mjs +41 -0
  295. package/dist/models/baseline-approval.mjs.map +1 -0
  296. package/dist/models/baseline-archive.d.mts +0 -2
  297. package/dist/models/baseline-archive.mjs +1 -2
  298. package/dist/models/baseline-atomic.mjs +66 -0
  299. package/dist/models/baseline-atomic.mjs.map +1 -0
  300. package/dist/models/baseline-conflict.d.mts +6 -0
  301. package/dist/models/baseline-conflict.mjs +9 -0
  302. package/dist/models/baseline-conflict.mjs.map +1 -0
  303. package/dist/models/baseline-export.d.mts +0 -2
  304. package/dist/models/baseline-export.mjs +1 -2
  305. package/dist/models/baseline-import.mjs +1 -2
  306. package/dist/models/baseline-origin.d.mts +16 -0
  307. package/dist/models/baseline-origin.mjs +35 -0
  308. package/dist/models/baseline-origin.mjs.map +1 -0
  309. package/dist/models/baseline-status.d.mts +0 -2
  310. package/dist/models/baseline-status.mjs +1 -2
  311. package/dist/models/baseline-transfer.d.mts +1 -2
  312. package/dist/models/baseline-transfer.mjs +30 -2
  313. package/dist/models/baseline-transfer.mjs.map +1 -1
  314. package/dist/models/baseline.d.mts +5 -2
  315. package/dist/models/baseline.mjs +91 -12
  316. package/dist/models/baseline.mjs.map +1 -1
  317. package/dist/models/build-component.d.mts +0 -2
  318. package/dist/models/build-component.mjs +1 -2
  319. package/dist/models/build-review-status.d.mts +0 -2
  320. package/dist/models/build-review-status.mjs +1 -2
  321. package/dist/models/build-review.d.mts +0 -2
  322. package/dist/models/build-review.mjs +1 -2
  323. package/dist/models/build.d.mts +1 -3
  324. package/dist/models/build.mjs +14 -15
  325. package/dist/models/build.mjs.map +1 -1
  326. package/dist/models/capture-attempt.d.mts +0 -2
  327. package/dist/models/capture-attempt.mjs +1 -2
  328. package/dist/models/capture-job.d.mts +0 -2
  329. package/dist/models/capture-job.mjs +1 -2
  330. package/dist/models/capture-log.d.mts +0 -2
  331. package/dist/models/capture-log.mjs +1 -2
  332. package/dist/models/capture-shard.d.mts +0 -2
  333. package/dist/models/capture-shard.mjs +1 -2
  334. package/dist/models/changeset.d.mts +0 -2
  335. package/dist/models/changeset.mjs +1 -2
  336. package/dist/models/columns.mjs +1 -2
  337. package/dist/models/comment.d.mts +0 -2
  338. package/dist/models/comment.mjs +1 -2
  339. package/dist/models/component.d.mts +0 -2
  340. package/dist/models/component.mjs +1 -2
  341. package/dist/models/connections.d.mts +0 -2
  342. package/dist/models/connections.mjs +1 -2
  343. package/dist/models/content-ref.d.mts +0 -2
  344. package/dist/models/content-ref.mjs +1 -2
  345. package/dist/models/credentials.d.mts +0 -2
  346. package/dist/models/credentials.mjs +1 -2
  347. package/dist/models/design-credentials.d.mts +0 -2
  348. package/dist/models/design-credentials.mjs +1 -2
  349. package/dist/models/design-health.d.mts +16 -3
  350. package/dist/models/design-health.mjs +21 -3
  351. package/dist/models/design-health.mjs.map +1 -1
  352. package/dist/models/design-nodes.d.mts +11 -4
  353. package/dist/models/design-nodes.mjs +18 -3
  354. package/dist/models/design-nodes.mjs.map +1 -1
  355. package/dist/models/design-renders.mjs +1 -2
  356. package/dist/models/design-resolve.d.mts +0 -2
  357. package/dist/models/design-resolve.mjs +1 -2
  358. package/dist/models/design.d.mts +0 -2
  359. package/dist/models/design.mjs +1 -2
  360. package/dist/models/inherited-ratio.d.mts +0 -2
  361. package/dist/models/inherited-ratio.mjs +1 -2
  362. package/dist/models/insight.d.mts +0 -2
  363. package/dist/models/insight.mjs +1 -2
  364. package/dist/models/label.d.mts +0 -2
  365. package/dist/models/label.mjs +1 -2
  366. package/dist/models/member.d.mts +0 -2
  367. package/dist/models/member.mjs +1 -2
  368. package/dist/models/notification-channel.d.mts +0 -2
  369. package/dist/models/notification-channel.mjs +1 -2
  370. package/dist/models/notification-subscription.d.mts +0 -2
  371. package/dist/models/notification-subscription.mjs +1 -2
  372. package/dist/models/project-group-mapping.d.mts +0 -2
  373. package/dist/models/project-group-mapping.mjs +1 -2
  374. package/dist/models/project.d.mts +0 -2
  375. package/dist/models/project.mjs +1 -2
  376. package/dist/models/public-origin.d.mts +0 -2
  377. package/dist/models/public-origin.mjs +1 -2
  378. package/dist/models/review-switch-off.d.mts +0 -2
  379. package/dist/models/review-switch-off.mjs +1 -2
  380. package/dist/models/site-settings.d.mts +0 -2
  381. package/dist/models/site-settings.mjs +1 -2
  382. package/dist/models/snapshot.d.mts +3 -2
  383. package/dist/models/snapshot.mjs +4 -2
  384. package/dist/models/snapshot.mjs.map +1 -1
  385. package/dist/models/token.d.mts +0 -2
  386. package/dist/models/token.mjs +1 -2
  387. package/dist/models/usage.d.mts +0 -2
  388. package/dist/models/usage.mjs +1 -2
  389. package/dist/models/user.d.mts +0 -2
  390. package/dist/models/user.mjs +1 -2
  391. package/dist/models/webhook.d.mts +0 -2
  392. package/dist/models/webhook.mjs +1 -2
  393. package/dist/models.d.mts +6 -3
  394. package/dist/models.mjs +6 -3
  395. package/dist/options.d.mts +0 -2
  396. package/dist/options.mjs +2 -4
  397. package/dist/options.mjs.map +1 -1
  398. package/dist/paths.d.mts +3 -3
  399. package/dist/paths.mjs +7 -3
  400. package/dist/paths.mjs.map +1 -1
  401. package/dist/retention/ai.d.mts +1 -3
  402. package/dist/retention/ai.mjs +0 -1
  403. package/dist/retention/pending.mjs +1 -2
  404. package/dist/retention/published-gzip-sweep.mjs +28 -0
  405. package/dist/retention/published-gzip-sweep.mjs.map +1 -0
  406. package/dist/retention/screenshot-sweep.d.mts +0 -2
  407. package/dist/retention/screenshot-sweep.mjs +1 -2
  408. package/dist/retention.d.mts +0 -2
  409. package/dist/retention.mjs +5 -2
  410. package/dist/retention.mjs.map +1 -1
  411. package/dist/schema/baseline.d.mts +2 -2
  412. package/dist/schema/build-component.d.mts +0 -2
  413. package/dist/schema/build-review.d.mts +0 -2
  414. package/dist/schema/build.d.mts +0 -2
  415. package/dist/schema/capture-attempt.d.mts +0 -2
  416. package/dist/schema/capture-job.d.mts +0 -2
  417. package/dist/schema/capture-shard.d.mts +0 -2
  418. package/dist/schema/comment.d.mts +0 -2
  419. package/dist/schema/connection.d.mts +0 -2
  420. package/dist/schema/content-ref.d.mts +0 -2
  421. package/dist/schema/design.d.mts +2 -2
  422. package/dist/schema/insight.d.mts +0 -2
  423. package/dist/schema/label.d.mts +0 -2
  424. package/dist/schema/member.d.mts +0 -2
  425. package/dist/schema/notification.d.mts +0 -2
  426. package/dist/schema/project-group-mapping.d.mts +0 -2
  427. package/dist/schema/project.d.mts +0 -2
  428. package/dist/schema/site-setting.d.mts +0 -2
  429. package/dist/schema/snapshot.d.mts +5 -2
  430. package/dist/schema/token.d.mts +0 -2
  431. package/dist/schema/usage.d.mts +0 -2
  432. package/dist/schema/user.d.mts +0 -2
  433. package/dist/schema/webhook.d.mts +0 -2
  434. package/dist/schema.d.mts +4 -6
  435. package/dist/serverless.d.mts +0 -2
  436. package/dist/serverless.mjs +1 -2
  437. package/dist/test-helpers/build-reference-contract.d.mts +14 -0
  438. package/dist/test-helpers/build-reference-contract.mjs +66 -0
  439. package/dist/test-helpers/build-reference-contract.mjs.map +1 -0
  440. package/dist/test-helpers/create-project.d.mts +0 -2
  441. package/dist/test-helpers/create-project.mjs +1 -2
  442. package/dist/test-helpers/database-contract.d.mts +0 -2
  443. package/dist/test-helpers/database-contract.mjs +37 -2
  444. package/dist/test-helpers/database-contract.mjs.map +1 -1
  445. package/dist/test-helpers/deferred.d.mts +0 -2
  446. package/dist/test-helpers/deferred.mjs +1 -2
  447. package/dist/test-helpers/design-contract.d.mts +0 -2
  448. package/dist/test-helpers/design-contract.mjs +1 -2
  449. package/dist/test-helpers/fake-adapters.d.mts +0 -2
  450. package/dist/test-helpers/fake-adapters.mjs +1 -2
  451. package/dist/test-helpers/fake-database.d.mts +0 -2
  452. package/dist/test-helpers/fake-database.mjs +7 -2
  453. package/dist/test-helpers/fake-database.mjs.map +1 -1
  454. package/dist/test-helpers/fake-design.d.mts +0 -2
  455. package/dist/test-helpers/fake-design.mjs +1 -2
  456. package/dist/test-helpers/fake-notifier.d.mts +0 -2
  457. package/dist/test-helpers/fake-notifier.mjs +1 -2
  458. package/dist/test-helpers/fake-storage.d.mts +0 -2
  459. package/dist/test-helpers/fake-storage.mjs +1 -2
  460. package/dist/test-helpers/fake-tables.mjs +6 -2
  461. package/dist/test-helpers/fake-tables.mjs.map +1 -1
  462. package/dist/test-helpers/manual-clock.d.mts +0 -2
  463. package/dist/test-helpers/manual-clock.mjs +1 -2
  464. package/dist/test-helpers/notifier-contract.d.mts +0 -2
  465. package/dist/test-helpers/notifier-contract.mjs +1 -2
  466. package/dist/test-helpers/queue-contract.d.mts +0 -2
  467. package/dist/test-helpers/queue-contract.mjs +1 -2
  468. package/dist/test-helpers/queue-multi-instance.d.mts +0 -2
  469. package/dist/test-helpers/queue-multi-instance.mjs +1 -2
  470. package/dist/test-helpers/runner-contract.d.mts +0 -2
  471. package/dist/test-helpers/runner-contract.mjs +1 -2
  472. package/dist/test-helpers/sql-chunks.mjs +2 -2
  473. package/dist/test-helpers/sql-chunks.mjs.map +1 -1
  474. package/dist/test-helpers/storage-contract.d.mts +0 -2
  475. package/dist/test-helpers/storage-contract.mjs +1 -2
  476. package/dist/test-helpers.d.mts +2 -1
  477. package/dist/test-helpers.mjs +2 -1
  478. package/dist/tracing.mjs +36 -6
  479. package/dist/tracing.mjs.map +1 -1
  480. package/dist/types.d.mts +0 -2
  481. package/dist/types.mjs +1 -2
  482. package/dist/urls.d.mts +0 -2
  483. package/dist/urls.mjs +1 -2
  484. package/dist/utils/adaptive-concurrency.d.mts +0 -2
  485. package/dist/utils/adaptive-concurrency.mjs +1 -2
  486. package/dist/utils/connect-retry.d.mts +2 -2
  487. package/dist/utils/connect-retry.mjs +2 -3
  488. package/dist/utils/connect-retry.mjs.map +1 -1
  489. package/dist/utils/encrypt.d.mts +0 -2
  490. package/dist/utils/encrypt.mjs +1 -2
  491. package/dist/utils/environment.d.mts +0 -2
  492. package/dist/utils/environment.mjs +1 -2
  493. package/dist/utils/hash.d.mts +0 -2
  494. package/dist/utils/hash.mjs +1 -2
  495. package/dist/utils/http.d.mts +13 -3
  496. package/dist/utils/http.mjs +79 -17
  497. package/dist/utils/http.mjs.map +1 -1
  498. package/dist/utils/map-adaptive.d.mts +0 -2
  499. package/dist/utils/map-adaptive.mjs +1 -2
  500. package/dist/utils/map-limit.d.mts +0 -2
  501. package/dist/utils/map-limit.mjs +1 -2
  502. package/dist/utils/mime.d.mts +0 -2
  503. package/dist/utils/mime.mjs +1 -2
  504. package/dist/utils/page-limiter.d.mts +0 -2
  505. package/dist/utils/page-limiter.mjs +1 -2
  506. package/dist/utils/path-security.d.mts +0 -2
  507. package/dist/utils/path-security.mjs +2 -4
  508. package/dist/utils/path-security.mjs.map +1 -1
  509. package/dist/utils/public-origin.d.mts +0 -2
  510. package/dist/utils/public-origin.mjs +1 -2
  511. package/dist/utils/redact.d.mts +0 -2
  512. package/dist/utils/redact.mjs +1 -2
  513. package/dist/utils/remote-environment.d.mts +40 -3
  514. package/dist/utils/remote-environment.mjs +52 -3
  515. package/dist/utils/remote-environment.mjs.map +1 -1
  516. package/dist/utils/resources.d.mts +0 -2
  517. package/dist/utils/resources.mjs +1 -2
  518. package/dist/utils/screenshot-store.d.mts +0 -2
  519. package/dist/utils/screenshot-store.mjs +1 -2
  520. package/dist/utils/session-gate.d.mts +7 -6
  521. package/dist/utils/session-gate.mjs +28 -12
  522. package/dist/utils/session-gate.mjs.map +1 -1
  523. package/dist/utils/storybook-token.d.mts +11 -3
  524. package/dist/utils/storybook-token.mjs +10 -9
  525. package/dist/utils/storybook-token.mjs.map +1 -1
  526. package/dist/utils/storybook-url.d.mts +0 -2
  527. package/dist/utils/storybook-url.mjs +1 -2
  528. package/dist/utils/timing.d.mts +0 -2
  529. package/dist/utils/timing.mjs +1 -2
  530. package/dist/utils/ulid.d.mts +0 -2
  531. package/dist/utils/ulid.mjs +1 -2
  532. package/dist/utils/zip-read.mjs +1 -2
  533. package/dist/utils/zip-write.d.mts +0 -2
  534. package/dist/utils/zip-write.mjs +1 -2
  535. package/dist/utils.d.mts +5 -5
  536. package/dist/utils.mjs +5 -5
  537. package/package.json +6 -15
package/dist/models.mjs CHANGED
@@ -4,8 +4,10 @@ import.meta.url;
4
4
  import { WebhookModel } from "./models/webhook.mjs";
5
5
  import { CaptureAttemptModel, emitAttemptLog } from "./models/capture-attempt.mjs";
6
6
  import { BuildModel, isPublicBuild } from "./models/build.mjs";
7
- import { BaselineModel } from "./models/baseline.mjs";
7
+ import { BaselineWriteConflict } from "./models/baseline-conflict.mjs";
8
8
  import { NO_BASELINE, baselineStatus, isBaselineDrifted } from "./models/baseline-status.mjs";
9
+ import { BaselineApprovalConflict } from "./models/baseline-approval.mjs";
10
+ import { BaselineModel } from "./models/baseline.mjs";
9
11
  import { SnapshotModel } from "./models/snapshot.mjs";
10
12
  import { CaptureShardModel } from "./models/capture-shard.mjs";
11
13
  import { ProjectModel } from "./models/project.mjs";
@@ -18,6 +20,7 @@ import { PUBLIC_ORIGIN_KEY, createPublicOriginResolver } from "./models/public-o
18
20
  import { CaptureLogModel } from "./models/capture-log.mjs";
19
21
  import { BuildComponentModel } from "./models/build-component.mjs";
20
22
  import { normalizeImportPath, resolveDesigns } from "./models/design-resolve.mjs";
23
+ import { resolveBaselineOrigin } from "./models/baseline-origin.mjs";
21
24
  import { BASELINE_ARCHIVE_FORMAT, BASELINE_ARCHIVE_LIMITS, BASELINE_ARCHIVE_VERSION, BaselineArchiveError } from "./models/baseline-archive.mjs";
22
25
  import { BaselineTransferModel } from "./models/baseline-transfer.mjs";
23
26
  import { AiBudgetAlertModel } from "./models/ai-budget-alert.mjs";
@@ -31,7 +34,7 @@ import { componentHistory, componentPresence, worstStatus } from "./models/compo
31
34
  import { ConnectionModel } from "./models/connections.mjs";
32
35
  import { DesignModel } from "./models/design.mjs";
33
36
  import { DesignCredentialModel, PROJECT_SCOPE } from "./models/design-credentials.mjs";
34
- import { DESIGN_TTL_MS, credentialDaysLeft, isDesignStale } from "./models/design-health.mjs";
37
+ import { DESIGN_TTL_MS, credentialDaysLeft, designChange, isDesignStale } from "./models/design-health.mjs";
35
38
  import { DesignNodeModel } from "./models/design-nodes.mjs";
36
39
  import { LabelModel } from "./models/label.mjs";
37
40
  import { PackageUsageModel, matchUpstreamStory } from "./models/usage.mjs";
@@ -42,4 +45,4 @@ import { TokenModel } from "./models/token.mjs";
42
45
  import { UserModel } from "./models/user.mjs";
43
46
  import { ContentRefModel } from "./models/content-ref.mjs";
44
47
  import { probeCredentials, reencryptCredentials } from "./models/credentials.mjs";
45
- export { AiBudgetAlertModel, AiUsageModel, BASELINE_ARCHIVE_FORMAT, BASELINE_ARCHIVE_LIMITS, BASELINE_ARCHIVE_VERSION, BaselineArchiveError, BaselineModel, BaselineTransferModel, BuildComponentModel, BuildModel, BuildReviewModel, CaptureAttemptModel, CaptureJobModel, CaptureLogModel, CaptureShardModel, CommentModel, ConnectionModel, ContentRefModel, DESIGN_TTL_MS, DesignCredentialModel, DesignModel, DesignNodeModel, InsightModel, LabelModel, MemberModel, NO_BASELINE, NotificationChannelModel, NotificationSubscriptionModel, PROJECT_SCOPE, PUBLIC_ORIGIN_KEY, PackageUsageModel, ProjectGroupMappingModel, ProjectModel, RECENT_BUILDS_FOR_INHERITED_RATIO, SiteSettingsModel, SnapshotModel, TERMINAL_JOB_STATUSES, TokenModel, UserModel, WebhookModel, acceptPendingWithoutWork, attemptRowId, averageInheritedRatio, baselineStatus, changesetOf, componentHistory, componentPresence, computeChangeset, createPublicOriginResolver, credentialDaysLeft, deriveReviewStatus, emitAttemptLog, findChangesetBaseline, isBaselineDrifted, isDesignStale, isPublicBuild, isTerminalJob, latestVerdicts, matchUpstreamStory, normalizeImportPath, parseAttemptRowId, probeCredentials, reencryptCredentials, resolveDesigns, worstStatus };
48
+ export { AiBudgetAlertModel, AiUsageModel, BASELINE_ARCHIVE_FORMAT, BASELINE_ARCHIVE_LIMITS, BASELINE_ARCHIVE_VERSION, BaselineApprovalConflict, BaselineArchiveError, BaselineModel, BaselineTransferModel, BaselineWriteConflict, BuildComponentModel, BuildModel, BuildReviewModel, CaptureAttemptModel, CaptureJobModel, CaptureLogModel, CaptureShardModel, CommentModel, ConnectionModel, ContentRefModel, DESIGN_TTL_MS, DesignCredentialModel, DesignModel, DesignNodeModel, InsightModel, LabelModel, MemberModel, NO_BASELINE, NotificationChannelModel, NotificationSubscriptionModel, PROJECT_SCOPE, PUBLIC_ORIGIN_KEY, PackageUsageModel, ProjectGroupMappingModel, ProjectModel, RECENT_BUILDS_FOR_INHERITED_RATIO, SiteSettingsModel, SnapshotModel, TERMINAL_JOB_STATUSES, TokenModel, UserModel, WebhookModel, acceptPendingWithoutWork, attemptRowId, averageInheritedRatio, baselineStatus, changesetOf, componentHistory, componentPresence, computeChangeset, createPublicOriginResolver, credentialDaysLeft, deriveReviewStatus, designChange, emitAttemptLog, findChangesetBaseline, isBaselineDrifted, isDesignStale, isPublicBuild, isTerminalJob, latestVerdicts, matchUpstreamStory, normalizeImportPath, parseAttemptRowId, probeCredentials, reencryptCredentials, resolveBaselineOrigin, resolveDesigns, worstStatus };
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { ShelfOptions } from "./config.mjs";
4
- //#region src/options.d.ts
5
4
  /** Where capture scratch lives when `config.scratchDir` is not set. */
6
5
  declare const DEFAULT_SCRATCH_DIR: string;
7
6
  /**
@@ -13,6 +12,5 @@ declare const DEFAULT_SCRATCH_DIR: string;
13
12
  * with or without `testing`.
14
13
  */
15
14
  declare function resolveShelfOptions(options: ShelfOptions): ShelfOptions;
16
- //#endregion
17
15
  export { DEFAULT_SCRATCH_DIR, resolveShelfOptions };
18
16
  //# sourceMappingURL=options.d.mts.map
package/dist/options.mjs CHANGED
@@ -1,10 +1,9 @@
1
- import "node:path";
1
+ import { join } from "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { notificationsConfigSchema } from "./config.mjs";
5
5
  import { tmpdir } from "node:os";
6
- import { join } from "node:path";
7
- //#region src/options.ts
6
+ // src/options.ts
8
7
  /** Resolves `ShelfOptions` once at boot: validates `testing.compare` and fills the scratch default. */
9
8
  /** Where capture scratch lives when `config.scratchDir` is not set. */
10
9
  const DEFAULT_SCRATCH_DIR = join(tmpdir(), "storyshelf");
@@ -84,7 +83,6 @@ function assertNotificationsConfig(config) {
84
83
  throw new Error(`Invalid notifications.config: ${issues}`);
85
84
  }
86
85
  }
87
- //#endregion
88
86
  export { DEFAULT_SCRATCH_DIR, resolveShelfOptions };
89
87
 
90
88
  //# sourceMappingURL=options.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"options.mjs","names":[],"sources":["../src/options.ts"],"sourcesContent":["/** Resolves `ShelfOptions` once at boot: validates `testing.compare` and fills the scratch default. */\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { notificationsConfigSchema, type ShelfOptions } from \"./config.ts\";\n\n/** Where capture scratch lives when `config.scratchDir` is not set. */\nexport const DEFAULT_SCRATCH_DIR: string = join(tmpdir(), \"storyshelf\");\n\n/**\n * Validate and complete the options.\n *\n * - `testing.compare.threads` must be a non-negative integer.\n * - `notifications.config` must match its schema (it used to live in `ShelfConfig`, which validated it).\n * - `config.scratchDir` defaults to {@link DEFAULT_SCRATCH_DIR}: publishing extracts zips there,\n * with or without `testing`.\n */\nexport function resolveShelfOptions(options: ShelfOptions): ShelfOptions {\n assertThreads(options.testing?.compare?.threads);\n assertTestingProcess(options.testing);\n assertTestingBackable(options);\n assertNotificationsConfig(options.notifications?.config);\n if (options.config?.scratchDir !== undefined) {\n return options;\n }\n return { ...options, config: { ...options.config, scratchDir: DEFAULT_SCRATCH_DIR } };\n}\n\n/**\n * A `testing` instance renders here, so it needs a database a long-running server can keep.\n */\nfunction assertTestingBackable(options: ShelfOptions): void {\n if (options.testing === undefined) {\n return;\n }\n if (options.database.metadata.supportsTesting === false) {\n throw new Error(\n `The ${options.database.metadata.name} database cannot back testing: it is reachable only ` +\n `from a platform that ends the process after each request, so nothing would run capture ` +\n `jobs. Remove the testing option to host Storybooks only, or use a network database ` +\n `(Turso, Postgres, MySQL, SQLite on a volume).`,\n );\n }\n}\n\nfunction assertTestingProcess(testing: ShelfOptions[\"testing\"]): void {\n if (testing !== undefined && testing.runner === undefined) {\n throw new Error(\"testing requires testing.runner\");\n }\n if (testing?.workerEntry !== undefined && testing.runner === undefined) {\n throw new Error(\"testing.workerEntry requires testing.runner (it is the in-process fallback)\");\n }\n if (testing?.mode === \"fork\" && testing.workerEntry === undefined) {\n throw new Error('testing.mode \"fork\" requires testing.workerEntry');\n }\n assertShardSize(testing);\n assertTestingNumbers(testing);\n}\n\n/** Numeric `testing` options: concurrency, retries and the Storybook URL rate limit. */\nfunction assertTestingNumbers(testing: ShelfOptions[\"testing\"]): void {\n assertConcurrency(testing?.concurrency);\n assertRetries(testing?.retries);\n assertLeaseSeconds(testing?.leaseSeconds);\n assertStorybookUrlRateLimit(testing?.storybookUrlRateLimit);\n}\n\n/** A whole number of requests per minute; `0` would refuse every render. */\nfunction assertStorybookUrlRateLimit(limit: number | undefined): void {\n if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {\n throw new Error(\n `testing.storybookUrlRateLimit must be a positive whole number of requests per minute, got ${String(limit)}`,\n );\n }\n}\n\n/** Retries are counted after the first attempt: `0` runs a job once. */\nfunction assertRetries(retries: number | undefined): void {\n if (retries !== undefined && (!Number.isInteger(retries) || retries < 0)) {\n throw new Error(`testing.retries must be a non-negative integer, got ${String(retries)}`);\n }\n}\n\n/** A lease must be a positive length: `0` would expire every job at once. */\nfunction assertLeaseSeconds(seconds: number | undefined): void {\n if (seconds !== undefined && (!Number.isFinite(seconds) || seconds <= 0)) {\n throw new Error(\n `testing.leaseSeconds must be a positive number of seconds, got ${String(seconds)}`,\n );\n }\n}\n\n/** `0` would deadlock the consumer. */\nfunction assertConcurrency(concurrency: number | undefined): void {\n if (concurrency !== undefined && (!Number.isInteger(concurrency) || concurrency < 1)) {\n throw new Error(`testing.concurrency must be a positive integer, got ${String(concurrency)}`);\n }\n}\n\nfunction assertShardSize(testing: ShelfOptions[\"testing\"]): void {\n const size = testing?.shardSize;\n if (size === undefined) {\n return;\n }\n if (!Number.isInteger(size) || size < 1) {\n throw new Error(`testing.shardSize must be a positive integer, got ${String(size)}`);\n }\n if (testing?.runner === undefined) {\n throw new Error(\"testing.shardSize requires testing.runner (workers set their own shardSize)\");\n }\n}\n\nfunction assertThreads(threads: number | undefined): void {\n if (threads !== undefined && (!Number.isInteger(threads) || threads < 0)) {\n throw new Error(\n `testing.compare.threads must be a non-negative integer, got ${String(threads)}`,\n );\n }\n}\n\nfunction assertNotificationsConfig(config: unknown): void {\n if (config === undefined) {\n return;\n }\n const result = notificationsConfigSchema.safeParse(config);\n if (!result.success) {\n const issues = result.error.issues.map((i) => `${i.path.join(\".\")}: ${i.message}`).join(\"; \");\n throw new Error(`Invalid notifications.config: ${issues}`);\n }\n}\n"],"mappings":";;;;;;;;;AAMA,MAAa,sBAA8B,KAAK,OAAO,GAAG,YAAY;;;;;;;;;AAUtE,SAAgB,oBAAoB,SAAqC;CACvE,cAAc,QAAQ,SAAS,SAAS,OAAO;CAC/C,qBAAqB,QAAQ,OAAO;CACpC,sBAAsB,OAAO;CAC7B,0BAA0B,QAAQ,eAAe,MAAM;CACvD,IAAI,QAAQ,QAAQ,eAAe,KAAA,GACjC,OAAO;CAET,OAAO;EAAE,GAAG;EAAS,QAAQ;GAAE,GAAG,QAAQ;GAAQ,YAAY;EAAoB;CAAE;AACtF;;;;AAKA,SAAS,sBAAsB,SAA6B;CAC1D,IAAI,QAAQ,YAAY,KAAA,GACtB;CAEF,IAAI,QAAQ,SAAS,SAAS,oBAAoB,OAChD,MAAM,IAAI,MACR,OAAO,QAAQ,SAAS,SAAS,KAAK,4QAIxC;AAEJ;AAEA,SAAS,qBAAqB,SAAwC;CACpE,IAAI,YAAY,KAAA,KAAa,QAAQ,WAAW,KAAA,GAC9C,MAAM,IAAI,MAAM,iCAAiC;CAEnD,IAAI,SAAS,gBAAgB,KAAA,KAAa,QAAQ,WAAW,KAAA,GAC3D,MAAM,IAAI,MAAM,6EAA6E;CAE/F,IAAI,SAAS,SAAS,UAAU,QAAQ,gBAAgB,KAAA,GACtD,MAAM,IAAI,MAAM,oDAAkD;CAEpE,gBAAgB,OAAO;CACvB,qBAAqB,OAAO;AAC9B;;AAGA,SAAS,qBAAqB,SAAwC;CACpE,kBAAkB,SAAS,WAAW;CACtC,cAAc,SAAS,OAAO;CAC9B,mBAAmB,SAAS,YAAY;CACxC,4BAA4B,SAAS,qBAAqB;AAC5D;;AAGA,SAAS,4BAA4B,OAAiC;CACpE,IAAI,UAAU,KAAA,MAAc,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,IAC9D,MAAM,IAAI,MACR,6FAA6F,OAAO,KAAK,GAC3G;AAEJ;;AAGA,SAAS,cAAc,SAAmC;CACxD,IAAI,YAAY,KAAA,MAAc,CAAC,OAAO,UAAU,OAAO,KAAK,UAAU,IACpE,MAAM,IAAI,MAAM,uDAAuD,OAAO,OAAO,GAAG;AAE5F;;AAGA,SAAS,mBAAmB,SAAmC;CAC7D,IAAI,YAAY,KAAA,MAAc,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,IACpE,MAAM,IAAI,MACR,kEAAkE,OAAO,OAAO,GAClF;AAEJ;;AAGA,SAAS,kBAAkB,aAAuC;CAChE,IAAI,gBAAgB,KAAA,MAAc,CAAC,OAAO,UAAU,WAAW,KAAK,cAAc,IAChF,MAAM,IAAI,MAAM,uDAAuD,OAAO,WAAW,GAAG;AAEhG;AAEA,SAAS,gBAAgB,SAAwC;CAC/D,MAAM,OAAO,SAAS;CACtB,IAAI,SAAS,KAAA,GACX;CAEF,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,GACpC,MAAM,IAAI,MAAM,qDAAqD,OAAO,IAAI,GAAG;CAErF,IAAI,SAAS,WAAW,KAAA,GACtB,MAAM,IAAI,MAAM,6EAA6E;AAEjG;AAEA,SAAS,cAAc,SAAmC;CACxD,IAAI,YAAY,KAAA,MAAc,CAAC,OAAO,UAAU,OAAO,KAAK,UAAU,IACpE,MAAM,IAAI,MACR,+DAA+D,OAAO,OAAO,GAC/E;AAEJ;AAEA,SAAS,0BAA0B,QAAuB;CACxD,IAAI,WAAW,KAAA,GACb;CAEF,MAAM,SAAS,0BAA0B,UAAU,MAAM;CACzD,IAAI,CAAC,OAAO,SAAS;EACnB,MAAM,SAAS,OAAO,MAAM,OAAO,KAAK,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,KAAK,IAAI;EAC5F,MAAM,IAAI,MAAM,iCAAiC,QAAQ;CAC3D;AACF"}
1
+ {"version":3,"file":"options.mjs","names":[],"sources":["../src/options.ts"],"sourcesContent":["/** Resolves `ShelfOptions` once at boot: validates `testing.compare` and fills the scratch default. */\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { notificationsConfigSchema, type ShelfOptions } from \"./config.ts\";\n\n/** Where capture scratch lives when `config.scratchDir` is not set. */\nexport const DEFAULT_SCRATCH_DIR: string = join(tmpdir(), \"storyshelf\");\n\n/**\n * Validate and complete the options.\n *\n * - `testing.compare.threads` must be a non-negative integer.\n * - `notifications.config` must match its schema (it used to live in `ShelfConfig`, which validated it).\n * - `config.scratchDir` defaults to {@link DEFAULT_SCRATCH_DIR}: publishing extracts zips there,\n * with or without `testing`.\n */\nexport function resolveShelfOptions(options: ShelfOptions): ShelfOptions {\n assertThreads(options.testing?.compare?.threads);\n assertTestingProcess(options.testing);\n assertTestingBackable(options);\n assertNotificationsConfig(options.notifications?.config);\n if (options.config?.scratchDir !== undefined) {\n return options;\n }\n return { ...options, config: { ...options.config, scratchDir: DEFAULT_SCRATCH_DIR } };\n}\n\n/**\n * A `testing` instance renders here, so it needs a database a long-running server can keep.\n */\nfunction assertTestingBackable(options: ShelfOptions): void {\n if (options.testing === undefined) {\n return;\n }\n if (options.database.metadata.supportsTesting === false) {\n throw new Error(\n `The ${options.database.metadata.name} database cannot back testing: it is reachable only ` +\n `from a platform that ends the process after each request, so nothing would run capture ` +\n `jobs. Remove the testing option to host Storybooks only, or use a network database ` +\n `(Turso, Postgres, MySQL, SQLite on a volume).`,\n );\n }\n}\n\nfunction assertTestingProcess(testing: ShelfOptions[\"testing\"]): void {\n if (testing !== undefined && testing.runner === undefined) {\n throw new Error(\"testing requires testing.runner\");\n }\n if (testing?.workerEntry !== undefined && testing.runner === undefined) {\n throw new Error(\"testing.workerEntry requires testing.runner (it is the in-process fallback)\");\n }\n if (testing?.mode === \"fork\" && testing.workerEntry === undefined) {\n throw new Error('testing.mode \"fork\" requires testing.workerEntry');\n }\n assertShardSize(testing);\n assertTestingNumbers(testing);\n}\n\n/** Numeric `testing` options: concurrency, retries and the Storybook URL rate limit. */\nfunction assertTestingNumbers(testing: ShelfOptions[\"testing\"]): void {\n assertConcurrency(testing?.concurrency);\n assertRetries(testing?.retries);\n assertLeaseSeconds(testing?.leaseSeconds);\n assertStorybookUrlRateLimit(testing?.storybookUrlRateLimit);\n}\n\n/** A whole number of requests per minute; `0` would refuse every render. */\nfunction assertStorybookUrlRateLimit(limit: number | undefined): void {\n if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {\n throw new Error(\n `testing.storybookUrlRateLimit must be a positive whole number of requests per minute, got ${String(limit)}`,\n );\n }\n}\n\n/** Retries are counted after the first attempt: `0` runs a job once. */\nfunction assertRetries(retries: number | undefined): void {\n if (retries !== undefined && (!Number.isInteger(retries) || retries < 0)) {\n throw new Error(`testing.retries must be a non-negative integer, got ${String(retries)}`);\n }\n}\n\n/** A lease must be a positive length: `0` would expire every job at once. */\nfunction assertLeaseSeconds(seconds: number | undefined): void {\n if (seconds !== undefined && (!Number.isFinite(seconds) || seconds <= 0)) {\n throw new Error(\n `testing.leaseSeconds must be a positive number of seconds, got ${String(seconds)}`,\n );\n }\n}\n\n/** `0` would deadlock the consumer. */\nfunction assertConcurrency(concurrency: number | undefined): void {\n if (concurrency !== undefined && (!Number.isInteger(concurrency) || concurrency < 1)) {\n throw new Error(`testing.concurrency must be a positive integer, got ${String(concurrency)}`);\n }\n}\n\nfunction assertShardSize(testing: ShelfOptions[\"testing\"]): void {\n const size = testing?.shardSize;\n if (size === undefined) {\n return;\n }\n if (!Number.isInteger(size) || size < 1) {\n throw new Error(`testing.shardSize must be a positive integer, got ${String(size)}`);\n }\n if (testing?.runner === undefined) {\n throw new Error(\"testing.shardSize requires testing.runner (workers set their own shardSize)\");\n }\n}\n\nfunction assertThreads(threads: number | undefined): void {\n if (threads !== undefined && (!Number.isInteger(threads) || threads < 0)) {\n throw new Error(\n `testing.compare.threads must be a non-negative integer, got ${String(threads)}`,\n );\n }\n}\n\nfunction assertNotificationsConfig(config: unknown): void {\n if (config === undefined) {\n return;\n }\n const result = notificationsConfigSchema.safeParse(config);\n if (!result.success) {\n const issues = result.error.issues.map((i) => `${i.path.join(\".\")}: ${i.message}`).join(\"; \");\n throw new Error(`Invalid notifications.config: ${issues}`);\n }\n}\n"],"mappings":";;;;;;;;AAMA,MAAa,sBAA8B,KAAK,OAAO,GAAG,YAAY;;;;;;;;;AAUtE,SAAgB,oBAAoB,SAAqC;CACvE,cAAc,QAAQ,SAAS,SAAS,OAAO;CAC/C,qBAAqB,QAAQ,OAAO;CACpC,sBAAsB,OAAO;CAC7B,0BAA0B,QAAQ,eAAe,MAAM;CACvD,IAAI,QAAQ,QAAQ,eAAe,KAAA,GACjC,OAAO;CAET,OAAO;EAAE,GAAG;EAAS,QAAQ;GAAE,GAAG,QAAQ;GAAQ,YAAY;EAAoB;CAAE;AACtF;;;;AAKA,SAAS,sBAAsB,SAA6B;CAC1D,IAAI,QAAQ,YAAY,KAAA,GACtB;CAEF,IAAI,QAAQ,SAAS,SAAS,oBAAoB,OAChD,MAAM,IAAI,MACR,OAAO,QAAQ,SAAS,SAAS,KAAK,4QAIxC;AAEJ;AAEA,SAAS,qBAAqB,SAAwC;CACpE,IAAI,YAAY,KAAA,KAAa,QAAQ,WAAW,KAAA,GAC9C,MAAM,IAAI,MAAM,iCAAiC;CAEnD,IAAI,SAAS,gBAAgB,KAAA,KAAa,QAAQ,WAAW,KAAA,GAC3D,MAAM,IAAI,MAAM,6EAA6E;CAE/F,IAAI,SAAS,SAAS,UAAU,QAAQ,gBAAgB,KAAA,GACtD,MAAM,IAAI,MAAM,oDAAkD;CAEpE,gBAAgB,OAAO;CACvB,qBAAqB,OAAO;AAC9B;;AAGA,SAAS,qBAAqB,SAAwC;CACpE,kBAAkB,SAAS,WAAW;CACtC,cAAc,SAAS,OAAO;CAC9B,mBAAmB,SAAS,YAAY;CACxC,4BAA4B,SAAS,qBAAqB;AAC5D;;AAGA,SAAS,4BAA4B,OAAiC;CACpE,IAAI,UAAU,KAAA,MAAc,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,IAC9D,MAAM,IAAI,MACR,6FAA6F,OAAO,KAAK,GAC3G;AAEJ;;AAGA,SAAS,cAAc,SAAmC;CACxD,IAAI,YAAY,KAAA,MAAc,CAAC,OAAO,UAAU,OAAO,KAAK,UAAU,IACpE,MAAM,IAAI,MAAM,uDAAuD,OAAO,OAAO,GAAG;AAE5F;;AAGA,SAAS,mBAAmB,SAAmC;CAC7D,IAAI,YAAY,KAAA,MAAc,CAAC,OAAO,SAAS,OAAO,KAAK,WAAW,IACpE,MAAM,IAAI,MACR,kEAAkE,OAAO,OAAO,GAClF;AAEJ;;AAGA,SAAS,kBAAkB,aAAuC;CAChE,IAAI,gBAAgB,KAAA,MAAc,CAAC,OAAO,UAAU,WAAW,KAAK,cAAc,IAChF,MAAM,IAAI,MAAM,uDAAuD,OAAO,WAAW,GAAG;AAEhG;AAEA,SAAS,gBAAgB,SAAwC;CAC/D,MAAM,OAAO,SAAS;CACtB,IAAI,SAAS,KAAA,GACX;CAEF,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,GACpC,MAAM,IAAI,MAAM,qDAAqD,OAAO,IAAI,GAAG;CAErF,IAAI,SAAS,WAAW,KAAA,GACtB,MAAM,IAAI,MAAM,6EAA6E;AAEjG;AAEA,SAAS,cAAc,SAAmC;CACxD,IAAI,YAAY,KAAA,MAAc,CAAC,OAAO,UAAU,OAAO,KAAK,UAAU,IACpE,MAAM,IAAI,MACR,+DAA+D,OAAO,OAAO,GAC/E;AAEJ;AAEA,SAAS,0BAA0B,QAAuB;CACxD,IAAI,WAAW,KAAA,GACb;CAEF,MAAM,SAAS,0BAA0B,UAAU,MAAM;CACzD,IAAI,CAAC,OAAO,SAAS;EACnB,MAAM,SAAS,OAAO,MAAM,OAAO,KAAK,MAAM,GAAG,EAAE,KAAK,KAAK,GAAG,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,KAAK,IAAI;EAC5F,MAAM,IAAI,MAAM,iCAAiC,QAAQ;CAC3D;AACF"}
package/dist/paths.d.mts CHANGED
@@ -1,6 +1,5 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/utils/paths.d.ts
4
3
  /**
5
4
  * Path segment for a mode on a given engine. Chromium keeps the bare mode key
6
5
  * so pre-multi-browser objects stay addressable; other engines get `~browser`.
@@ -22,10 +21,11 @@ declare function designRenderPath(projectId: string, provider: string, fileKey:
22
21
  declare function baselinePath(projectId: string, branch: string, storyId: string, viewport: string, browser?: string): string;
23
22
  /** Storage prefix for an extracted published Storybook. */
24
23
  declare function storybookDir(projectId: string, buildId: string): string;
24
+ /** Disposable gzip representation of a content-addressed published file. */
25
+ declare function publishedGzipPath(hash: string): string;
25
26
  /** Storage path for an uploaded Storybook zip. */
26
27
  declare function storybookZipPath(projectId: string, buildId: string): string;
27
28
  /** Storage path of a build's persisted catalog (ADR 0006, decision 9), next to its `storybook/` statics. */
28
29
  declare function catalogPath(projectId: string, buildId: string): string;
29
- //#endregion
30
- export { baselinePath, catalogPath, contentDir, contentPath, designRenderPath, diffPath, isContentPath, modeSegment, screenshotPath, storybookDir, storybookZipPath };
30
+ export { baselinePath, catalogPath, contentDir, contentPath, designRenderPath, diffPath, isContentPath, modeSegment, publishedGzipPath, screenshotPath, storybookDir, storybookZipPath };
31
31
  //# sourceMappingURL=paths.d.mts.map
package/dist/paths.mjs CHANGED
@@ -2,7 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { isSafeSegment } from "./utils/path-security.mjs";
5
- //#region src/utils/paths.ts
5
+ // src/utils/paths.ts
6
6
  /** Storage path builders. Every segment is validated — a third-party
7
7
  * capture runner echoing `story.id = "../../x"` must fail here, not in an
8
8
  * adapter with its own key-jailing quirks. */
@@ -82,6 +82,11 @@ function storybookDir(projectId, buildId) {
82
82
  assertSegments([projectId, buildId]);
83
83
  return `${projectId}/builds/${buildId}/storybook`;
84
84
  }
85
+ /** Disposable gzip representation of a content-addressed published file. */
86
+ function publishedGzipPath(hash) {
87
+ if (!/^[a-f0-9]{64}$/u.test(hash)) throw new Error("Invalid published content hash");
88
+ return `derived/published-gzip/${hash}.gz`;
89
+ }
85
90
  /** Storage path for an uploaded Storybook zip. */
86
91
  function storybookZipPath(projectId, buildId) {
87
92
  assertSegments([projectId, buildId]);
@@ -92,7 +97,6 @@ function catalogPath(projectId, buildId) {
92
97
  assertSegments([projectId, buildId]);
93
98
  return `${projectId}/builds/${buildId}/catalog.json`;
94
99
  }
95
- //#endregion
96
- export { baselinePath, catalogPath, contentDir, contentPath, designRenderPath, diffPath, isContentPath, modeSegment, screenshotPath, storybookDir, storybookZipPath };
100
+ export { baselinePath, catalogPath, contentDir, contentPath, designRenderPath, diffPath, isContentPath, modeSegment, publishedGzipPath, screenshotPath, storybookDir, storybookZipPath };
97
101
 
98
102
  //# sourceMappingURL=paths.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.mjs","names":[],"sources":["../src/utils/paths.ts"],"sourcesContent":["/** Storage path builders. Every segment is validated — a third-party\n * capture runner echoing `story.id = \"../../x\"` must fail here, not in an\n * adapter with its own key-jailing quirks. */\nimport { isSafeSegment } from \"./path-security.ts\";\n\n/** Throw when any key segment could escape its storage prefix. */\nfunction assertSegments(segments: string[]): void {\n const bad = segments.filter((segment) => !isSafeSegment(segment) || segment.length === 0);\n if (bad.length > 0) {\n throw new Error(`Unsafe storage path segment: ${bad.join(\",\")}`);\n }\n}\n/**\n * Path segment for a mode on a given engine. Chromium keeps the bare mode key\n * so pre-multi-browser objects stay addressable; other engines get `~browser`.\n */\nexport function modeSegment(viewport: string, browser?: string): string {\n return browser && browser !== \"chromium\" ? `${viewport}~${browser}` : viewport;\n}\n\n/** Storage path for a captured snapshot screenshot. */\nexport function screenshotPath(\n projectId: string,\n buildId: string,\n storyId: string,\n viewport: string,\n browser?: string,\n): string {\n const segment = modeSegment(viewport, browser);\n assertSegments([projectId, buildId, storyId, segment]);\n return `${projectId}/builds/${buildId}/screenshots/${storyId}/${segment}.png`;\n}\n\n/** Storage prefix of a project's content-addressed screenshots. */\nexport function contentDir(projectId: string): string {\n assertSegments([projectId]);\n return `${projectId}/content`;\n}\n\n/** Storage path of a screenshot stored by the SHA-256 of its bytes (shared by builds and baselines). */\nexport function contentPath(projectId: string, hash: string): string {\n assertSegments([projectId, hash]);\n return `${contentDir(projectId)}/${hash}.png`;\n}\n\n/** Whether `path` is one of `projectId`'s content-addressed screenshots. */\nexport function isContentPath(projectId: string, path: string): boolean {\n return path.startsWith(`${contentDir(projectId)}/`) && path.endsWith(\".png\");\n}\n\n/** Storage path for a snapshot diff overlay image. */\nexport function diffPath(\n projectId: string,\n buildId: string,\n storyId: string,\n viewport: string,\n browser?: string,\n): string {\n const segment = modeSegment(viewport, browser);\n assertSegments([projectId, buildId, storyId, segment]);\n return `${projectId}/builds/${buildId}/diffs/${storyId}/${segment}.png`;\n}\n\n/** Storage path for a cached design node render (overwritten in place when the file version moves). */\nexport function designRenderPath(\n projectId: string,\n provider: string,\n fileKey: string,\n nodeId: string,\n viewerId = \"\",\n): string {\n const node = nodeId.replaceAll(\":\", \"-\");\n assertSegments([projectId, provider, fileKey, node, ...(viewerId ? [viewerId] : [])]);\n const owner = viewerId ? `user-${viewerId}/` : \"\";\n return `${projectId}/design/${provider}/${owner}${fileKey}/${node}.png`;\n}\n\n/** Storage path for a branch baseline screenshot. */\nexport function baselinePath(\n projectId: string,\n branch: string,\n storyId: string,\n viewport: string,\n browser?: string,\n): string {\n const segment = modeSegment(viewport, browser);\n assertSegments([projectId, branch, storyId, segment]);\n return `${projectId}/baselines/${branch}/${storyId}/${segment}.png`;\n}\n\n/** Storage prefix for an extracted published Storybook. */\nexport function storybookDir(projectId: string, buildId: string): string {\n assertSegments([projectId, buildId]);\n return `${projectId}/builds/${buildId}/storybook`;\n}\n\n/** Storage path for an uploaded Storybook zip. */\nexport function storybookZipPath(projectId: string, buildId: string): string {\n assertSegments([projectId, buildId]);\n return `${projectId}/builds/${buildId}/storybook.zip`;\n}\n\n/** Storage path of a build's persisted catalog (ADR 0006, decision 9), next to its `storybook/` statics. */\nexport function catalogPath(projectId: string, buildId: string): string {\n assertSegments([projectId, buildId]);\n return `${projectId}/builds/${buildId}/catalog.json`;\n}\n"],"mappings":";;;;;;;;;AAMA,SAAS,eAAe,UAA0B;CAChD,MAAM,MAAM,SAAS,QAAQ,YAAY,CAAC,cAAc,OAAO,KAAK,QAAQ,WAAW,CAAC;CACxF,IAAI,IAAI,SAAS,GACf,MAAM,IAAI,MAAM,gCAAgC,IAAI,KAAK,GAAG,GAAG;AAEnE;;;;;AAKA,SAAgB,YAAY,UAAkB,SAA0B;CACtE,OAAO,WAAW,YAAY,aAAa,GAAG,SAAS,GAAG,YAAY;AACxE;;AAGA,SAAgB,eACd,WACA,SACA,SACA,UACA,SACQ;CACR,MAAM,UAAU,YAAY,UAAU,OAAO;CAC7C,eAAe;EAAC;EAAW;EAAS;EAAS;CAAO,CAAC;CACrD,OAAO,GAAG,UAAU,UAAU,QAAQ,eAAe,QAAQ,GAAG,QAAQ;AAC1E;;AAGA,SAAgB,WAAW,WAA2B;CACpD,eAAe,CAAC,SAAS,CAAC;CAC1B,OAAO,GAAG,UAAU;AACtB;;AAGA,SAAgB,YAAY,WAAmB,MAAsB;CACnE,eAAe,CAAC,WAAW,IAAI,CAAC;CAChC,OAAO,GAAG,WAAW,SAAS,EAAE,GAAG,KAAK;AAC1C;;AAGA,SAAgB,cAAc,WAAmB,MAAuB;CACtE,OAAO,KAAK,WAAW,GAAG,WAAW,SAAS,EAAE,EAAE,KAAK,KAAK,SAAS,MAAM;AAC7E;;AAGA,SAAgB,SACd,WACA,SACA,SACA,UACA,SACQ;CACR,MAAM,UAAU,YAAY,UAAU,OAAO;CAC7C,eAAe;EAAC;EAAW;EAAS;EAAS;CAAO,CAAC;CACrD,OAAO,GAAG,UAAU,UAAU,QAAQ,SAAS,QAAQ,GAAG,QAAQ;AACpE;;AAGA,SAAgB,iBACd,WACA,UACA,SACA,QACA,WAAW,IACH;CACR,MAAM,OAAO,OAAO,WAAW,KAAK,GAAG;CACvC,eAAe;EAAC;EAAW;EAAU;EAAS;EAAM,GAAI,WAAW,CAAC,QAAQ,IAAI,CAAC;CAAE,CAAC;CAEpF,OAAO,GAAG,UAAU,UAAU,SAAS,GADzB,WAAW,QAAQ,SAAS,KAAK,KACG,QAAQ,GAAG,KAAK;AACpE;;AAGA,SAAgB,aACd,WACA,QACA,SACA,UACA,SACQ;CACR,MAAM,UAAU,YAAY,UAAU,OAAO;CAC7C,eAAe;EAAC;EAAW;EAAQ;EAAS;CAAO,CAAC;CACpD,OAAO,GAAG,UAAU,aAAa,OAAO,GAAG,QAAQ,GAAG,QAAQ;AAChE;;AAGA,SAAgB,aAAa,WAAmB,SAAyB;CACvE,eAAe,CAAC,WAAW,OAAO,CAAC;CACnC,OAAO,GAAG,UAAU,UAAU,QAAQ;AACxC;;AAGA,SAAgB,iBAAiB,WAAmB,SAAyB;CAC3E,eAAe,CAAC,WAAW,OAAO,CAAC;CACnC,OAAO,GAAG,UAAU,UAAU,QAAQ;AACxC;;AAGA,SAAgB,YAAY,WAAmB,SAAyB;CACtE,eAAe,CAAC,WAAW,OAAO,CAAC;CACnC,OAAO,GAAG,UAAU,UAAU,QAAQ;AACxC"}
1
+ {"version":3,"file":"paths.mjs","names":[],"sources":["../src/utils/paths.ts"],"sourcesContent":["/** Storage path builders. Every segment is validated — a third-party\n * capture runner echoing `story.id = \"../../x\"` must fail here, not in an\n * adapter with its own key-jailing quirks. */\nimport { isSafeSegment } from \"./path-security.ts\";\n\n/** Throw when any key segment could escape its storage prefix. */\nfunction assertSegments(segments: string[]): void {\n const bad = segments.filter((segment) => !isSafeSegment(segment) || segment.length === 0);\n if (bad.length > 0) {\n throw new Error(`Unsafe storage path segment: ${bad.join(\",\")}`);\n }\n}\n/**\n * Path segment for a mode on a given engine. Chromium keeps the bare mode key\n * so pre-multi-browser objects stay addressable; other engines get `~browser`.\n */\nexport function modeSegment(viewport: string, browser?: string): string {\n return browser && browser !== \"chromium\" ? `${viewport}~${browser}` : viewport;\n}\n\n/** Storage path for a captured snapshot screenshot. */\nexport function screenshotPath(\n projectId: string,\n buildId: string,\n storyId: string,\n viewport: string,\n browser?: string,\n): string {\n const segment = modeSegment(viewport, browser);\n assertSegments([projectId, buildId, storyId, segment]);\n return `${projectId}/builds/${buildId}/screenshots/${storyId}/${segment}.png`;\n}\n\n/** Storage prefix of a project's content-addressed screenshots. */\nexport function contentDir(projectId: string): string {\n assertSegments([projectId]);\n return `${projectId}/content`;\n}\n\n/** Storage path of a screenshot stored by the SHA-256 of its bytes (shared by builds and baselines). */\nexport function contentPath(projectId: string, hash: string): string {\n assertSegments([projectId, hash]);\n return `${contentDir(projectId)}/${hash}.png`;\n}\n\n/** Whether `path` is one of `projectId`'s content-addressed screenshots. */\nexport function isContentPath(projectId: string, path: string): boolean {\n return path.startsWith(`${contentDir(projectId)}/`) && path.endsWith(\".png\");\n}\n\n/** Storage path for a snapshot diff overlay image. */\nexport function diffPath(\n projectId: string,\n buildId: string,\n storyId: string,\n viewport: string,\n browser?: string,\n): string {\n const segment = modeSegment(viewport, browser);\n assertSegments([projectId, buildId, storyId, segment]);\n return `${projectId}/builds/${buildId}/diffs/${storyId}/${segment}.png`;\n}\n\n/** Storage path for a cached design node render (overwritten in place when the file version moves). */\nexport function designRenderPath(\n projectId: string,\n provider: string,\n fileKey: string,\n nodeId: string,\n viewerId = \"\",\n): string {\n const node = nodeId.replaceAll(\":\", \"-\");\n assertSegments([projectId, provider, fileKey, node, ...(viewerId ? [viewerId] : [])]);\n const owner = viewerId ? `user-${viewerId}/` : \"\";\n return `${projectId}/design/${provider}/${owner}${fileKey}/${node}.png`;\n}\n\n/** Storage path for a branch baseline screenshot. */\nexport function baselinePath(\n projectId: string,\n branch: string,\n storyId: string,\n viewport: string,\n browser?: string,\n): string {\n const segment = modeSegment(viewport, browser);\n assertSegments([projectId, branch, storyId, segment]);\n return `${projectId}/baselines/${branch}/${storyId}/${segment}.png`;\n}\n\n/** Storage prefix for an extracted published Storybook. */\nexport function storybookDir(projectId: string, buildId: string): string {\n assertSegments([projectId, buildId]);\n return `${projectId}/builds/${buildId}/storybook`;\n}\n\n/** Disposable gzip representation of a content-addressed published file. */\nexport function publishedGzipPath(hash: string): string {\n if (!/^[a-f0-9]{64}$/u.test(hash)) throw new Error(\"Invalid published content hash\");\n return `derived/published-gzip/${hash}.gz`;\n}\n\n/** Storage path for an uploaded Storybook zip. */\nexport function storybookZipPath(projectId: string, buildId: string): string {\n assertSegments([projectId, buildId]);\n return `${projectId}/builds/${buildId}/storybook.zip`;\n}\n\n/** Storage path of a build's persisted catalog (ADR 0006, decision 9), next to its `storybook/` statics. */\nexport function catalogPath(projectId: string, buildId: string): string {\n assertSegments([projectId, buildId]);\n return `${projectId}/builds/${buildId}/catalog.json`;\n}\n"],"mappings":";;;;;;;;;AAMA,SAAS,eAAe,UAA0B;CAChD,MAAM,MAAM,SAAS,QAAQ,YAAY,CAAC,cAAc,OAAO,KAAK,QAAQ,WAAW,CAAC;CACxF,IAAI,IAAI,SAAS,GACf,MAAM,IAAI,MAAM,gCAAgC,IAAI,KAAK,GAAG,GAAG;AAEnE;;;;;AAKA,SAAgB,YAAY,UAAkB,SAA0B;CACtE,OAAO,WAAW,YAAY,aAAa,GAAG,SAAS,GAAG,YAAY;AACxE;;AAGA,SAAgB,eACd,WACA,SACA,SACA,UACA,SACQ;CACR,MAAM,UAAU,YAAY,UAAU,OAAO;CAC7C,eAAe;EAAC;EAAW;EAAS;EAAS;CAAO,CAAC;CACrD,OAAO,GAAG,UAAU,UAAU,QAAQ,eAAe,QAAQ,GAAG,QAAQ;AAC1E;;AAGA,SAAgB,WAAW,WAA2B;CACpD,eAAe,CAAC,SAAS,CAAC;CAC1B,OAAO,GAAG,UAAU;AACtB;;AAGA,SAAgB,YAAY,WAAmB,MAAsB;CACnE,eAAe,CAAC,WAAW,IAAI,CAAC;CAChC,OAAO,GAAG,WAAW,SAAS,EAAE,GAAG,KAAK;AAC1C;;AAGA,SAAgB,cAAc,WAAmB,MAAuB;CACtE,OAAO,KAAK,WAAW,GAAG,WAAW,SAAS,EAAE,EAAE,KAAK,KAAK,SAAS,MAAM;AAC7E;;AAGA,SAAgB,SACd,WACA,SACA,SACA,UACA,SACQ;CACR,MAAM,UAAU,YAAY,UAAU,OAAO;CAC7C,eAAe;EAAC;EAAW;EAAS;EAAS;CAAO,CAAC;CACrD,OAAO,GAAG,UAAU,UAAU,QAAQ,SAAS,QAAQ,GAAG,QAAQ;AACpE;;AAGA,SAAgB,iBACd,WACA,UACA,SACA,QACA,WAAW,IACH;CACR,MAAM,OAAO,OAAO,WAAW,KAAK,GAAG;CACvC,eAAe;EAAC;EAAW;EAAU;EAAS;EAAM,GAAI,WAAW,CAAC,QAAQ,IAAI,CAAC;CAAE,CAAC;CAEpF,OAAO,GAAG,UAAU,UAAU,SAAS,GADzB,WAAW,QAAQ,SAAS,KAAK,KACG,QAAQ,GAAG,KAAK;AACpE;;AAGA,SAAgB,aACd,WACA,QACA,SACA,UACA,SACQ;CACR,MAAM,UAAU,YAAY,UAAU,OAAO;CAC7C,eAAe;EAAC;EAAW;EAAQ;EAAS;CAAO,CAAC;CACpD,OAAO,GAAG,UAAU,aAAa,OAAO,GAAG,QAAQ,GAAG,QAAQ;AAChE;;AAGA,SAAgB,aAAa,WAAmB,SAAyB;CACvE,eAAe,CAAC,WAAW,OAAO,CAAC;CACnC,OAAO,GAAG,UAAU,UAAU,QAAQ;AACxC;;AAGA,SAAgB,kBAAkB,MAAsB;CACtD,IAAI,CAAC,kBAAkB,KAAK,IAAI,GAAG,MAAM,IAAI,MAAM,gCAAgC;CACnF,OAAO,0BAA0B,KAAK;AACxC;;AAGA,SAAgB,iBAAiB,WAAmB,SAAyB;CAC3E,eAAe,CAAC,WAAW,OAAO,CAAC;CACnC,OAAO,GAAG,UAAU,UAAU,QAAQ;AACxC;;AAGA,SAAgB,YAAY,WAAmB,SAAyB;CACtE,eAAe,CAAC,WAAW,OAAO,CAAC;CACnC,OAAO,GAAG,UAAU,UAAU,QAAQ;AACxC"}
@@ -1,8 +1,7 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- import { DatabaseAdapter } from "../adapter/database.mjs";
4
3
  import { Logger } from "../logger.mjs";
5
- //#region src/retention/ai.d.ts
4
+ import { DatabaseAdapter } from "../adapter/database.mjs";
6
5
  /** Pending/running rows older than this are marked `interrupted`. */
7
6
  declare const STALE_RUN_MS: number;
8
7
  /** What one AI retention pass removed. */
@@ -13,6 +12,5 @@ interface AiPurgeResult {
13
12
  }
14
13
  /** Run one AI retention pass (safe to call from any instance, any time). */
15
14
  declare function purgeAiData(db: DatabaseAdapter, now?: Date, logger?: Logger): Promise<AiPurgeResult>;
16
- //#endregion
17
15
  export { AiPurgeResult, STALE_RUN_MS, purgeAiData };
18
16
  //# sourceMappingURL=ai.d.mts.map
@@ -17,7 +17,6 @@ async function purgeAiData(db, now = /* @__PURE__ */ new Date(), logger) {
17
17
  if (result.health > 0 || result.usage > 0 || result.interrupted > 0) logger?.info(result, "ai data retention pass complete");
18
18
  return result;
19
19
  }
20
- //#endregion
21
20
  export { STALE_RUN_MS, purgeAiData };
22
21
 
23
22
  //# sourceMappingURL=ai.mjs.map
@@ -4,7 +4,7 @@ import.meta.url;
4
4
  import { BuildModel } from "../models/build.mjs";
5
5
  import { BuildReviewModel } from "../models/build-review.mjs";
6
6
  import { CommentModel } from "../models/comment.mjs";
7
- //#region src/retention/pending.ts
7
+ // src/retention/pending.ts
8
8
  /**
9
9
  * The project's pending builds whose last activity (upload, comment or verdict) is before
10
10
  * `cutoff`. `capture_attempts` need no check: a build capture worked on leaves `pending`.
@@ -41,7 +41,6 @@ function stampOf(item) {
41
41
  function hasNewerOnBranch(all, build) {
42
42
  return all.some((other) => other.id !== build.id && other.gitBranch === build.gitBranch && other.createdAt > build.createdAt);
43
43
  }
44
- //#endregion
45
44
  export { pendingCandidates };
46
45
 
47
46
  //# sourceMappingURL=pending.mjs.map
@@ -0,0 +1,28 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ // src/retention/published-gzip-sweep.ts
5
+ const PREFIX = "derived/published-gzip/";
6
+ const MAX_PER_SWEEP = 128;
7
+ const cursors = /* @__PURE__ */ new WeakMap();
8
+ /** Reclaim disposable orphan variants; never delete content or other storage keys. */
9
+ async function sweepPublishedGzip(storage) {
10
+ const keys = (await storage.list(PREFIX)).filter((key) => /^derived\/published-gzip\/[a-f0-9]{64}\.gz$/u.test(key)).toSorted();
11
+ if (keys.length === 0) return;
12
+ const cursor = cursors.get(storage);
13
+ const next = cursor ? keys.findIndex((key) => key > cursor) : 0;
14
+ const start = Math.max(0, next);
15
+ const batch = [...keys.slice(start), ...keys.slice(0, start)].slice(0, MAX_PER_SWEEP);
16
+ for (const key of batch) await removeOrphan(storage, key);
17
+ const last = batch.at(-1);
18
+ if (last) cursors.set(storage, last);
19
+ }
20
+ async function removeOrphan(storage, key) {
21
+ const hash = key.slice(23, -3);
22
+ try {
23
+ if (!await storage.exists(`content/${hash}`)) await storage.delete(key);
24
+ } catch {}
25
+ }
26
+ export { sweepPublishedGzip };
27
+
28
+ //# sourceMappingURL=published-gzip-sweep.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"published-gzip-sweep.mjs","names":[],"sources":["../../src/retention/published-gzip-sweep.ts"],"sourcesContent":["import type { StorageAdapter } from \"../adapters/storage.ts\";\n\nconst PREFIX = \"derived/published-gzip/\";\nconst MAX_PER_SWEEP = 128;\nconst cursors = new WeakMap<StorageAdapter, string>();\n\n/** Reclaim disposable orphan variants; never delete content or other storage keys. */\nexport async function sweepPublishedGzip(storage: StorageAdapter): Promise<void> {\n const keys = (await storage.list(PREFIX))\n .filter((key) => /^derived\\/published-gzip\\/[a-f0-9]{64}\\.gz$/u.test(key))\n .toSorted();\n if (keys.length === 0) return;\n const cursor = cursors.get(storage);\n const next = cursor ? keys.findIndex((key) => key > cursor) : 0;\n const start = Math.max(0, next);\n const batch = [...keys.slice(start), ...keys.slice(0, start)].slice(0, MAX_PER_SWEEP);\n for (const key of batch) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- bounded, sequential storage work\n await removeOrphan(storage, key);\n }\n const last = batch.at(-1);\n if (last) cursors.set(storage, last);\n}\n\nasync function removeOrphan(storage: StorageAdapter, key: string): Promise<void> {\n const hash = key.slice(PREFIX.length, -3);\n try {\n if (!(await storage.exists(`content/${hash}`))) await storage.delete(key);\n } catch {\n // Retention is best effort; a missing variant can always be regenerated.\n }\n}\n"],"mappings":";;;;AAEA,MAAM,SAAS;AACf,MAAM,gBAAgB;AACtB,MAAM,0BAAU,IAAI,QAAgC;;AAGpD,eAAsB,mBAAmB,SAAwC;CAC/E,MAAM,QAAQ,MAAM,QAAQ,KAAK,MAAM,EAAA,CACpC,QAAQ,QAAQ,+CAA+C,KAAK,GAAG,CAAC,CAAC,CACzE,SAAS;CACZ,IAAI,KAAK,WAAW,GAAG;CACvB,MAAM,SAAS,QAAQ,IAAI,OAAO;CAClC,MAAM,OAAO,SAAS,KAAK,WAAW,QAAQ,MAAM,MAAM,IAAI;CAC9D,MAAM,QAAQ,KAAK,IAAI,GAAG,IAAI;CAC9B,MAAM,QAAQ,CAAC,GAAG,KAAK,MAAM,KAAK,GAAG,GAAG,KAAK,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,aAAa;CACpF,KAAK,MAAM,OAAO,OAEhB,MAAM,aAAa,SAAS,GAAG;CAEjC,MAAM,OAAO,MAAM,GAAG,EAAE;CACxB,IAAI,MAAM,QAAQ,IAAI,SAAS,IAAI;AACrC;AAEA,eAAe,aAAa,SAAyB,KAA4B;CAC/E,MAAM,OAAO,IAAI,MAAM,IAAe,EAAE;CACxC,IAAI;EACF,IAAI,CAAE,MAAM,QAAQ,OAAO,WAAW,MAAM,GAAI,MAAM,QAAQ,OAAO,GAAG;CAC1E,QAAQ,CAER;AACF"}
@@ -4,7 +4,6 @@ import { DatabaseAdapter } from "../adapter/database.mjs";
4
4
  import { StorageAdapter } from "../adapter/storage.mjs";
5
5
  import { Logger } from "pino";
6
6
  import { Table } from "drizzle-orm";
7
- //#region src/retention/screenshot-sweep.d.ts
8
7
  /** Longest an unreferenced screenshot is kept after the sweep first sees it (default 24 h). */
9
8
  declare const SWEEP_GRACE_MS: number;
10
9
  /** Tables the sweep reads references and in-flight builds from. */
@@ -46,6 +45,5 @@ declare function sweepScreenshotContent(deps: SweepDeps, projectId: string, opti
46
45
  now?: number;
47
46
  graceMs?: number;
48
47
  }): Promise<SweepResult>;
49
- //#endregion
50
48
  export { SWEEP_GRACE_MS, SweepDeps, SweepResult, SweepTables, sweepScreenshotContent };
51
49
  //# sourceMappingURL=screenshot-sweep.d.mts.map
@@ -5,7 +5,7 @@ import { col } from "../models/columns.mjs";
5
5
  import { contentDir } from "../paths.mjs";
6
6
  import { CLAIMS_DIR } from "../utils/screenshot-store.mjs";
7
7
  import { and, eq, gte, inArray, like } from "drizzle-orm";
8
- //#region src/retention/screenshot-sweep.ts
8
+ // src/retention/screenshot-sweep.ts
9
9
  /** Mark-and-sweep for a project's content-addressed screenshots. */
10
10
  /** Longest an unreferenced screenshot is kept after the sweep first sees it (default 24 h). */
11
11
  const SWEEP_GRACE_MS = 864e5;
@@ -122,7 +122,6 @@ async function readMarker(storage, dir) {
122
122
  async function writeMarker(storage, dir, next, previous) {
123
123
  if (JSON.stringify(next) !== JSON.stringify(previous)) await storage.write(`${dir}${MARKER}`, Buffer.from(JSON.stringify(next)));
124
124
  }
125
- //#endregion
126
125
  export { SWEEP_GRACE_MS, sweepScreenshotContent };
127
126
 
128
127
  //# sourceMappingURL=screenshot-sweep.mjs.map
@@ -7,7 +7,6 @@ import { SWEEP_GRACE_MS, SweepDeps, SweepResult, SweepTables, sweepScreenshotCon
7
7
  import { AiPurgeResult, STALE_RUN_MS, purgeAiData } from "./retention/ai.mjs";
8
8
  import { Logger } from "pino";
9
9
  import { Table } from "drizzle-orm";
10
- //#region src/retention/purge.d.ts
11
10
  /** Table handles for retention queries. */
12
11
  interface RetentionTables {
13
12
  builds: Table;
@@ -54,6 +53,5 @@ interface BranchGcResult {
54
53
  removedBranches: number;
55
54
  removedBaselines: number;
56
55
  }
57
- //#endregion
58
56
  export { type AiPurgeResult, BranchGcResult, PurgeOptions, PurgeResult, Retention, RetentionTables, STALE_RUN_MS, SWEEP_GRACE_MS, type SweepDeps, type SweepResult, type SweepTables, purgeAiData, sweepScreenshotContent };
59
57
  //# sourceMappingURL=retention.d.mts.map
@@ -4,6 +4,7 @@ import.meta.url;
4
4
  import { TERMINAL_BUILD_STATUSES } from "./types.mjs";
5
5
  import { col } from "./models/columns.mjs";
6
6
  import { BuildModel } from "./models/build.mjs";
7
+ import { publishedGzipPath } from "./paths.mjs";
7
8
  import { BaselineModel } from "./models/baseline.mjs";
8
9
  import { CaptureJobModel } from "./models/capture-job.mjs";
9
10
  import { BuildComponentModel } from "./models/build-component.mjs";
@@ -11,10 +12,11 @@ import { InsightModel } from "./models/insight.mjs";
11
12
  import { LabelModel } from "./models/label.mjs";
12
13
  import { PackageUsageModel } from "./models/usage.mjs";
13
14
  import { pendingCandidates } from "./retention/pending.mjs";
15
+ import { sweepPublishedGzip } from "./retention/published-gzip-sweep.mjs";
14
16
  import { SWEEP_GRACE_MS, sweepScreenshotContent } from "./retention/screenshot-sweep.mjs";
15
17
  import { STALE_RUN_MS, purgeAiData } from "./retention/ai.mjs";
16
18
  import { and, desc, eq, inArray, lt } from "drizzle-orm";
17
- //#region src/retention/purge.ts
19
+ // src/retention/purge.ts
18
20
  /** Removes expired transient builds while keeping baselines and persistent builds. */
19
21
  var Retention = class {
20
22
  db;
@@ -153,9 +155,11 @@ var Retention = class {
153
155
  const all = await this.db.list(contentRefs);
154
156
  for (const row of all) if (row.refCount === 0 && row.lastSeenAt < cutoff) {
155
157
  await this.storage.delete(`content/${row.hash}`).catch(() => {});
158
+ if (/^[a-f0-9]{64}$/u.test(row.hash)) await this.storage.delete(publishedGzipPath(row.hash)).catch(() => {});
156
159
  await this.db.remove(contentRefs, row.hash);
157
160
  }
158
161
  } catch {}
162
+ await sweepPublishedGzip(this.storage).catch(() => {});
159
163
  return files.length;
160
164
  }
161
165
  async purgeStaleBranches(project, ttlDays) {
@@ -196,7 +200,6 @@ function branchLatestAt(rows) {
196
200
  }
197
201
  return latestAt;
198
202
  }
199
- //#endregion
200
203
  export { Retention, STALE_RUN_MS, SWEEP_GRACE_MS, purgeAiData, sweepScreenshotContent };
201
204
 
202
205
  //# sourceMappingURL=retention.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"retention.mjs","names":[],"sources":["../src/retention/purge.ts"],"sourcesContent":["import { and, desc, eq, inArray, lt } from \"drizzle-orm\";\nimport type { Table } from \"drizzle-orm\";\nimport type { Logger } from \"pino\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildComponentModel } from \"../models/build-component.ts\";\nimport { BuildModel, type BuildTables } from \"../models/build.ts\";\nimport { CaptureJobModel } from \"../models/capture-job.ts\";\nimport { InsightModel } from \"../models/insight.ts\";\nimport { LabelModel, type LabelTables } from \"../models/label.ts\";\nimport { PackageUsageModel } from \"../models/usage.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { TERMINAL_BUILD_STATUSES } from \"../types.ts\";\nimport { pendingCandidates } from \"./pending.ts\";\nimport { sweepScreenshotContent } from \"./screenshot-sweep.ts\";\n\nexport {\n sweepScreenshotContent,\n SWEEP_GRACE_MS,\n type SweepDeps,\n type SweepResult,\n type SweepTables,\n} from \"./screenshot-sweep.ts\";\nimport { col } from \"../models/columns.ts\";\nexport { purgeAiData, STALE_RUN_MS, type AiPurgeResult } from \"./ai.ts\";\n\n/** Table handles for retention queries. */\nexport interface RetentionTables {\n builds: Table;\n buildLabels: Table;\n baselines: Table;\n}\n\n/** Removes expired transient builds while keeping baselines and persistent builds. */\nexport class Retention {\n private readonly db: DatabaseAdapter;\n private readonly storage: StorageAdapter;\n private readonly tables: RetentionTables;\n private readonly logger?: Logger;\n\n constructor(\n db: DatabaseAdapter,\n storage: StorageAdapter,\n tables: RetentionTables,\n logger?: Logger,\n ) {\n this.db = db;\n this.storage = storage;\n this.tables = tables;\n this.logger = logger;\n }\n\n async purge(project: Project, options: PurgeOptions): Promise<PurgeResult> {\n const cutoff = new Date(Date.now() - options.ttlDays * 86_400_000).toISOString();\n const { buildIds, pending } = await this.selectBuilds(project, options, cutoff);\n const results = await Promise.all(\n buildIds.map(async (buildId) => ({\n files: await this.deleteBuildFiles(project.id, buildId),\n removed: await this.removeBuild(buildId),\n })),\n );\n const removedBuilds = results.filter((r: { removed: boolean }) => r.removed).length;\n const removedFiles = results.reduce((sum: number, r: { files: number }) => sum + r.files, 0);\n await this.sweepScreenshots(project);\n const removedJobs = await this.purgeFinishedJobs(cutoff);\n this.logger?.info(\n { projectId: project.id, removedBuilds, removedFiles, removedJobs, removedPending: pending },\n \"build retention purge complete\",\n );\n return { removedBuilds, removedFiles };\n }\n\n /**\n * The builds a purge removes: terminal builds past the window (minus the latest per branch when\n * asked), and pending builds idle past it (ADR 0013, decision 9). Persistent builds stay.\n */\n private async selectBuilds(\n project: Project,\n options: PurgeOptions,\n cutoff: string,\n ): Promise<{ buildIds: string[]; pending: number }> {\n const candidates = await this.db.list(this.tables.builds, {\n where: and(\n eq(col(this.tables.builds, \"projectId\"), project.id),\n inArray(col(this.tables.builds, \"status\"), [...TERMINAL_BUILD_STATUSES]),\n lt(col(this.tables.builds, \"updatedAt\"), cutoff),\n ),\n });\n const keep = options.keepLatestPerBranch\n ? await this.latestPerBranch(project.id)\n : new Set<string>();\n const persistent = await new LabelModel(\n this.db,\n this.tables as unknown as LabelTables,\n ).persistentBuildIds(project.id);\n const terminal = candidates\n .map((build) => (build as { id: string }).id)\n .filter((buildId) => !keep.has(buildId));\n // Pending builds leave on their own clock; \"the latest on a branch\" does not shield them.\n const pending = (await pendingCandidates(this.db, project.id, cutoff)).map(\n (candidate) => candidate.buildId,\n );\n return {\n buildIds: [...terminal, ...pending].filter((buildId) => !persistent.has(buildId)),\n pending: pending.length,\n };\n }\n\n /** Reclaim screenshots no snapshot or baseline references any more; never blocks the purge. */\n private async sweepScreenshots(project: Project): Promise<void> {\n try {\n await sweepScreenshotContent(\n {\n db: this.db,\n storage: this.storage,\n tables: {\n snapshots: this.db.tables.snapshots,\n baselines: this.tables.baselines,\n builds: this.tables.builds,\n },\n logger: this.logger,\n },\n project.id,\n );\n } catch (error) {\n this.logger?.warn({ err: error, projectId: project.id }, \"screenshot sweep failed\");\n }\n }\n\n /**\n * Drop finished queue jobs older than the build cutoff (they have no foreign key to a build, and\n * a finished job is only history); never blocks the purge. Queued and running jobs are kept.\n */\n private async purgeFinishedJobs(cutoff: string): Promise<number> {\n try {\n return await new CaptureJobModel(this.db).purgeFinished(cutoff);\n } catch (error) {\n this.logger?.warn({ err: error }, \"capture job purge failed\");\n return 0;\n }\n }\n\n private async removeBuild(buildId: string): Promise<boolean> {\n await new InsightModel(this.db).removeForBuild(buildId);\n await new CaptureJobModel(this.db).removeForBuild(buildId);\n await new BuildComponentModel(this.db).removeForBuild(buildId);\n await new PackageUsageModel(this.db).removeForBuild(buildId);\n await new BuildModel(this.db, this.tables as unknown as BuildTables).remove(buildId);\n return true;\n }\n\n private async latestPerBranch(projectId: string): Promise<Set<string>> {\n const rows = (await this.db.list(this.tables.builds, {\n where: eq(col(this.tables.builds, \"projectId\"), projectId),\n orderBy: desc(col(this.tables.builds, \"updatedAt\")),\n })) as unknown as { gitBranch: string; id: string }[];\n const latest = new Map<string, string>();\n for (const row of rows) {\n const branch = row.gitBranch;\n const current = latest.get(branch);\n if (!current) {\n latest.set(branch, row.id);\n }\n }\n return new Set(latest.values());\n }\n\n private async deleteBuildFiles(projectId: string, buildId: string): Promise<number> {\n const prefix = `${projectId}/builds/${buildId}/`;\n const manifestPath = `${prefix}storybook/manifest.json`;\n let manifestFiles: string[] = [];\n try {\n if (await this.storage.exists(manifestPath)) {\n const raw = await this.storage.read(manifestPath);\n const manifest = JSON.parse(raw.toString(\"utf8\")) as Record<string, string>;\n manifestFiles = Object.values(manifest);\n }\n } catch {\n // ignore — manifest may not exist for old builds\n }\n const files = await this.storage.list(prefix);\n await Promise.all(\n files.map(async (file) => {\n await this.storage.delete(file);\n }),\n );\n // Decrement content refs for this build's manifest\n for (const hash of manifestFiles) {\n try {\n const contentRefs = this.db.tables.contentRefs;\n const existing = (await this.db.get(contentRefs, hash)) as unknown as {\n refCount: number;\n } | null;\n // oxlint-disable-next-line unicorn/prefer-ternary -- refCount check is clearer as if/else\n if (existing) {\n const refCount = existing.refCount;\n // oxlint-disable-next-line unicorn/prefer-ternary -- grace logic is clearer as if/else\n if (refCount <= 1) {\n // Keep for 7-day grace: set refCount 0 and update lastSeenAt, don't delete yet\n await this.db.update(contentRefs, hash, {\n refCount: 0,\n lastSeenAt: new Date().toISOString(),\n });\n } else {\n await this.db.update(contentRefs, hash, {\n refCount: refCount - 1,\n lastSeenAt: new Date().toISOString(),\n });\n }\n }\n } catch {\n // ignore — content_refs may not exist\n }\n }\n // GC stale content_refs with 7-day grace\n try {\n const contentRefs = this.db.tables.contentRefs;\n const cutoff = new Date(Date.now() - 7 * 86_400_000).toISOString();\n const all = (await this.db.list(contentRefs)) as unknown as {\n hash: string;\n refCount: number;\n lastSeenAt: string;\n }[];\n for (const row of all) {\n if (row.refCount === 0 && row.lastSeenAt < cutoff) {\n await this.storage.delete(`content/${row.hash}`).catch(() => {\n // ignore storage delete failure\n });\n await this.db.remove(contentRefs, row.hash);\n }\n }\n } catch {\n // ignore — content_refs may not exist\n }\n return files.length;\n }\n\n async purgeStaleBranches(project: Project, ttlDays: number): Promise<BranchGcResult> {\n if (ttlDays <= 0) {\n return { removedBranches: 0, removedBaselines: 0 };\n }\n const cutoff = new Date(Date.now() - ttlDays * 86_400_000).toISOString();\n const rows = (await this.db.list(this.tables.builds, {\n where: eq(col(this.tables.builds, \"projectId\"), project.id),\n })) as unknown as { gitBranch: string; updatedAt: string }[];\n const stale = collectStaleBranches(rows, project.gitDefaultBranch, cutoff);\n if (stale.size === 0) {\n return { removedBranches: 0, removedBaselines: 0 };\n }\n const baselines = new BaselineModel(this.db, this.tables, this.storage);\n const removedBaselines = await baselines.removeStaleBranches(project.id, stale);\n await this.sweepScreenshots(project);\n this.logger?.info(\n { projectId: project.id, removedBranches: stale.size, removedBaselines },\n \"branch GC complete\",\n );\n return { removedBranches: stale.size, removedBaselines };\n }\n}\n\n/** Options controlling which builds a retention purge removes. */\nexport interface PurgeOptions {\n ttlDays: number;\n keepLatestPerBranch: boolean;\n}\n\n/** Counts of builds and files removed by a retention purge. */\nexport interface PurgeResult {\n removedBuilds: number;\n removedFiles: number;\n}\n\n/** Counts of stale branches and baselines removed. */\nexport interface BranchGcResult {\n removedBranches: number;\n removedBaselines: number;\n}\n\nfunction collectStaleBranches(\n rows: { gitBranch: string; updatedAt: string }[],\n defaultBranch: string,\n cutoff: string,\n): Set<string> {\n const latestAt = branchLatestAt(rows);\n const stale = new Set<string>();\n for (const [branch, updatedAt] of latestAt) {\n if (branch !== defaultBranch && updatedAt < cutoff) {\n stale.add(branch);\n }\n }\n return stale;\n}\n\nfunction branchLatestAt(rows: { gitBranch: string; updatedAt: string }[]): Map<string, string> {\n const latestAt = new Map<string, string>();\n for (const row of rows) {\n const current = latestAt.get(row.gitBranch);\n if (!current || row.updatedAt > current) {\n latestAt.set(row.gitBranch, row.updatedAt);\n }\n }\n return latestAt;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"retention.mjs","names":[],"sources":["../src/retention/purge.ts"],"sourcesContent":["import { and, desc, eq, inArray, lt } from \"drizzle-orm\";\nimport type { Table } from \"drizzle-orm\";\nimport type { Logger } from \"pino\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildComponentModel } from \"../models/build-component.ts\";\nimport { BuildModel, type BuildTables } from \"../models/build.ts\";\nimport { CaptureJobModel } from \"../models/capture-job.ts\";\nimport { InsightModel } from \"../models/insight.ts\";\nimport { LabelModel, type LabelTables } from \"../models/label.ts\";\nimport { PackageUsageModel } from \"../models/usage.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { TERMINAL_BUILD_STATUSES } from \"../types.ts\";\nimport { pendingCandidates } from \"./pending.ts\";\nimport { sweepPublishedGzip } from \"./published-gzip-sweep.ts\";\nimport { sweepScreenshotContent } from \"./screenshot-sweep.ts\";\n\nexport {\n sweepScreenshotContent,\n SWEEP_GRACE_MS,\n type SweepDeps,\n type SweepResult,\n type SweepTables,\n} from \"./screenshot-sweep.ts\";\nimport { col } from \"../models/columns.ts\";\nimport { publishedGzipPath } from \"../utils/paths.ts\";\nexport { purgeAiData, STALE_RUN_MS, type AiPurgeResult } from \"./ai.ts\";\n\n/** Table handles for retention queries. */\nexport interface RetentionTables {\n builds: Table;\n buildLabels: Table;\n baselines: Table;\n}\n\n/** Removes expired transient builds while keeping baselines and persistent builds. */\nexport class Retention {\n private readonly db: DatabaseAdapter;\n private readonly storage: StorageAdapter;\n private readonly tables: RetentionTables;\n private readonly logger?: Logger;\n\n constructor(\n db: DatabaseAdapter,\n storage: StorageAdapter,\n tables: RetentionTables,\n logger?: Logger,\n ) {\n this.db = db;\n this.storage = storage;\n this.tables = tables;\n this.logger = logger;\n }\n\n async purge(project: Project, options: PurgeOptions): Promise<PurgeResult> {\n const cutoff = new Date(Date.now() - options.ttlDays * 86_400_000).toISOString();\n const { buildIds, pending } = await this.selectBuilds(project, options, cutoff);\n const results = await Promise.all(\n buildIds.map(async (buildId) => ({\n files: await this.deleteBuildFiles(project.id, buildId),\n removed: await this.removeBuild(buildId),\n })),\n );\n const removedBuilds = results.filter((r: { removed: boolean }) => r.removed).length;\n const removedFiles = results.reduce((sum: number, r: { files: number }) => sum + r.files, 0);\n await this.sweepScreenshots(project);\n const removedJobs = await this.purgeFinishedJobs(cutoff);\n this.logger?.info(\n { projectId: project.id, removedBuilds, removedFiles, removedJobs, removedPending: pending },\n \"build retention purge complete\",\n );\n return { removedBuilds, removedFiles };\n }\n\n /**\n * The builds a purge removes: terminal builds past the window (minus the latest per branch when\n * asked), and pending builds idle past it (ADR 0013, decision 9). Persistent builds stay.\n */\n private async selectBuilds(\n project: Project,\n options: PurgeOptions,\n cutoff: string,\n ): Promise<{ buildIds: string[]; pending: number }> {\n const candidates = await this.db.list(this.tables.builds, {\n where: and(\n eq(col(this.tables.builds, \"projectId\"), project.id),\n inArray(col(this.tables.builds, \"status\"), [...TERMINAL_BUILD_STATUSES]),\n lt(col(this.tables.builds, \"updatedAt\"), cutoff),\n ),\n });\n const keep = options.keepLatestPerBranch\n ? await this.latestPerBranch(project.id)\n : new Set<string>();\n const persistent = await new LabelModel(\n this.db,\n this.tables as unknown as LabelTables,\n ).persistentBuildIds(project.id);\n const terminal = candidates\n .map((build) => (build as { id: string }).id)\n .filter((buildId) => !keep.has(buildId));\n // Pending builds leave on their own clock; \"the latest on a branch\" does not shield them.\n const pending = (await pendingCandidates(this.db, project.id, cutoff)).map(\n (candidate) => candidate.buildId,\n );\n return {\n buildIds: [...terminal, ...pending].filter((buildId) => !persistent.has(buildId)),\n pending: pending.length,\n };\n }\n\n /** Reclaim screenshots no snapshot or baseline references any more; never blocks the purge. */\n private async sweepScreenshots(project: Project): Promise<void> {\n try {\n await sweepScreenshotContent(\n {\n db: this.db,\n storage: this.storage,\n tables: {\n snapshots: this.db.tables.snapshots,\n baselines: this.tables.baselines,\n builds: this.tables.builds,\n },\n logger: this.logger,\n },\n project.id,\n );\n } catch (error) {\n this.logger?.warn({ err: error, projectId: project.id }, \"screenshot sweep failed\");\n }\n }\n\n /**\n * Drop finished queue jobs older than the build cutoff (they have no foreign key to a build, and\n * a finished job is only history); never blocks the purge. Queued and running jobs are kept.\n */\n private async purgeFinishedJobs(cutoff: string): Promise<number> {\n try {\n return await new CaptureJobModel(this.db).purgeFinished(cutoff);\n } catch (error) {\n this.logger?.warn({ err: error }, \"capture job purge failed\");\n return 0;\n }\n }\n\n private async removeBuild(buildId: string): Promise<boolean> {\n await new InsightModel(this.db).removeForBuild(buildId);\n await new CaptureJobModel(this.db).removeForBuild(buildId);\n await new BuildComponentModel(this.db).removeForBuild(buildId);\n await new PackageUsageModel(this.db).removeForBuild(buildId);\n await new BuildModel(this.db, this.tables as unknown as BuildTables).remove(buildId);\n return true;\n }\n\n private async latestPerBranch(projectId: string): Promise<Set<string>> {\n const rows = (await this.db.list(this.tables.builds, {\n where: eq(col(this.tables.builds, \"projectId\"), projectId),\n orderBy: desc(col(this.tables.builds, \"updatedAt\")),\n })) as unknown as { gitBranch: string; id: string }[];\n const latest = new Map<string, string>();\n for (const row of rows) {\n const branch = row.gitBranch;\n const current = latest.get(branch);\n if (!current) {\n latest.set(branch, row.id);\n }\n }\n return new Set(latest.values());\n }\n\n private async deleteBuildFiles(projectId: string, buildId: string): Promise<number> {\n const prefix = `${projectId}/builds/${buildId}/`;\n const manifestPath = `${prefix}storybook/manifest.json`;\n let manifestFiles: string[] = [];\n try {\n if (await this.storage.exists(manifestPath)) {\n const raw = await this.storage.read(manifestPath);\n const manifest = JSON.parse(raw.toString(\"utf8\")) as Record<string, string>;\n manifestFiles = Object.values(manifest);\n }\n } catch {\n // ignore — manifest may not exist for old builds\n }\n const files = await this.storage.list(prefix);\n await Promise.all(\n files.map(async (file) => {\n await this.storage.delete(file);\n }),\n );\n // Decrement content refs for this build's manifest\n for (const hash of manifestFiles) {\n try {\n const contentRefs = this.db.tables.contentRefs;\n const existing = (await this.db.get(contentRefs, hash)) as unknown as {\n refCount: number;\n } | null;\n // oxlint-disable-next-line unicorn/prefer-ternary -- refCount check is clearer as if/else\n if (existing) {\n const refCount = existing.refCount;\n // oxlint-disable-next-line unicorn/prefer-ternary -- grace logic is clearer as if/else\n if (refCount <= 1) {\n // Keep for 7-day grace: set refCount 0 and update lastSeenAt, don't delete yet\n await this.db.update(contentRefs, hash, {\n refCount: 0,\n lastSeenAt: new Date().toISOString(),\n });\n } else {\n await this.db.update(contentRefs, hash, {\n refCount: refCount - 1,\n lastSeenAt: new Date().toISOString(),\n });\n }\n }\n } catch {\n // ignore — content_refs may not exist\n }\n }\n // GC stale content_refs with 7-day grace\n try {\n const contentRefs = this.db.tables.contentRefs;\n const cutoff = new Date(Date.now() - 7 * 86_400_000).toISOString();\n const all = (await this.db.list(contentRefs)) as unknown as {\n hash: string;\n refCount: number;\n lastSeenAt: string;\n }[];\n for (const row of all) {\n if (row.refCount === 0 && row.lastSeenAt < cutoff) {\n await this.storage.delete(`content/${row.hash}`).catch(() => {\n // ignore storage delete failure\n });\n // Variants are disposable; never let their deletion affect authoritative data.\n if (/^[a-f0-9]{64}$/u.test(row.hash)) {\n await this.storage.delete(publishedGzipPath(row.hash)).catch(() => {});\n }\n await this.db.remove(contentRefs, row.hash);\n }\n }\n } catch {\n // ignore — content_refs may not exist\n }\n await sweepPublishedGzip(this.storage).catch(() => {});\n return files.length;\n }\n\n async purgeStaleBranches(project: Project, ttlDays: number): Promise<BranchGcResult> {\n if (ttlDays <= 0) {\n return { removedBranches: 0, removedBaselines: 0 };\n }\n const cutoff = new Date(Date.now() - ttlDays * 86_400_000).toISOString();\n const rows = (await this.db.list(this.tables.builds, {\n where: eq(col(this.tables.builds, \"projectId\"), project.id),\n })) as unknown as { gitBranch: string; updatedAt: string }[];\n const stale = collectStaleBranches(rows, project.gitDefaultBranch, cutoff);\n if (stale.size === 0) {\n return { removedBranches: 0, removedBaselines: 0 };\n }\n const baselines = new BaselineModel(this.db, this.tables, this.storage);\n const removedBaselines = await baselines.removeStaleBranches(project.id, stale);\n await this.sweepScreenshots(project);\n this.logger?.info(\n { projectId: project.id, removedBranches: stale.size, removedBaselines },\n \"branch GC complete\",\n );\n return { removedBranches: stale.size, removedBaselines };\n }\n}\n\n/** Options controlling which builds a retention purge removes. */\nexport interface PurgeOptions {\n ttlDays: number;\n keepLatestPerBranch: boolean;\n}\n\n/** Counts of builds and files removed by a retention purge. */\nexport interface PurgeResult {\n removedBuilds: number;\n removedFiles: number;\n}\n\n/** Counts of stale branches and baselines removed. */\nexport interface BranchGcResult {\n removedBranches: number;\n removedBaselines: number;\n}\n\nfunction collectStaleBranches(\n rows: { gitBranch: string; updatedAt: string }[],\n defaultBranch: string,\n cutoff: string,\n): Set<string> {\n const latestAt = branchLatestAt(rows);\n const stale = new Set<string>();\n for (const [branch, updatedAt] of latestAt) {\n if (branch !== defaultBranch && updatedAt < cutoff) {\n stale.add(branch);\n }\n }\n return stale;\n}\n\nfunction branchLatestAt(rows: { gitBranch: string; updatedAt: string }[]): Map<string, string> {\n const latestAt = new Map<string, string>();\n for (const row of rows) {\n const current = latestAt.get(row.gitBranch);\n if (!current || row.updatedAt > current) {\n latestAt.set(row.gitBranch, row.updatedAt);\n }\n }\n return latestAt;\n}\n"],"mappings":"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"}
@@ -1,6 +1,5 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/schema/baseline.d.ts
4
3
  /** A baseline row. */
5
4
  interface Baseline {
6
5
  id: string;
@@ -9,6 +8,8 @@ interface Baseline {
9
8
  viewportName: string;
10
9
  branch: string;
11
10
  snapshotId: string | null;
11
+ /** Build that actually rendered this image; null for unknown or imported origins. */
12
+ sourceBuildId?: string | null;
12
13
  screenshotPath: string;
13
14
  infraHash: string | null;
14
15
  /** JSON `Fingerprint` behind `infraHash`; null on baselines recorded before fingerprints. */
@@ -20,6 +21,5 @@ interface Baseline {
20
21
  createdAt: string;
21
22
  updatedAt: string;
22
23
  }
23
- //#endregion
24
24
  export { Baseline };
25
25
  //# sourceMappingURL=baseline.d.mts.map
@@ -1,6 +1,5 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/schema/build-component.d.ts
4
3
  /**
5
4
  * Row type of `build_components` (ADR 0006, decision 9): one row per component of a published
6
5
  * build, written from the build's Storybook index when publishing ends. A component is the
@@ -20,6 +19,5 @@ interface BuildComponent {
20
19
  hash: string;
21
20
  createdAt: string;
22
21
  }
23
- //#endregion
24
22
  export { BuildComponent };
25
23
  //# sourceMappingURL=build-component.d.mts.map
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { ReviewVerdict } from "../types.mjs";
4
- //#region src/schema/build-review.d.ts
5
4
  /** One verdict on one build. */
6
5
  interface BuildReview {
7
6
  id: string;
@@ -12,6 +11,5 @@ interface BuildReview {
12
11
  body: string | null;
13
12
  createdAt: string;
14
13
  }
15
- //#endregion
16
14
  export { BuildReview };
17
15
  //# sourceMappingURL=build-review.d.mts.map
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { BuildStatus } from "../types.mjs";
4
- //#region src/schema/build.d.ts
5
4
  /** A build row. */
6
5
  interface Build {
7
6
  id: string;
@@ -36,6 +35,5 @@ interface Build {
36
35
  createdAt: string;
37
36
  updatedAt: string;
38
37
  }
39
- //#endregion
40
38
  export { Build };
41
39
  //# sourceMappingURL=build.d.mts.map
@@ -1,7 +1,6 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { JobStatus } from "../adapter/capture-queue.mjs";
4
- //#region src/schema/capture-attempt.d.ts
5
4
  /** A capture attempt row: one queued run of a build's capture pipeline. */
6
5
  interface CaptureAttempt {
7
6
  id: string;
@@ -33,6 +32,5 @@ interface CaptureLog {
33
32
  fields: string | null;
34
33
  createdAt: string;
35
34
  }
36
- //#endregion
37
35
  export { CaptureAttempt, CaptureLog, CaptureLogLevel };
38
36
  //# sourceMappingURL=capture-attempt.d.mts.map
@@ -1,6 +1,5 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/schema/capture-job.d.ts
4
3
  /**
5
4
  * Row types of `capture_jobs`, the database queue (ADR 0006, decision 4).
6
5
  *
@@ -55,6 +54,5 @@ interface CaptureJobRow {
55
54
  createdAt: string;
56
55
  updatedAt: string;
57
56
  }
58
- //#endregion
59
57
  export { CaptureAttemptStatus, CaptureJobEntry, CaptureJobKind, CaptureJobRow, CaptureJobStatus };
60
58
  //# sourceMappingURL=capture-job.d.mts.map
@@ -1,6 +1,5 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/schema/capture-shard.d.ts
4
3
  /** What a row of `capture_shards` is: a stage of a sharded capture, or the claim that finalizes it. */
5
4
  type CaptureShardKind = "plan" | "render" | "finalize";
6
5
  /** Lifecycle of one shard row. */
@@ -30,6 +29,5 @@ interface CaptureShard {
30
29
  createdAt: string;
31
30
  updatedAt: string;
32
31
  }
33
- //#endregion
34
32
  export { CaptureShard, CaptureShardKind, CaptureShardStatus };
35
33
  //# sourceMappingURL=capture-shard.d.mts.map
@@ -1,6 +1,5 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/schema/comment.d.ts
4
3
  /** A comment row. */
5
4
  interface Comment {
6
5
  id: string;
@@ -16,6 +15,5 @@ interface Comment {
16
15
  createdAt: string;
17
16
  updatedAt: string;
18
17
  }
19
- //#endregion
20
18
  export { Comment };
21
19
  //# sourceMappingURL=comment.d.mts.map
@@ -1,6 +1,5 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/schema/connection.d.ts
4
3
  /**
5
4
  * A user's own OAuth connection to an external service (`design` providers today,
6
5
  * issue trackers later). One per user and service; tokens are encrypted at rest.
@@ -19,6 +18,5 @@ interface UserConnection {
19
18
  createdAt: string;
20
19
  updatedAt: string;
21
20
  }
22
- //#endregion
23
21
  export { UserConnection };
24
22
  //# sourceMappingURL=connection.d.mts.map
@@ -1,6 +1,5 @@
1
1
  import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
- //#region src/schema/content-ref.d.ts
4
3
  /** Content ref row for deduplicated assets. */
5
4
  interface ContentRef {
6
5
  hash: string;
@@ -8,6 +7,5 @@ interface ContentRef {
8
7
  lastSeenAt: string;
9
8
  createdAt: string;
10
9
  }
11
- //#endregion
12
10
  export { ContentRef };
13
11
  //# sourceMappingURL=content-ref.d.mts.map