@reddoorla/maintenance 0.95.0 → 0.96.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/README.md +37 -2
  2. package/dist/airtable-SJV4ZSMS.js +9 -0
  3. package/dist/{announce-PGT4SZZB.js → announce-45HVP4J4.js} +7 -7
  4. package/dist/{blux-4KNXUP2K.js → blux-RUFMNOUJ.js} +5 -5
  5. package/dist/{bump-deps-C5G2EFBB.js → bump-deps-EL3TKMIL.js} +6 -6
  6. package/dist/{chunk-XN37VTDB.js → chunk-37B4LVB3.js} +2 -2
  7. package/dist/{chunk-BVEDKMNG.js → chunk-3EA43S3R.js} +28 -10
  8. package/dist/chunk-3EA43S3R.js.map +1 -0
  9. package/dist/{chunk-XHHMM5HX.js → chunk-43SW3FGW.js} +63 -1
  10. package/dist/chunk-43SW3FGW.js.map +1 -0
  11. package/dist/{chunk-WMBLR5VA.js → chunk-44SDGPGQ.js} +3 -3
  12. package/dist/chunk-4UTJ4G6P.js +52 -0
  13. package/dist/chunk-4UTJ4G6P.js.map +1 -0
  14. package/dist/{chunk-R53VL3RI.js → chunk-5CJVUPJU.js} +1 -1
  15. package/dist/{chunk-R53VL3RI.js.map → chunk-5CJVUPJU.js.map} +1 -1
  16. package/dist/{chunk-4AIYWWKV.js → chunk-62YWOFVN.js} +36 -5
  17. package/dist/chunk-62YWOFVN.js.map +1 -0
  18. package/dist/{chunk-22GKJO5F.js → chunk-6INOBNRO.js} +2 -2
  19. package/dist/{chunk-BYGPUUCT.js → chunk-72FJZ6LO.js} +43 -11
  20. package/dist/chunk-72FJZ6LO.js.map +1 -0
  21. package/dist/{chunk-5GUJ36TL.js → chunk-74QRWE6G.js} +2 -2
  22. package/dist/chunk-A5QKNOXW.js +72 -0
  23. package/dist/chunk-A5QKNOXW.js.map +1 -0
  24. package/dist/{chunk-2NQUCS7O.js → chunk-BIYQN7FH.js} +3 -1
  25. package/dist/{chunk-2NQUCS7O.js.map → chunk-BIYQN7FH.js.map} +1 -1
  26. package/dist/{chunk-S7RYFUIK.js → chunk-CA6ADGOC.js} +2 -2
  27. package/dist/chunk-CGQZFQAY.js +38 -0
  28. package/dist/chunk-CGQZFQAY.js.map +1 -0
  29. package/dist/{chunk-YLYXW5PH.js → chunk-CXICCLNM.js} +34 -4
  30. package/dist/chunk-CXICCLNM.js.map +1 -0
  31. package/dist/{chunk-KZRR4NFG.js → chunk-D3CTOCUR.js} +3 -3
  32. package/dist/{chunk-CWXTNPNZ.js → chunk-DVJNYPFR.js} +3 -3
  33. package/dist/chunk-DVJNYPFR.js.map +1 -0
  34. package/dist/{chunk-AVEQUR44.js → chunk-EASYCD6F.js} +60 -17
  35. package/dist/chunk-EASYCD6F.js.map +1 -0
  36. package/dist/{chunk-BCKEP5S6.js → chunk-ECDJDHKT.js} +2 -2
  37. package/dist/{chunk-DMPHP7UP.js → chunk-EQQ4H6L3.js} +2 -2
  38. package/dist/{chunk-WVNDEYXL.js → chunk-GEUC5VGM.js} +11 -5
  39. package/dist/chunk-GEUC5VGM.js.map +1 -0
  40. package/dist/{chunk-Z2K4JPQ3.js → chunk-H445EH33.js} +2 -2
  41. package/dist/chunk-IHHAYXKY.js +351 -0
  42. package/dist/chunk-IHHAYXKY.js.map +1 -0
  43. package/dist/{chunk-FUWFLAWQ.js → chunk-IMYL67JV.js} +3 -3
  44. package/dist/{chunk-3Z6X3BHU.js → chunk-KN52PM63.js} +7 -2
  45. package/dist/{chunk-3Z6X3BHU.js.map → chunk-KN52PM63.js.map} +1 -1
  46. package/dist/{chunk-WJMPCVOR.js → chunk-LAJ3FF54.js} +12 -4
  47. package/dist/chunk-LAJ3FF54.js.map +1 -0
  48. package/dist/{chunk-5UINJNAJ.js → chunk-MZZDMBDT.js} +16 -3
  49. package/dist/chunk-MZZDMBDT.js.map +1 -0
  50. package/dist/{chunk-DFKBTBO3.js → chunk-NVLREB5R.js} +35 -6
  51. package/dist/chunk-NVLREB5R.js.map +1 -0
  52. package/dist/{chunk-AEUABPAJ.js → chunk-OTEVKRAH.js} +3 -3
  53. package/dist/{chunk-KQB2BX4V.js → chunk-P4A6D6HO.js} +4 -4
  54. package/dist/chunk-P7323YVW.js +134 -0
  55. package/dist/chunk-P7323YVW.js.map +1 -0
  56. package/dist/{chunk-4763KQ6O.js → chunk-PXRN2MER.js} +2 -2
  57. package/dist/{chunk-TR3HFH6J.js → chunk-QFSL2L2B.js} +67 -10
  58. package/dist/chunk-QFSL2L2B.js.map +1 -0
  59. package/dist/{chunk-2LYOAB2E.js → chunk-RRE74VWI.js} +2 -2
  60. package/dist/{chunk-LM2NHRCJ.js → chunk-SHGIDTYF.js} +4 -4
  61. package/dist/chunk-SHGIDTYF.js.map +1 -0
  62. package/dist/{chunk-ZQV7UQNE.js → chunk-SQ4FSNYL.js} +2 -2
  63. package/dist/{chunk-OCNSIBMP.js → chunk-T76MA2FH.js} +135 -1
  64. package/dist/{chunk-OCNSIBMP.js.map → chunk-T76MA2FH.js.map} +1 -1
  65. package/dist/{chunk-6R56OKJZ.js → chunk-UIITN37J.js} +2 -2
  66. package/dist/{chunk-CJ2RMTT7.js → chunk-VEIIUCYL.js} +2 -2
  67. package/dist/{chunk-SVAAONYB.js → chunk-WQLNUEZL.js} +12 -2
  68. package/dist/{chunk-SVAAONYB.js.map → chunk-WQLNUEZL.js.map} +1 -1
  69. package/dist/{chunk-WSJ7RO54.js → chunk-XED2DNAI.js} +23 -5
  70. package/dist/chunk-XED2DNAI.js.map +1 -0
  71. package/dist/{chunk-FSGICFXB.js → chunk-XG2A6GIG.js} +17 -5
  72. package/dist/chunk-XG2A6GIG.js.map +1 -0
  73. package/dist/{chunk-TO7DJYLZ.js → chunk-XY7YV2YQ.js} +178 -39
  74. package/dist/chunk-XY7YV2YQ.js.map +1 -0
  75. package/dist/{chunk-HTABUCF4.js → chunk-Y5BVR4HW.js} +3 -3
  76. package/dist/chunk-Y6FCXRIC.js +3972 -0
  77. package/dist/chunk-Y6FCXRIC.js.map +1 -0
  78. package/dist/{chunk-2SLED3NB.js → chunk-YDZ7XGPZ.js} +1 -1
  79. package/dist/{chunk-2SLED3NB.js.map → chunk-YDZ7XGPZ.js.map} +1 -1
  80. package/dist/{chunk-QF6IQQFP.js → chunk-Z7FDJ35L.js} +18 -12
  81. package/dist/chunk-Z7FDJ35L.js.map +1 -0
  82. package/dist/{chunk-5EEMTJMT.js → chunk-ZH5LOQET.js} +4 -4
  83. package/dist/cli/bin.js +49 -31
  84. package/dist/cli/bin.js.map +1 -1
  85. package/dist/cli/commands/audit.js +18 -18
  86. package/dist/client-L7NAUG6J.js +11 -0
  87. package/dist/configs/eslint.js +27 -1
  88. package/dist/configs/eslint.js.map +1 -1
  89. package/dist/configs/playwright-a11y.d.ts +11 -1
  90. package/dist/configs/playwright-a11y.js +3 -1
  91. package/dist/{convert-to-pnpm-YTFREUJK.js → convert-to-pnpm-4DON7IFS.js} +6 -6
  92. package/dist/{db-BBEM7CBX.js → db-5K4CDEGJ.js} +45 -20
  93. package/dist/db-5K4CDEGJ.js.map +1 -0
  94. package/dist/deadletter-G4M3HSVM.js +17 -0
  95. package/dist/{digest-65BOZ7AT.js → digest-5AJGWS33.js} +45 -22
  96. package/dist/digest-5AJGWS33.js.map +1 -0
  97. package/dist/{digest-collectors-EHFY2OQC.js → digest-collectors-MC5NWEUA.js} +12 -10
  98. package/dist/{digest-state-3KHZF7GD.js → digest-state-ZDPKR7MW.js} +18 -2
  99. package/dist/digest-state-ZDPKR7MW.js.map +1 -0
  100. package/dist/{ensure-site-WIDO6LGU.js → ensure-site-XESZHZEB.js} +39 -11
  101. package/dist/ensure-site-XESZHZEB.js.map +1 -0
  102. package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-GJWGKTLD.js} +6 -4
  103. package/dist/{forms-notify-target-2LWF5C5B.js → forms-notify-target-4NPLX2UG.js} +5 -5
  104. package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
  105. package/dist/{github-signals-GAHNOAHS.js → github-signals-DMU74SAX.js} +10 -10
  106. package/dist/{header-image-YN3AQRY3.js → header-image-BRTOLHJF.js} +6 -5
  107. package/dist/header-image-BRTOLHJF.js.map +1 -0
  108. package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-UCLMSSCN.js} +8 -7
  109. package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-UCLMSSCN.js.map} +1 -1
  110. package/dist/{health-mirror-EB76EERV.js → health-mirror-ONRT4DQ5.js} +7 -7
  111. package/dist/{import-airtable-TAKSN4NV.js → import-airtable-IIK2QUUF.js} +3 -3
  112. package/dist/index.d.ts +135 -4
  113. package/dist/index.js +68 -34
  114. package/dist/index.js.map +1 -1
  115. package/dist/{init-FFE4FCO2.js → init-3LMZTCVD.js} +19 -18
  116. package/dist/{init-FFE4FCO2.js.map → init-3LMZTCVD.js.map} +1 -1
  117. package/dist/{launch-4RVWPZGI.js → launch-XTDMC6LD.js} +209 -61
  118. package/dist/launch-XTDMC6LD.js.map +1 -0
  119. package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
  120. package/dist/{lighthouse-airtable-YMH4UA7R.js → lighthouse-airtable-3NOFB42B.js} +3 -3
  121. package/dist/{match-harness-2Q6ISEES.js → match-harness-OW45CB5H.js} +14 -7
  122. package/dist/match-harness-OW45CB5H.js.map +1 -0
  123. package/dist/migrate-NE3DUMTM.js +7 -0
  124. package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +3 -3
  125. package/dist/{onboard-TM2KESGF.js → onboard-CZYBWVWH.js} +6 -6
  126. package/dist/{orchestrate-MWXENQOS.js → orchestrate-TGDFGY52.js} +8 -8
  127. package/dist/{parity-CAJ477PS.js → parity-AEKYSLN6.js} +5 -5
  128. package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
  129. package/dist/{preflight-4VQP7JJL.js → preflight-POZZI3GN.js} +9 -9
  130. package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-AHY5ULJR.js} +38 -8
  131. package/dist/prismic-ci-AHY5ULJR.js.map +1 -0
  132. package/dist/{prismic-models-W6CNLTWH.js → prismic-models-URYGODNY.js} +9 -9
  133. package/dist/prospect/types.d.ts +72 -0
  134. package/dist/prospect/types.js +1 -1
  135. package/dist/{prospect-audit-X4Z67LWW.js → prospect-audit-AJEE3Z4L.js} +16 -7
  136. package/dist/prospect-audit-AJEE3Z4L.js.map +1 -0
  137. package/dist/{prospect-audits-RVYEA4KH.js → prospect-audits-IZVQY3OQ.js} +12 -4
  138. package/dist/protection-audit-3J3UEOVT.js +413 -0
  139. package/dist/protection-audit-3J3UEOVT.js.map +1 -0
  140. package/dist/recipes/sync-configs.js +3 -3
  141. package/dist/{render-YGNGTX3W.js → render-MHUUHPLN.js} +36 -9
  142. package/dist/render-MHUUHPLN.js.map +1 -0
  143. package/dist/{render-from-row-IFSBHWDH.js → render-from-row-65DC6ZP3.js} +4 -4
  144. package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-ONJMBUHJ.js} +3 -3
  145. package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
  146. package/dist/replay-4FVFOMHH.js.map +1 -0
  147. package/dist/{report-4GFD4MW2.js → report-RC5LGPU3.js} +22 -20
  148. package/dist/report-RC5LGPU3.js.map +1 -0
  149. package/dist/{report-mirror-IP64R7B3.js → report-mirror-KKZZ6RMZ.js} +13 -9
  150. package/dist/{report-mirror-IP64R7B3.js.map → report-mirror-KKZZ6RMZ.js.map} +1 -1
  151. package/dist/{schema-2V7BGYVT.js → schema-KWUQEZ5O.js} +1 -1
  152. package/dist/schema-KWUQEZ5O.js.map +1 -0
  153. package/dist/{self-updating-MWXIZLL7.js → self-updating-RKKIYNV4.js} +8 -7
  154. package/dist/{self-updating-MWXIZLL7.js.map → self-updating-RKKIYNV4.js.map} +1 -1
  155. package/dist/{selftest-MAB2PBXQ.js → selftest-AFLR7YV7.js} +10 -10
  156. package/dist/site-lookup-DTOLJ4EA.js +27 -0
  157. package/dist/site-lookup-DTOLJ4EA.js.map +1 -0
  158. package/dist/{site-mirror-UKHREAD6.js → site-mirror-H2FYMN55.js} +23 -9
  159. package/dist/site-mirror-H2FYMN55.js.map +1 -0
  160. package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-BWHR3XX5.js} +8 -7
  161. package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-BWHR3XX5.js.map} +1 -1
  162. package/dist/{submissions-5GKJPCIH.js → submissions-II5J35QM.js} +4 -4
  163. package/dist/{submissions-3I3GAQAX.js → submissions-JDZP35JM.js} +49 -9
  164. package/dist/submissions-JDZP35JM.js.map +1 -0
  165. package/dist/{svelte-codemods-WSV2FGNG.js → svelte-codemods-QWGIRNN6.js} +6 -6
  166. package/dist/{sync-K3PKHVLH.js → sync-NKPDCMYW.js} +5 -5
  167. package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-HGLQV55H.js} +6 -6
  168. package/dist/{upgrade-VTD6V4HS.js → upgrade-WZ44IARG.js} +6 -6
  169. package/dist/util/git.d.ts +48 -9
  170. package/dist/util/git.js +1 -1
  171. package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
  172. package/dist/{websites-TB4TCAWM.js → websites-6DHIHJ2D.js} +4 -2
  173. package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-BIK6LNVC.js} +5 -5
  174. package/dist/write-audits-to-airtable-BIK6LNVC.js.map +1 -0
  175. package/package.json +3 -2
  176. package/dist/airtable-JGCPS7JH.js +0 -9
  177. package/dist/chunk-4AIYWWKV.js.map +0 -1
  178. package/dist/chunk-5UINJNAJ.js.map +0 -1
  179. package/dist/chunk-6VKYQATC.js +0 -1708
  180. package/dist/chunk-6VKYQATC.js.map +0 -1
  181. package/dist/chunk-7MKT4I5T.js +0 -62
  182. package/dist/chunk-7MKT4I5T.js.map +0 -1
  183. package/dist/chunk-AVEQUR44.js.map +0 -1
  184. package/dist/chunk-BVEDKMNG.js.map +0 -1
  185. package/dist/chunk-BYGPUUCT.js.map +0 -1
  186. package/dist/chunk-CWXTNPNZ.js.map +0 -1
  187. package/dist/chunk-DFKBTBO3.js.map +0 -1
  188. package/dist/chunk-FSGICFXB.js.map +0 -1
  189. package/dist/chunk-GASBX52O.js +0 -12
  190. package/dist/chunk-GASBX52O.js.map +0 -1
  191. package/dist/chunk-LM2NHRCJ.js.map +0 -1
  192. package/dist/chunk-QF6IQQFP.js.map +0 -1
  193. package/dist/chunk-TO7DJYLZ.js.map +0 -1
  194. package/dist/chunk-TR3HFH6J.js.map +0 -1
  195. package/dist/chunk-WJMPCVOR.js.map +0 -1
  196. package/dist/chunk-WSJ7RO54.js.map +0 -1
  197. package/dist/chunk-WVNDEYXL.js.map +0 -1
  198. package/dist/chunk-XHHMM5HX.js.map +0 -1
  199. package/dist/chunk-YLYXW5PH.js.map +0 -1
  200. package/dist/client-M2V6FHH5.js +0 -11
  201. package/dist/db-BBEM7CBX.js.map +0 -1
  202. package/dist/digest-65BOZ7AT.js.map +0 -1
  203. package/dist/digest-state-3KHZF7GD.js.map +0 -1
  204. package/dist/ensure-site-WIDO6LGU.js.map +0 -1
  205. package/dist/header-image-YN3AQRY3.js.map +0 -1
  206. package/dist/launch-4RVWPZGI.js.map +0 -1
  207. package/dist/match-harness-2Q6ISEES.js.map +0 -1
  208. package/dist/migrate-K4JETR36.js +0 -7
  209. package/dist/prismic-ci-6GKKZGB5.js.map +0 -1
  210. package/dist/prospect-audit-X4Z67LWW.js.map +0 -1
  211. package/dist/protection-audit-SJO53MEQ.js +0 -168
  212. package/dist/protection-audit-SJO53MEQ.js.map +0 -1
  213. package/dist/render-YGNGTX3W.js.map +0 -1
  214. package/dist/replay-PCSQOGCZ.js.map +0 -1
  215. package/dist/report-4GFD4MW2.js.map +0 -1
  216. package/dist/schema-2V7BGYVT.js.map +0 -1
  217. package/dist/site-mirror-UKHREAD6.js.map +0 -1
  218. package/dist/submissions-3I3GAQAX.js.map +0 -1
  219. /package/dist/{airtable-JGCPS7JH.js.map → airtable-SJV4ZSMS.js.map} +0 -0
  220. /package/dist/{announce-PGT4SZZB.js.map → announce-45HVP4J4.js.map} +0 -0
  221. /package/dist/{blux-4KNXUP2K.js.map → blux-RUFMNOUJ.js.map} +0 -0
  222. /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-EL3TKMIL.js.map} +0 -0
  223. /package/dist/{chunk-XN37VTDB.js.map → chunk-37B4LVB3.js.map} +0 -0
  224. /package/dist/{chunk-WMBLR5VA.js.map → chunk-44SDGPGQ.js.map} +0 -0
  225. /package/dist/{chunk-22GKJO5F.js.map → chunk-6INOBNRO.js.map} +0 -0
  226. /package/dist/{chunk-5GUJ36TL.js.map → chunk-74QRWE6G.js.map} +0 -0
  227. /package/dist/{chunk-S7RYFUIK.js.map → chunk-CA6ADGOC.js.map} +0 -0
  228. /package/dist/{chunk-KZRR4NFG.js.map → chunk-D3CTOCUR.js.map} +0 -0
  229. /package/dist/{chunk-BCKEP5S6.js.map → chunk-ECDJDHKT.js.map} +0 -0
  230. /package/dist/{chunk-DMPHP7UP.js.map → chunk-EQQ4H6L3.js.map} +0 -0
  231. /package/dist/{chunk-Z2K4JPQ3.js.map → chunk-H445EH33.js.map} +0 -0
  232. /package/dist/{chunk-FUWFLAWQ.js.map → chunk-IMYL67JV.js.map} +0 -0
  233. /package/dist/{chunk-AEUABPAJ.js.map → chunk-OTEVKRAH.js.map} +0 -0
  234. /package/dist/{chunk-KQB2BX4V.js.map → chunk-P4A6D6HO.js.map} +0 -0
  235. /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
  236. /package/dist/{chunk-2LYOAB2E.js.map → chunk-RRE74VWI.js.map} +0 -0
  237. /package/dist/{chunk-ZQV7UQNE.js.map → chunk-SQ4FSNYL.js.map} +0 -0
  238. /package/dist/{chunk-6R56OKJZ.js.map → chunk-UIITN37J.js.map} +0 -0
  239. /package/dist/{chunk-CJ2RMTT7.js.map → chunk-VEIIUCYL.js.map} +0 -0
  240. /package/dist/{chunk-HTABUCF4.js.map → chunk-Y5BVR4HW.js.map} +0 -0
  241. /package/dist/{chunk-5EEMTJMT.js.map → chunk-ZH5LOQET.js.map} +0 -0
  242. /package/dist/{client-M2V6FHH5.js.map → client-L7NAUG6J.js.map} +0 -0
  243. /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-4DON7IFS.js.map} +0 -0
  244. /package/dist/{digest-collectors-EHFY2OQC.js.map → deadletter-G4M3HSVM.js.map} +0 -0
  245. /package/dist/{fleet-state-UEVZ3X3J.js.map → digest-collectors-MC5NWEUA.js.map} +0 -0
  246. /package/dist/{freeze-A6MUCZU4.js.map → fleet-state-GJWGKTLD.js.map} +0 -0
  247. /package/dist/{forms-notify-target-2LWF5C5B.js.map → forms-notify-target-4NPLX2UG.js.map} +0 -0
  248. /package/dist/{import-airtable-TAKSN4NV.js.map → freeze-GUQ43EAU.js.map} +0 -0
  249. /package/dist/{github-signals-GAHNOAHS.js.map → github-signals-DMU74SAX.js.map} +0 -0
  250. /package/dist/{health-mirror-EB76EERV.js.map → health-mirror-ONRT4DQ5.js.map} +0 -0
  251. /package/dist/{lighthouse-VHI77PFB.js.map → import-airtable-IIK2QUUF.js.map} +0 -0
  252. /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
  253. /package/dist/{migrate-K4JETR36.js.map → lighthouse-airtable-3NOFB42B.js.map} +0 -0
  254. /package/dist/{notify-IISTHNBR.js.map → migrate-NE3DUMTM.js.map} +0 -0
  255. /package/dist/{orchestrate-MWXENQOS.js.map → notify-K3WVH3DM.js.map} +0 -0
  256. /package/dist/{onboard-TM2KESGF.js.map → onboard-CZYBWVWH.js.map} +0 -0
  257. /package/dist/{parity-CAJ477PS.js.map → orchestrate-TGDFGY52.js.map} +0 -0
  258. /package/dist/{pipeline-YROAY6DT.js.map → parity-AEKYSLN6.js.map} +0 -0
  259. /package/dist/{prospect-audits-RVYEA4KH.js.map → pipeline-UUPXOS2J.js.map} +0 -0
  260. /package/dist/{preflight-4VQP7JJL.js.map → preflight-POZZI3GN.js.map} +0 -0
  261. /package/dist/{prismic-models-W6CNLTWH.js.map → prismic-models-URYGODNY.js.map} +0 -0
  262. /package/dist/{render-from-row-IFSBHWDH.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
  263. /package/dist/{websites-TB4TCAWM.js.map → render-from-row-65DC6ZP3.js.map} +0 -0
  264. /package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-ONJMBUHJ.js.map} +0 -0
  265. /package/dist/{selftest-MAB2PBXQ.js.map → selftest-AFLR7YV7.js.map} +0 -0
  266. /package/dist/{submissions-5GKJPCIH.js.map → submissions-II5J35QM.js.map} +0 -0
  267. /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-QWGIRNN6.js.map} +0 -0
  268. /package/dist/{sync-K3PKHVLH.js.map → sync-NKPDCMYW.js.map} +0 -0
  269. /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-HGLQV55H.js.map} +0 -0
  270. /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-WZ44IARG.js.map} +0 -0
  271. /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
  272. /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → websites-6DHIHJ2D.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/recipes/health-endpoint/index.ts","../src/recipes/health-endpoint/template.ts"],"sourcesContent":["import { access, mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { withRecipe } from \"../_with-recipe.js\";\nimport { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\nimport { formatWithPrettier, PRETTIER_FLAG_NOTE } from \"../_prettier.js\";\nimport {\n HEALTH_ENDPOINT_RELATIVE,\n HEALTH_ENDPOINT_TEMPLATE,\n HEALTH_ENDPOINT_TEMPLATE_NO_PRISMIC,\n} from \"./template.js\";\n\nexport type HealthEndpointDeps = { spawn: SpawnFn };\n\n/** Where a Prismic client module lives across the fleet — `.ts` on sources,\n * `.js`/`.mjs` on some compiled clones. Existence of ANY picks the Prismic\n * variant; none => the import-free variant (a static import of a missing module\n * breaks the Vite build, which feature-detection can't rescue). */\nconst PRISMICIO_CANDIDATES = [\n \"src/lib/prismicio.ts\",\n \"src/lib/prismicio.js\",\n \"src/lib/prismicio.mjs\",\n];\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function siteHasPrismic(sitePath: string): Promise<boolean> {\n for (const rel of PRISMICIO_CANDIDATES) {\n if (await fileExists(join(sitePath, rel))) return true;\n }\n return false;\n}\n\n/**\n * Writes a resilient `src/routes/health/+server.ts` if the route doesn't already\n * exist. The function-health audit fetches this endpoint; existing sites need it\n * or the Report Health Gate blocks their Maintenance reports on \"unknown\". Operator\n * edits to an existing endpoint are never clobbered (noop on existing file).\n *\n * Two heterogeneity fixes the fleet rollout exposed: (1) it detects whether the\n * site has a `$lib/prismicio` module and writes the Prismic-free variant when it\n * doesn't (a static import of a missing module fails the build); (2) it runs the\n * SITE's own prettier on the written file before committing, so the file matches\n * that site's formatting config (tabs/spaces, quote style vary fleet-wide) and\n * doesn't red the CI format check. Prettier is best-effort — a site without it\n * installed commits unformatted with a flag note rather than failing the recipe.\n */\nexport async function healthEndpoint(\n site: Site,\n deps: HealthEndpointDeps = { spawn: defaultSpawn },\n): Promise<RecipeResult> {\n const target = join(site.path, HEALTH_ENDPOINT_RELATIVE);\n return withRecipe<{ target: string; template: string; hasPrismic: boolean }>({\n name: \"health-endpoint\",\n site,\n plan: async () => {\n if (await fileExists(target)) {\n return { kind: \"noop\", notes: `${HEALTH_ENDPOINT_RELATIVE} already exists` };\n }\n const hasPrismic = await siteHasPrismic(site.path);\n const template = hasPrismic ? HEALTH_ENDPOINT_TEMPLATE : HEALTH_ENDPOINT_TEMPLATE_NO_PRISMIC;\n return { kind: \"apply\", plan: { target, template, hasPrismic } };\n },\n apply: async (planned, { commit, cwd }) => {\n await mkdir(dirname(planned.target), { recursive: true });\n await writeFile(planned.target, planned.template, \"utf-8\");\n\n const notes: string[] = [];\n if (!planned.hasPrismic) {\n notes.push(\"no $lib/prismicio — wrote Prismic-free /health (prismic: skipped)\");\n }\n\n // Format to the SITE's own prettier config so CI's format check stays green\n // across the heterogeneous fleet. Best-effort: a site without prettier just\n // commits unformatted with a flag note (never fails the /health rollout).\n if (!(await formatWithPrettier(deps.spawn, cwd, [HEALTH_ENDPOINT_RELATIVE]))) {\n notes.push(PRETTIER_FLAG_NOTE);\n }\n\n await commit(\"feat: add /health endpoint (function-health probe)\");\n return notes.length > 0 ? { kind: \"ok\", notes: notes.join(\"; \") } : { kind: \"ok\" };\n },\n });\n}\n","/** Relative path inside a site where the /health endpoint lives. Deploys as a\n * Netlify function (adapter-netlify v6); the function-health audit fetches it. */\nexport const HEALTH_ENDPOINT_RELATIVE = \"src/routes/health/+server.ts\";\n\n/** Resilient /health for existing sites. Unlike the starter's, it does NOT\n * statically import { createClient, isPlaceholderRepo } from \"$lib/prismicio\" —\n * older clones lack `isPlaceholderRepo`, and a missing named import breaks the\n * Vite build. Instead it namespace-imports and feature-detects: `createClient`\n * is universal to Prismic SvelteKit sites; a missing one => prismic:\"skipped\"\n * (the gate treats CMS as \"never ran\" and keeps blocking — never a false green).\n * Any probe error => \"error\" (reds CMS, fail-safe). The gate keys off ok + prismic. */\nexport const HEALTH_ENDPOINT_TEMPLATE = `import { json } from \"@sveltejs/kit\";\nimport { env as privateEnv } from \"$env/dynamic/private\";\nimport { env as publicEnv } from \"$env/dynamic/public\";\nimport * as prismicio from \"$lib/prismicio\";\nimport type { RequestHandler } from \"./$types\";\n\n// A live probe — must never be prerendered.\nexport const prerender = false;\n\ntype PrismicHealth = \"ok\" | \"error\" | \"skipped\";\n\ntype PrismicClient = { getRepository: () => Promise<unknown> };\ntype PrismicModule = {\n createClient?: (opts: { fetch: typeof globalThis.fetch }) => PrismicClient;\n isPlaceholderRepo?: boolean;\n};\n\n// Server-side Prismic reachability probe. Hits the PUBLIC repository-metadata\n// endpoint (getRepository — no token), time-boxed, returning ONLY a status\n// string; the repository body is never included (/health is public).\nasync function probePrismic(fetch: typeof globalThis.fetch): Promise<PrismicHealth> {\n const mod = prismicio as PrismicModule;\n const isPlaceholder = mod.isPlaceholderRepo ?? false;\n if (isPlaceholder || typeof mod.createClient !== \"function\") return \"skipped\";\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n const client = mod.createClient({ fetch });\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error(\"prismic health probe timed out\")), 5000);\n });\n await Promise.race([client.getRepository(), timeout]);\n return \"ok\";\n } catch {\n return \"error\";\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\nexport const GET: RequestHandler = async ({ fetch }) => {\n const prismic = await probePrismic(fetch);\n const forms = {\n ingestUrl: !!privateEnv.FORMS_INGEST_URL,\n ingestToken: !!privateEnv.FORMS_INGEST_TOKEN,\n turnstile: !!publicEnv.PUBLIC_TURNSTILE_SITE_KEY?.trim(),\n };\n // We're inside the handler, so the function ran; ok is false only when the\n // Prismic probe actively errored.\n const ok = prismic !== \"error\";\n return json({ ok, prismic, forms });\n};\n`;\n\n/** Prismic-free /health for sites WITHOUT a `$lib/prismicio` module (non-Prismic\n * sites, e.g. a static/bespoke build). Feature-detection can survive a missing\n * *export*, but a static `import * as prismicio from \"$lib/prismicio\"` fails the\n * Vite build when the *module* doesn't exist — so those sites need a variant with\n * no import at all. CMS is reported as a constant \"skipped\" (the gate treats it as\n * \"never ran\" and never false-greens it). Same JSON shape as the Prismic template\n * (ok/prismic/forms) so the function-health audit parses both identically. */\nexport const HEALTH_ENDPOINT_TEMPLATE_NO_PRISMIC = `import { json } from \"@sveltejs/kit\";\nimport { env as privateEnv } from \"$env/dynamic/private\";\nimport { env as publicEnv } from \"$env/dynamic/public\";\nimport type { RequestHandler } from \"./$types\";\n\n// A live probe — must never be prerendered.\nexport const prerender = false;\n\nexport const GET: RequestHandler = async () => {\n // This site has no $lib/prismicio module, so there is no CMS to probe: report\n // \"skipped\" (the gate treats CMS as \"never ran\", never a false green).\n const prismic = \"skipped\" as const;\n const forms = {\n ingestUrl: !!privateEnv.FORMS_INGEST_URL,\n ingestToken: !!privateEnv.FORMS_INGEST_TOKEN,\n turnstile: !!publicEnv.PUBLIC_TURNSTILE_SITE_KEY?.trim(),\n };\n // We're inside the handler, so the function ran.\n const ok = true;\n return json({ ok, prismic, forms });\n};\n`;\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,QAAQ,OAAO,iBAAiB;AACzC,SAAS,SAAS,YAAY;;;ACCvB,IAAM,2BAA2B;AASjC,IAAM,2BAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4DjC,IAAM,sCAAsC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ADrDnD,IAAM,uBAAuB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AACF;AAEA,eAAe,WAAW,MAAgC;AACxD,MAAI;AACF,UAAM,OAAO,IAAI;AACjB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,eAAe,UAAoC;AAChE,aAAW,OAAO,sBAAsB;AACtC,QAAI,MAAM,WAAW,KAAK,UAAU,GAAG,CAAC,EAAG,QAAO;AAAA,EACpD;AACA,SAAO;AACT;AAgBA,eAAsB,eACpB,MACA,OAA2B,EAAE,OAAO,aAAa,GAC1B;AACvB,QAAM,SAAS,KAAK,KAAK,MAAM,wBAAwB;AACvD,SAAO,WAAsE;AAAA,IAC3E,MAAM;AAAA,IACN;AAAA,IACA,MAAM,YAAY;AAChB,UAAI,MAAM,WAAW,MAAM,GAAG;AAC5B,eAAO,EAAE,MAAM,QAAQ,OAAO,GAAG,wBAAwB,kBAAkB;AAAA,MAC7E;AACA,YAAM,aAAa,MAAM,eAAe,KAAK,IAAI;AACjD,YAAM,WAAW,aAAa,2BAA2B;AACzD,aAAO,EAAE,MAAM,SAAS,MAAM,EAAE,QAAQ,UAAU,WAAW,EAAE;AAAA,IACjE;AAAA,IACA,OAAO,OAAO,SAAS,EAAE,QAAQ,IAAI,MAAM;AACzC,YAAM,MAAM,QAAQ,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;AACxD,YAAM,UAAU,QAAQ,QAAQ,QAAQ,UAAU,OAAO;AAEzD,YAAM,QAAkB,CAAC;AACzB,UAAI,CAAC,QAAQ,YAAY;AACvB,cAAM,KAAK,wEAAmE;AAAA,MAChF;AAKA,UAAI,CAAE,MAAM,mBAAmB,KAAK,OAAO,KAAK,CAAC,wBAAwB,CAAC,GAAI;AAC5E,cAAM,KAAK,kBAAkB;AAAA,MAC/B;AAEA,YAAM,OAAO,oDAAoD;AACjE,aAAO,MAAM,SAAS,IAAI,EAAE,MAAM,MAAM,OAAO,MAAM,KAAK,IAAI,EAAE,IAAI,EAAE,MAAM,KAAK;AAAA,IACnF;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/db/fleet-state.ts"],"sourcesContent":["/** Phase 2 of the Airtable → Turso migration (#539): the fleet-state READ layer.\n *\n * Returns the exact `WebsiteRow` shape the Airtable module returns, so every\n * repoint is an import-only swap at a composition root — the same trick\n * listSubmissionsForSite used for the hybrid-db cutover. The raw values live\n * in `sites`/`site_health`/`site_schedule` (written by the importer / hourly\n * sync); coercion to `WebsiteRow` happens HERE, reusing the Airtable module's\n * own exported coercers (`toVerdict`, `toFrequency`, `parseNotifyRouting`,\n * `parseSecurityAdvisories`, `trimToNull`) so there is one truth for each.\n *\n * The equivalence instrument (tests/db/fleet-state.test.ts) pins this module\n * to `mapRow` field-by-field: for a fixture record, `mapRow(rec)` must deep-\n * equal the row read back through here after an import. A new WebsiteRow\n * field fails that test until this module carries it.\n *\n * `headerImage` is the ONE deliberate exception (design D5): Airtable stopped\n * being its source — the bytes belong in `sites.header_image*`. Those columns\n * ARE now written: the header-image CLI dual-writes on every generation and the\n * one-shot backfill copied the rest, so as of 2026-08-25 production carries a\n * BLOB for 12 of the 13 maintained sites (the 13th, LA Homelessness Youth, has\n * no header image in Airtable either — its reports are blocked at approve for\n * exactly that). An earlier version of this comment said the columns were empty\n * fleet-wide; that was true when written and is no longer.\n *\n * The `url` is \"\" because the bytes live in the row itself, not behind a signed\n * URL — which is why `url` is NOT a usable handle. A consumer that needs the\n * image calls `loadHeaderImage(db, siteId)` for the bytes; the send path still\n * fetches the Airtable attachment, and moving it over is its own change.\n */\nimport { sql, type Selectable, type Updateable } from \"kysely\";\nimport type { Db } from \"./client.js\";\nimport type { ReportsTable, SiteHealthTable, SiteScheduleTable, SitesTable } from \"./schema.js\";\nimport {\n SITE_FIELDS,\n siteValueFor,\n healthColumnFor,\n scheduleColumnFor,\n mapReportRecord,\n mapWebsiteRecord,\n type RawRecord,\n} from \"./import-airtable.js\";\nimport type { AirtableCellValue } from \"../reports/airtable/websites.js\";\nimport {\n toReportType,\n parseAutoEvidence,\n type ReportRow,\n type DeliveryStatus,\n} from \"../reports/airtable/reports.js\";\nimport { MAINTENANCE_CHECKLIST, TESTING_CHECKLIST } from \"../reports/checklist.js\";\nimport type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport { canonicalizeStatus } from \"../reports/airtable/site-status.js\";\nimport {\n parseNotifyRouting,\n parseSecurityAdvisories,\n toFrequency,\n toPrismicModelsVerdict,\n toVerdict,\n trimToNull,\n} from \"../reports/airtable/websites.js\";\n\ntype JoinedRow = Record<string, unknown>;\n\nconst str = (v: unknown): string | null => (typeof v === \"string\" ? v : null);\nconst num = (v: unknown): number | null => (typeof v === \"number\" ? v : null);\n/** Stored 1/0/null (importer's b01n) → boolean | null. */\nconst bool = (v: unknown): boolean | null => (typeof v === \"number\" ? v !== 0 : null);\n\n/** Stored JSON array (importer-normalized) → the trimmed string[] mapRow yields. */\nfunction parseAwc(raw: unknown): string[] {\n if (typeof raw !== \"string\" || raw.trim() === \"\") return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed\n .filter((x): x is string => typeof x === \"string\")\n .map((s) => s.trim())\n .filter(Boolean);\n } catch {\n return [];\n }\n}\n\nfunction rowFromJoined(r: JoinedRow): WebsiteRow {\n const name = String(r.name ?? \"\");\n const headerFilename = str(r.header_image_filename);\n const headerType = str(r.header_image_type);\n return {\n id: String(r.id),\n name,\n url: str(r.url) ?? \"\",\n // `sites.status` holds the RAW Airtable cell (the importer stores it verbatim\n // so the hourly parity check compares raw-to-raw). Canonicalizing HERE — the\n // twin of mapRow's seam — is what keeps the #558 equivalence instrument green.\n status: canonicalizeStatus(r.status),\n statusRaw: str(r.status),\n pointOfContact: str(r.point_of_contact),\n maintenanceFreq: toFrequency(r.maintenance_freq, `${name} maintenance`),\n testingFreq: toFrequency(r.testing_freq, `${name} testing`),\n maintenanceFreqRaw: str(r.maintenance_freq),\n testingFreqRaw: str(r.testing_freq),\n maintenanceDay: str(r.maintenance_day),\n testingDay: str(r.testing_day),\n ga4PropertyId: str(r.ga4_property_id),\n searchQuery: str(r.search_query),\n searchConsoleProperty: str(r.search_console_property),\n analyticsSoftFailAt: str(r.analytics_soft_fail_at),\n gitRepo: str(r.git_repo),\n reportRecipientsTo: str(r.report_recipients_to),\n reportRecipientsCc: str(r.report_recipients_cc),\n acceptedWatchConditions: parseAwc(r.accepted_watch_conditions),\n headerImage:\n headerFilename !== null && headerType !== null\n ? { url: \"\", filename: headerFilename, type: headerType }\n : null,\n pScore: num(r.p_score),\n rScore: num(r.r_score),\n bpScore: num(r.bp_score),\n seoScore: num(r.seo_score),\n lastLighthouseAuditAt: str(r.lighthouse_at),\n a11yViolations: num(r.a11y_violations),\n depsDrifted: num(r.deps_drifted),\n depsMajorBehind: num(r.deps_major_behind),\n depsOutdated: num(r.deps_outdated),\n depsMajorOutdated: num(r.deps_major_outdated),\n securityVulnsCritical: num(r.vulns_critical),\n securityVulnsHigh: num(r.vulns_high),\n securityVulnsModerate: num(r.vulns_moderate),\n securityVulnsLow: num(r.vulns_low),\n securityAutoFixAttempts: num(r.auto_fix_attempts),\n lastSecurityAuditAt: str(r.security_audit_at),\n securityAdvisories: parseSecurityAdvisories(r.security_advisories),\n certDaysRemaining: num(r.cert_days_remaining),\n domainCheckedAt: str(r.domain_checked_at),\n netlifyId: trimToNull(r.netlify_id),\n deployStatus: str(r.deploy_status),\n lastDeployAt: str(r.last_deploy_at),\n deployLogUrl: str(r.deploy_log_url),\n deployCheckedAt: str(r.deploy_checked_at),\n functionHealth: toVerdict(r.function_health),\n cmsReachable: toVerdict(r.cms_reachable),\n turnstileWidget: toVerdict(r.turnstile_widget),\n functionHealthCheckedAt: str(r.function_health_checked_at),\n crossbrowserOk: bool(r.crossbrowser_ok),\n mobileOk: bool(r.mobile_ok),\n linksOk: bool(r.links_ok),\n brokenLinks: num(r.broken_links),\n browserCheckedAt: str(r.browser_checked_at),\n reachableOk: toVerdict(r.uptime_reachable),\n titleMetaOk: toVerdict(r.titles_meta_ok),\n copyIntro: trimToNull(r.copy_intro),\n copyContact: trimToNull(r.copy_contact),\n copyFooter: trimToNull(r.copy_footer),\n launchedAt: str(r.launched_at),\n newsletterWebhook: trimToNull(r.newsletter_webhook),\n notifyRouting: parseNotifyRouting(r.notify_routing),\n notifyRoutingRaw: trimToNull(r.notify_routing),\n mailchimpApiKey: trimToNull(r.mailchimp_api_key),\n mailchimpAudienceId: trimToNull(r.mailchimp_audience_id),\n requireTurnstile: typeof r.require_turnstile === \"number\" ? r.require_turnstile !== 0 : false,\n renovateFailingCis: num(r.renovate_failing_cis),\n defaultBranchCi: str(r.default_branch_ci),\n lastCommitAt: str(r.last_commit_at),\n githubSignalsAt: str(r.github_signals_at),\n smokeOk: toVerdict(r.smoke_ok),\n lastSmokeAt: str(r.last_smoke_at),\n formE2eOk: toVerdict(r.form_e2e_ok),\n formE2eCheckedAt: str(r.form_e2e_checked_at),\n prismicModels: toPrismicModelsVerdict(r.prismic_models),\n prismicModelsCheckedAt: str(r.prismic_models_checked_at),\n prismicModelsDrift: str(r.prismic_models_drift),\n prismicAckUntil: str(r.prismic_ack_until),\n nextMaintenanceAt: str(r.next_maintenance_at),\n nextTestingAt: str(r.next_testing_at),\n };\n}\n\n/** Every sites column EXCEPT the header-image BLOB. Since the 2026-08-24\n * backfill, header_image holds 0.7–3.5 MB JPEGs — a selectAll would haul\n * megabytes into every site lookup (per LEAD on the ingest path, ×44 on the\n * fleet list) and Turso bills the bytes. WebsiteRow only needs the metadata\n * columns; the bytes have their own dedicated readers when a consumer wants\n * them. Kept in lockstep with SitesTable by the blob-exclusion test. */\nconst SITE_COLUMNS = [\n \"sites.id\",\n \"sites.slug\",\n \"sites.name\",\n \"sites.url\",\n \"sites.status\",\n \"sites.point_of_contact\",\n \"sites.maintenance_freq\",\n \"sites.testing_freq\",\n \"sites.maintenance_day\",\n \"sites.testing_day\",\n \"sites.ga4_property_id\",\n \"sites.search_query\",\n \"sites.search_console_property\",\n \"sites.git_repo\",\n \"sites.netlify_id\",\n \"sites.report_recipients_to\",\n \"sites.report_recipients_cc\",\n \"sites.copy_intro\",\n \"sites.copy_contact\",\n \"sites.copy_footer\",\n \"sites.newsletter_webhook\",\n \"sites.mailchimp_api_key\",\n \"sites.mailchimp_audience_id\",\n \"sites.notify_routing\",\n \"sites.require_turnstile\",\n \"sites.accepted_watch_conditions\",\n \"sites.prismic_ack_until\",\n \"sites.launched_at\",\n \"sites.header_image_filename\",\n \"sites.header_image_type\",\n \"sites.header_image_generated_at\",\n \"sites.legacy\",\n] as const;\n\n/** The three-table join every read composes from. site_health/site_schedule\n * rows are upserted alongside sites by the importer, but LEFT JOIN anyway —\n * a site row must never vanish from a surface because a health row is absent. */\nfunction joined(db: Db) {\n return db\n .selectFrom(\"sites\")\n .leftJoin(\"site_health\", \"site_health.site_id\", \"sites.id\")\n .leftJoin(\"site_schedule\", \"site_schedule.site_id\", \"sites.id\")\n .select(SITE_COLUMNS)\n .selectAll([\"site_health\", \"site_schedule\"]);\n}\n\n/** Write-through for the site-detail editor (Phase 2): after the Airtable\n * write (still the Phase 2 source of truth), mirror the same cell into\n * `sites` so a Turso-reading page renders the edit immediately instead of\n * waiting for the next hourly sync. The column mapping is the IMPORTER's own\n * map — one truth — and the value gets the importer's empty-clears-to-null\n * semantics. Throws on a column the importer doesn't claim (the lockstep\n * test makes that unreachable for the editor's allowlist); the caller treats\n * a mirror failure as non-fatal — the hourly sync converges it.\n *\n * Handles the non-text columns too (`Require Turnstile` is a checkbox,\n * `Accepted Watch Conditions` a multi-select): the coercion is NOT repeated\n * here, it is delegated to the importer's own `siteValueFor`. That delegation\n * is the whole safety property — parity compares raw-to-raw, so a mirror that\n * stored `\"true\"` where the importer stores `1` would red every hourly run. */\nexport async function mirrorSiteField(\n db: Db,\n siteId: string,\n airtableColumn: string,\n value: AirtableCellValue,\n): Promise<void> {\n await mirrorSiteFields(db, siteId, { [airtableColumn]: value });\n}\n\n/** The multi-column form, and where the work actually happens (#539 Phase 5).\n *\n * `updateLaunched` is why it exists: it flips `Status` AND stamps `Launched at`\n * in one Airtable update, and mirroring those as two separate UPDATEs would\n * open a window where Turso says a site is maintained but never launched.\n *\n * Same contract as {@link mirrorHealthFields}, deliberately — it takes the\n * EXACT FieldSet just written to Airtable (the writers return it, so the mirror\n * cannot carry a different payload), and returns whether a `sites` row matched:\n * a site the hourly sync hasn't imported yet updates 0 rows → false, so the\n * caller can count it honestly instead of claiming it mirrored. An empty\n * FieldSet runs no SQL and returns true — nothing to mirror is not a miss. */\nexport async function mirrorSiteFields(\n db: Db,\n siteId: string,\n fields: Record<string, unknown>,\n): Promise<boolean> {\n const patch: Record<string, string | number | null> = {};\n for (const [airtableColumn, value] of Object.entries(fields)) {\n const col = SITE_FIELDS[airtableColumn];\n if (!col)\n throw new Error(`mirrorSiteFields: importer claims no sites column for '${airtableColumn}'`);\n // Empty text still clears to null (the importer's `s()` does the same); the\n // non-text columns get their own coercion inside siteValueFor.\n patch[col] = siteValueFor(col, value);\n }\n if (Object.keys(patch).length === 0) return true;\n const res = await db\n .updateTable(\"sites\")\n .set(patch as Updateable<SitesTable>)\n .where(\"id\", \"=\", siteId)\n .executeTakeFirst();\n // kysely/libSQL reports numUpdatedRows as a BigInt — compare in BigInt.\n return res.numUpdatedRows > 0n;\n}\n\n/** Mirror a NEWLY CREATED Airtable Websites record into Turso (#539 Phase 5).\n *\n * `ensure-site` CREATES a row, and every other site mirror is an UPDATE, which\n * does nothing at all for a row that does not exist yet — so a site\n * bootstrapped at 09:05 was invisible until the 09:20 sync, and every mirror\n * the rest of the bootstrap fired reported `mirrored=missed` with nothing to\n * update.\n *\n * Maps with the IMPORTER's own `mapWebsiteRecord`, which is also exactly what\n * parity diffs against: that delegation is what makes the mirrored rows\n * parity-clean by construction rather than by a column list someone has to\n * remember to extend.\n *\n * All THREE rows, not just `sites`. Parity reverse-checks `site_health` and\n * `site_schedule` per site and reports a missing one as `(row) ABSENT`, and a\n * later `mirrorHealthFields` would return `missed` forever with no row to hit.\n *\n * Upserts rather than inserts because `ensure-site` is re-run to RESUME a\n * bootstrap. The header_image* columns survive that by construction —\n * `mapWebsiteRecord` does not carry them (Airtable stopped being their source,\n * design D5), so the conflict branch cannot blank a stored plate whose bytes\n * live in no other store. */\nexport async function mirrorSiteInsert(db: Db, rec: RawRecord, computedAt: string): Promise<void> {\n const { site, health, schedule } = mapWebsiteRecord(rec, computedAt);\n await db\n .insertInto(\"sites\")\n .values(site)\n .onConflict((oc) => oc.column(\"id\").doUpdateSet(site))\n .execute();\n await db\n .insertInto(\"site_health\")\n .values(health)\n .onConflict((oc) => oc.column(\"site_id\").doUpdateSet(health))\n .execute();\n await db\n .insertInto(\"site_schedule\")\n .values(schedule)\n .onConflict((oc) => oc.column(\"site_id\").doUpdateSet(schedule))\n .execute();\n}\n\n/** Write-through mirror for the nightly health writers (#539 Phase 3). Takes\n * the EXACT FieldSet just written to Airtable (updateAuditFields /\n * updateGitHubSignals return it), so the mirror can never carry a different\n * payload than the Airtable write it shadows. Resolution + coercion come from\n * the importer's healthColumnFor — one truth; an Airtable column no\n * site_health column claims throws (same contract as mirrorSiteField).\n * Partial by design: absent fields stay untouched, matching\n * updateGitHubSignals' deliberate omission of a null lastCommitAt. Returns\n * whether a site_health row matched: a site the hourly sync hasn't imported\n * yet updates 0 rows → false, so the caller can count it honestly\n * (mirror_missed) instead of claiming it mirrored — it still converges on the\n * next sync, like every mirror. An empty FieldSet runs no SQL and returns\n * true: nothing to mirror is not a miss. */\nexport async function mirrorHealthFields(\n db: Db,\n siteId: string,\n fields: Record<string, unknown>,\n): Promise<boolean> {\n // Per-column value types (number vs text) are guaranteed by healthColumnFor's\n // coercion — the numeric/text split lives there, once — so the patch builds\n // untyped and casts at the .set() boundary (the importer's own idiom).\n const patch: Record<string, string | number | null> = {};\n for (const [field, value] of Object.entries(fields)) {\n const m = healthColumnFor(field);\n if (!m)\n throw new Error(`mirrorHealthFields: importer claims no site_health column for '${field}'`);\n patch[m.col] = m.coerce(value);\n }\n if (Object.keys(patch).length === 0) return true;\n const res = await db\n .updateTable(\"site_health\")\n .set(patch as Updateable<SiteHealthTable>)\n .where(\"site_id\", \"=\", siteId)\n .executeTakeFirst();\n // kysely/libSQL reports numUpdatedRows as a BigInt — compare in BigInt.\n return res.numUpdatedRows > 0n;\n}\n\n/** The site_schedule twin of {@link mirrorHealthFields}, for the nightly\n * next-due write-back. `computedAt` stamps when THIS computation ran — the\n * hourly sync overwrites it with its own import stamp, same as every mirrored\n * value. Empty fields → full no-op (no lone computed_at stamp for a write\n * that carried nothing). Same return contract as mirrorHealthFields: false\n * when the UPDATE matched no site_schedule row (site not yet imported), true\n * otherwise — including the empty-fields no-op. */\nexport async function mirrorScheduleFields(\n db: Db,\n siteId: string,\n fields: Record<string, unknown>,\n computedAt: string,\n): Promise<boolean> {\n const patch: Record<string, string | null> = {};\n for (const [field, value] of Object.entries(fields)) {\n const m = scheduleColumnFor(field);\n if (!m)\n throw new Error(\n `mirrorScheduleFields: importer claims no site_schedule column for '${field}'`,\n );\n patch[m.col] = m.coerce(value);\n }\n if (Object.keys(patch).length === 0) return true;\n patch.computed_at = computedAt;\n const res = await db\n .updateTable(\"site_schedule\")\n .set(patch as Updateable<SiteScheduleTable>)\n .where(\"site_id\", \"=\", siteId)\n .executeTakeFirst();\n return res.numUpdatedRows > 0n;\n}\n\n/** Same contract as the Airtable getWebsiteBySlug: slug is siteSlug(Name),\n * precomputed into the UNIQUE sites.slug column at import time. */\nexport async function getSiteBySlug(db: Db, slug: string): Promise<WebsiteRow | null> {\n const r = await joined(db).where(\"sites.slug\", \"=\", slug).executeTakeFirst();\n return r ? rowFromJoined(r as JoinedRow) : null;\n}\n\n/** By Airtable rec id (the PK) — approve-report's lookup shape. */\nexport async function getSiteById(db: Db, id: string): Promise<WebsiteRow | null> {\n const r = await joined(db).where(\"sites.id\", \"=\", id).executeTakeFirst();\n return r ? rowFromJoined(r as JoinedRow) : null;\n}\n\n/** Same contract as the Airtable listWebsites: every site, one row each.\n * Name-ordered for determinism (Airtable returned table order; no consumer\n * is order-sensitive — the cockpit groups and sorts itself). */\nexport async function listSites(db: Db): Promise<WebsiteRow[]> {\n const rows = await joined(db).orderBy(\"sites.name\").execute();\n return rows.map((r) => rowFromJoined(r as JoinedRow));\n}\n\n// ————————————————————————— reports —————————————————————————\n\n/** stable checklist key → Airtable column name. The importer stores stable keys\n * (mapReportRecord); `ReportRow.checklist` exposes Airtable column names — one\n * derived map, built from the checklist definitions themselves. */\nconst CHECKLIST_FIELD_BY_KEY: ReadonlyMap<string, string> = new Map(\n [...MAINTENANCE_CHECKLIST, ...TESTING_CHECKLIST].map((i) => [i.key, i.field]),\n);\n\nfunction checklistFromStored(raw: string | null): Record<string, boolean> {\n let stored: Record<string, unknown> = {};\n if (typeof raw === \"string\" && raw.trim() !== \"\") {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed))\n stored = parsed as Record<string, unknown>;\n } catch {\n // fall through to all-false — a bad blob must not throw the page\n }\n }\n const out: Record<string, boolean> = {};\n for (const [key, field] of CHECKLIST_FIELD_BY_KEY) out[field] = Boolean(stored[key]);\n return out;\n}\n\n/**\n * `hasRenderedHtml` is passed separately rather than read off `r`, because the\n * list read deliberately does not fetch the body (see REPORT_LIST_COLUMNS) and\n * only ever needed its null-ness. Taking it as an argument means a list row and a\n * full row are both honest inputs, and nothing here can start depending on body\n * CONTENT without the type changing to say so.\n */\nfunction reportRowFromDb(\n r: Omit<Selectable<ReportsTable>, \"rendered_html\">,\n hasRenderedHtml: boolean,\n): ReportRow {\n const p = r.lighthouse_performance;\n const a = r.lighthouse_accessibility;\n const b = r.lighthouse_best_practices;\n const seo = r.lighthouse_seo;\n const lighthouse =\n typeof p === \"number\" &&\n typeof a === \"number\" &&\n typeof b === \"number\" &&\n typeof seo === \"number\"\n ? { performance: p, accessibility: a, bestPractices: b, seo }\n : null;\n return {\n id: r.id,\n reportId: r.report_id ?? \"\",\n siteId: r.site_id ?? \"\",\n reportType: toReportType(r.report_type ?? undefined),\n period: r.period,\n periodStart: r.period_start,\n periodEnd: r.period_end,\n completedOn: r.completed_on,\n lighthouse,\n gaUsersCurrent: r.ga_users_current,\n gaUsersPrevious: r.ga_users_previous,\n searchFoundPage1: bool(r.search_found_page1),\n searchPosition: r.search_position,\n lastTestedDate: r.last_tested_date,\n commentary: r.commentary,\n subjectOverride: r.subject_override,\n draftReady: r.draft_ready !== 0,\n approvedToSend: r.approved_to_send !== 0,\n sentAt: r.sent_at,\n approvedAt: r.approved_at,\n approvedBy: r.approved_by,\n deliveryStatus: (r.delivery_status ?? \"pending\") as DeliveryStatus,\n // The body lives IN the row (rendered_html) — the link points at the\n // dashboard's own preview route instead of an EXPIRING Airtable signed URL.\n // Strictly better for the operator: the old link 404'd once the URL aged out.\n renderedHtmlAttachment: hasRenderedHtml\n ? { url: `/api/reports/${r.id}/preview`, filename: `${r.report_id ?? r.id}.html` }\n : null,\n resendMessageId: r.resend_message_id,\n checklist: checklistFromStored(r.checklist),\n autoEvidence: parseAutoEvidence(r.checklist_auto_evidence),\n sendOverride: r.send_override !== 0,\n overrideReason: r.override_reason,\n overrideBy: r.override_by,\n overrideAt: r.override_at,\n };\n}\n\n/** Every reports column EXCEPT the rendered_html body — the exact hazard\n * SITE_COLUMNS guards against on `sites`, on the table where it currently costs\n * more. Live, all 16 reports carry a body: 1.17 MB total, avg 73 KB. A list read\n * wants the body only to know WHETHER there is one, so `has_rendered_html`\n * computes that in SQL and the bytes stay in the database. Kept in lockstep with\n * ReportsTable by the body-exclusion test. */\nexport const REPORT_LIST_COLUMNS = [\n \"id\",\n \"site_id\",\n \"report_id\",\n \"report_type\",\n \"period\",\n \"period_start\",\n \"period_end\",\n \"completed_on\",\n \"lighthouse_performance\",\n \"lighthouse_accessibility\",\n \"lighthouse_best_practices\",\n \"lighthouse_seo\",\n \"ga_users_current\",\n \"ga_users_previous\",\n \"search_found_page1\",\n \"search_position\",\n \"last_tested_date\",\n \"commentary\",\n \"subject_override\",\n \"draft_ready\",\n \"approved_to_send\",\n \"approved_at\",\n \"approved_by\",\n \"send_override\",\n \"override_reason\",\n \"override_by\",\n \"override_at\",\n \"sent_at\",\n \"delivery_status\",\n \"resend_message_id\",\n \"checklist\",\n \"checklist_auto_evidence\",\n] as const;\n\n/** The one thing a list read still needs from the body: whether there IS one.\n * Computed in SQLite so the bytes never cross the wire. */\nconst HAS_RENDERED_HTML = sql<number>`(rendered_html is not null)`.as(\"has_rendered_html\");\n\n/** Same contract as the Airtable listAllReports. Newest-period-first (Airtable\n * returned manual table order; every consumer filters/sorts itself). */\nexport async function listAllReports(db: Db): Promise<ReportRow[]> {\n const rows = await db\n .selectFrom(\"reports\")\n .select(REPORT_LIST_COLUMNS)\n .select(HAS_RENDERED_HTML)\n .orderBy(\"period_start\", \"desc\")\n .orderBy(\"id\")\n .execute();\n return rows.map((r) => reportRowFromDb(r, r.has_rendered_html !== 0));\n}\n\n/** Same contract as the Airtable listReportsForSite — served by idx_reports_site.\n * Body-free for the same reason as listAllReports: the site-detail page renders\n * a link, never the HTML. */\nexport async function listReportsForSite(db: Db, siteId: string): Promise<ReportRow[]> {\n const rows = await db\n .selectFrom(\"reports\")\n .select(REPORT_LIST_COLUMNS)\n .select(HAS_RENDERED_HTML)\n .where(\"site_id\", \"=\", siteId)\n .orderBy(\"period_start\", \"desc\")\n .orderBy(\"id\")\n .execute();\n return rows.map((r) => reportRowFromDb(r, r.has_rendered_html !== 0));\n}\n\n/** Columns a report writer mirrors after its Airtable write (same pattern as\n * mirrorSiteField): the approve/override flow, the resend-webhook's delivery\n * status, and — since #539 Phase 5 — the drafting path's queue flag and a\n * re-run's refreshed scores.\n *\n * `rendered_html` stays deliberately OUT even though the drafting path now\n * writes it: bodies go through `storeRenderedHtml`, so a request handler\n * holding a patch cannot accidentally write megabytes of HTML. */\nexport type ReportMirrorPatch = Partial<\n Pick<\n ReportsTable,\n | \"approved_to_send\"\n | \"approved_at\"\n | \"approved_by\"\n | \"send_override\"\n | \"override_reason\"\n | \"override_by\"\n | \"override_at\"\n | \"delivery_status\"\n // Phase 4 report review: the console edits commentary and re-renders the\n // page immediately after the write, so the mirror has to carry it or the\n // operator sees their own save as a no-op until the next hourly sync.\n | \"commentary\"\n // Phase 5 drafting path. `draft_ready` is the queue flag `queueDraft`\n // writes — for the new draft AND for every row it supersedes, so without it\n // the console shows a site with two queued reports until the next sync.\n | \"draft_ready\"\n // A re-run (announce/launch reuse) refreshes an existing row's scores so the\n // eventually-sent email is not stale; the console reads the same numbers.\n | \"lighthouse_performance\"\n | \"lighthouse_accessibility\"\n | \"lighthouse_best_practices\"\n | \"lighthouse_seo\"\n | \"ga_users_current\"\n | \"ga_users_previous\"\n | \"search_found_page1\"\n | \"search_position\"\n | \"completed_on\"\n // #643 (the freeze): the send batch mirrors its own `Sent at` stamp. These\n // used to stay out because the hourly sync converged them; that sync is\n // retired, and the console's already-sent guards (approve, the commentary\n // lock, re-render) read `sent_at` from Turso — an unmirrored stamp would\n // disarm all three for every post-flip send.\n | \"sent_at\"\n | \"resend_message_id\"\n >\n>;\n\n/** Mirror an Airtable report write into Turso so the page re-render after an\n * approve/override/bounce shows the new state immediately. Callers route\n * failures through `mirrorWrite`, which decides fatal vs swallowed by the\n * freeze switch; an empty patch is a no-op, never invalid SQL. */\nexport async function mirrorReportPatch(\n db: Db,\n reportId: string,\n patch: ReportMirrorPatch,\n): Promise<void> {\n if (Object.keys(patch).length === 0) return;\n await db.updateTable(\"reports\").set(patch).where(\"id\", \"=\", reportId).execute();\n}\n\n/** Mirror a NEWLY CREATED Airtable Reports record into Turso (#539 Phase 5).\n *\n * Every other report mirror is an UPDATE, which silently does nothing for a row\n * that does not exist yet — so a draft created at 09:05 was invisible to the\n * Turso-backed console until the 09:20 sync. Takes the raw record Airtable\n * echoed back from the create, and maps it with the IMPORTER's own\n * `mapReportRecord`: parity diffs Turso against exactly that function, so\n * delegating is what makes the mirrored row parity-clean by construction rather\n * than by a column list someone has to remember to extend.\n *\n * Upsert, not insert, and the conflict branch drops `rendered_html` for the\n * same reason the importer's does: the body is written later by a separate\n * sharp-bearing step, so a re-mirror carrying null would blank a render that\n * had already succeeded. */\nexport async function mirrorReportInsert(db: Db, rec: RawRecord): Promise<void> {\n const row = mapReportRecord(rec, null);\n const { rendered_html: _rh, ...rowSansHtml } = row;\n void _rh;\n await db\n .insertInto(\"reports\")\n .values(row)\n .onConflict((oc) => oc.column(\"id\").doUpdateSet(rowSansHtml))\n .execute();\n}\n\n/**\n * Store a freshly rendered report body, replacing whatever was there.\n *\n * Deliberately NOT part of `ReportMirrorPatch`: that patch is the request path's\n * write-through (approve, override, delivery status), and a rendered body is\n * produced by a batch job with sharp, never by a dashboard POST. Keeping it out\n * means a handler cannot accidentally write megabytes of HTML from a request.\n *\n * It REPLACES unconditionally. The importer's `when-missing` mode skips a report\n * that already has a body — correct for an import, wrong here: a refresh whose\n * entire purpose is showing the newest commentary must overwrite.\n */\nexport async function storeRenderedHtml(db: Db, reportId: string, html: string): Promise<void> {\n await db.updateTable(\"reports\").set({ rendered_html: html }).where(\"id\", \"=\", reportId).execute();\n}\n\n/** By rec id (the PK) — approve-report's read. */\nexport async function getReportById(db: Db, id: string): Promise<ReportRow | null> {\n const r = await db.selectFrom(\"reports\").selectAll().where(\"id\", \"=\", id).executeTakeFirst();\n return r ? reportRowFromDb(r, r.rendered_html !== null) : null;\n}\n\n/** The preview route's read: the stored rendered body, or null when the report\n * has none (imported while its signed URL was expired, or predates rendering). */\nexport async function getReportHtml(\n db: Db,\n id: string,\n): Promise<{ html: string; reportId: string | null } | null> {\n const r = await db\n .selectFrom(\"reports\")\n .select([\"rendered_html\", \"report_id\"])\n .where(\"id\", \"=\", id)\n .executeTakeFirst();\n if (!r || r.rendered_html === null) return null;\n return { html: r.rendered_html, reportId: r.report_id };\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/github/gh.ts"],"sourcesContent":["import { mkdtemp, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { defaultSpawn, type SpawnFn } from \"../audits/util/spawn.js\";\nimport type { ExistingRuleset, RulesetPayload } from \"./rulesets.js\";\n\n/** Aggregate CI state of a PR's head commit, normalized from GitHub's rollup. */\nexport type CiState = \"passing\" | \"failing\" | \"pending\" | \"none\";\n\n/** GitHub's computed mergeability of a PR. `UNKNOWN` is transient — GitHub is\n * still computing it (e.g. right after a push) — so it should be read as \"not\n * known to conflict\", never as conflicting. */\nexport type PrMergeable = \"MERGEABLE\" | \"CONFLICTING\" | \"UNKNOWN\";\n\n/** A minimal open-PR summary with its head-commit CI rollup state + mergeability. */\nexport type PullRequestSummary = {\n number: number;\n title: string;\n url: string;\n headRef: string;\n ciState: CiState;\n mergeable: PrMergeable;\n};\n\n/**\n * Reject a value before it's interpolated into a `gh api` URL path. The\n * `owner/repo` split methods already validate shape; this is the defense-in-depth\n * guard for the `branch` and file-`path` segments. An unexpected value (`..`, a\n * leading `/`, whitespace, or a URL-structural char like `?#%` or a backslash)\n * could otherwise retarget the endpoint (escape the intended path, smuggle a\n * query string, or traverse). Conservative by design — legit branch names like\n * `maint/self-updating-x` and paths like `.github/workflows/ci.yml` pass.\n *\n * Both branch refs (`maint/self-updating-x`) and file paths\n * (`.github/workflows/ci.yml`) legitimately contain `/`, so a single slash is\n * allowed; what's rejected is `..`, a leading `/`, whitespace, or a\n * URL-structural char (`?`, `#`, `%`, backslash) that could escape or retarget\n * the endpoint.\n */\nexport function assertUrlSegment(kind: \"branch\" | \"path\", value: string): void {\n const structural = /[\\s?#%\\\\]|\\.\\./;\n if (value.length === 0 || value.startsWith(\"/\") || structural.test(value)) {\n throw new Error(\n `unsafe ${kind} for gh api path (illegal characters or traversal): ${JSON.stringify(value)}`,\n );\n }\n}\n\n/** Map GitHub's `statusCheckRollup.state` enum to our normalized CiState. */\nfunction mapRollupState(state: string | null | undefined): CiState {\n switch (state) {\n case \"SUCCESS\":\n return \"passing\";\n case \"FAILURE\":\n case \"ERROR\":\n return \"failing\";\n case \"PENDING\":\n case \"EXPECTED\":\n return \"pending\";\n default:\n return \"none\"; // null/undefined = no checks reported\n }\n}\n\n/** Coerce GitHub's `mergeable` enum to our PrMergeable. Anything unexpected\n * (including the literal `UNKNOWN` GitHub returns while still computing) maps to\n * `UNKNOWN` — i.e. \"not known to conflict\". Only an explicit `CONFLICTING` is. */\nfunction mapMergeable(state: string | null | undefined): PrMergeable {\n return state === \"MERGEABLE\" || state === \"CONFLICTING\" ? state : \"UNKNOWN\";\n}\n\nexport type GitHub = {\n openPullRequest: (\n repo: string,\n pr: { head: string; base: string; title: string; body: string },\n ) => Promise<{ url: string }>;\n /** Turn GitHub's platform auto-merge ON for a repo. Retained as the documented\n * ROLLBACK path — the fleet drives repos to auto-merge OFF (see\n * {@link disableRepoAutoMerge} and `recipes/self-updating`). */\n enableRepoAutoMerge: (repo: string) => Promise<void>;\n /** Turn GitHub's platform auto-merge OFF for a repo. This is the fleet default:\n * platform auto-merge is a per-PR flag anyone with write access can arm, and it\n * merges unattended outside Renovate's `packageRules`. Renovate merges from\n * inside its own run instead. */\n disableRepoAutoMerge: (repo: string) => Promise<void>;\n protectBranch: (repo: string, branch: string, requiredChecks: string[]) => Promise<void>;\n setRepoSecret: (repo: string, name: string, value: string) => Promise<void>;\n repoExists: (repo: string) => Promise<boolean>;\n defaultBranch: (repo: string) => Promise<string>;\n filesOnBranch: (repo: string, branch: string, paths: string[]) => Promise<string[]>;\n /** The raw contents of `path` on `branch`, or null when the file is absent (404).\n * Lets a caller detect a present-but-STALE file (content drift), where\n * `filesOnBranch` only sees existence. */\n fileContentsOnBranch: (repo: string, branch: string, path: string) => Promise<string | null>;\n branchProtectionContexts: (repo: string, branch: string) => Promise<string[]>;\n secretExists: (repo: string, name: string) => Promise<boolean>;\n autoMergeEnabled: (repo: string) => Promise<boolean>;\n findOpenSelfUpdatingPR: (repo: string) => Promise<string | null>;\n /** All open PRs on a repo with each head commit's normalized CI rollup state. */\n openPullRequests: (repo: string) => Promise<PullRequestSummary[]>;\n /** The default branch's latest-commit date + normalized CI rollup, one query. */\n defaultBranchStatus: (repo: string) => Promise<{ ciState: CiState; lastCommitAt: string | null }>;\n /** Renovate PRs (head `renovate/*`) merged at/after `sinceIso`. Used to record\n * `pr_automerged` fleet events. gh-shell (Actions only). */\n mergedRenovatePullRequests: (\n repo: string,\n sinceIso: string,\n ) => Promise<Array<{ number: number; title: string; url: string; mergedAt: string }>>;\n /** Fire a `workflow_dispatch` for `<workflow>` (a filename like `renovate.yml`)\n * on `ref`. Requires the token's `actions:write` scope; a 404 (no such\n * workflow) or 403 (missing scope) surfaces as a thrown error. */\n dispatchWorkflow: (repo: string, workflow: string, ref: string) => Promise<void>;\n /** REPO-sourced rulesets only (`includes_parents=false`): org-level rulesets\n * aren't ours to manage (and don't exist on the Free plan). */\n listRepoRulesets: (repo: string) => Promise<Array<{ id: number; name: string }>>;\n getRuleset: (repo: string, id: number) => Promise<ExistingRuleset>;\n createRuleset: (repo: string, payload: RulesetPayload) => Promise<void>;\n /** The update verb is PUT — `PATCH repos/{repo}/rulesets/{id}` returns 404\n * (verified live 2026-08-01), so a PATCH-based update would silently no-op\n * on every drifted repo while the recipe reported success. */\n updateRuleset: (repo: string, id: number, payload: RulesetPayload) => Promise<void>;\n /** True iff a check-run named `context` exists on `ref`'s HEAD commit. The\n * evidence gate for requiring a status check: with an empty bypass list, a\n * required context that never fires makes the repo permanently unmergeable\n * by everyone — so a context must be OBSERVED before it is required.\n * False-negatives (fresh repo, API hiccup) safely degrade to \"don't require\n * yet\"; false-positives are impossible (we name-match real runs). */\n checkContextObserved: (repo: string, ref: string, context: string) => Promise<boolean>;\n /** \"public\" | \"private\" (| \"internal\" on GHEC). Rulesets on private repos\n * need a paid plan, so callers gate on this rather than attempting. */\n repoVisibility: (repo: string) => Promise<string>;\n /** Every repo in the org (paginated) — the anti-hand-typed-list enumerator.\n * Sweeps driven by a hand-maintained or Airtable-scoped list have already\n * produced two false \"all clear\"s; enumerate from the API instead.\n * secretScanning/pushProtection come from the same listing (no extra call);\n * \"unavailable\" means the token couldn't read security_and_analysis (needs\n * admin/security read) — callers must treat that as unverified, not fine. */\n listOrgRepos: (org: string) => Promise<\n Array<{\n name: string;\n visibility: string;\n archived: boolean;\n secretScanning: string;\n pushProtection: string;\n }>\n >;\n /** Liveness of one workflow file: registered? state? when did it last\n * SUCCEED? Success, not mere existence of a run: a dead credential or\n * broken config still CREATES a run on every cron tick that fails in\n * seconds, keeping created_at perpetually fresh — counting those as alive\n * is exactly the silent-stop blindness this probe exists to catch.\n * A clean 404 is the answer `{present: false}` — any OTHER failure throws,\n * because \"couldn't check\" must never read as \"healthy\" downstream. */\n workflowHealth: (repo: string, filename: string) => Promise<WorkflowHealth>;\n /** Renovate's own Dependency Dashboard issue, which is the only place it\n * reports branches it has DECIDED TO STOP MANAGING. workflowHealth answers\n * \"did Renovate run?\"; this answers \"was it allowed to do anything?\" — a\n * distinction that cost the fleet 9 repos of frozen updates (see\n * renovateBlockedGaps). Absent dashboard is the answer `{present:false}`,\n * not an error: `dependencyDashboard` is a config a repo may disable. */\n dependencyDashboard: (repo: string) => Promise<DependencyDashboard>;\n /** Tip commit of one branch, or `null` if the branch is gone — which is the\n * answer, not an error: a dashboard naming a deleted branch is simply one\n * Renovate has not rewritten yet. */\n branchTip: (repo: string, branch: string) => Promise<BranchTip | null>;\n};\n\nexport type WorkflowHealth =\n { present: false } | { present: true; state: string; lastSuccessAt: string | null };\n\nexport type DependencyDashboard =\n { present: false } | { present: true; blockedBranches: string[]; unknownSections: string[] };\n\n/** Tip commit of a branch. `authorIsMachine` answers \"does any human own this\n * branch?\" — see renovateBlockedGaps for why that decides whether a blocked\n * branch is an orphan or a human's in-flight work. */\nexport type BranchTip = { authorIsMachine: boolean; committedAt: string };\n\n/** The section headings Renovate is known to emit, normalised to lowercase.\n * Sourced from lib/workers/repository/dependency-dashboard.ts at both live\n * majors — the wording moved once already (see BLOCKED_SECTION_RE), so the\n * legacy spellings stay in the set. Anything OUTSIDE this set means our\n * vocabulary has drifted from Renovate's and the blocked check can no longer\n * be trusted; parseUnknownSections reports it rather than reading silence as\n * health. */\nconst KNOWN_DASHBOARD_SECTIONS = new Set([\n // 43+\n \"pending approval\",\n \"group size not met\",\n \"awaiting schedule\",\n \"rate-limited\",\n \"errored\",\n \"pr creation approval required\",\n \"pr edited (blocked)\",\n \"pending status checks\",\n \"pending branch automerge\",\n \"other branches\",\n \"open\",\n \"pr closed (blocked)\",\n \"repository problems\",\n \"config migration needed\",\n \"config migration needed (blocked)\",\n \"deprecations / replacements\",\n \"abandoned dependencies\",\n \"vulnerabilities\",\n \"detected dependencies\",\n // <=42, kept so an older self-hosted Renovate does not read as drift\n \"edited/blocked\",\n \"ignored or blocked\",\n]);\n\n/** Both spellings of the \"I have stopped managing these branches\" heading.\n * Renovate renamed it `Edited/Blocked` -> `PR Edited (Blocked)` in 43.0.0,\n * and the fleet takes its Renovate major from whatever renovatebot/github-action\n * bakes in — so an ordinary action bump moves it. Matching only the current\n * wording would make this surface silently blind on the next rename. */\nconst BLOCKED_SECTION_RE = /^#{1,2}\\s+(?:PR Edited \\(Blocked\\)|Edited\\/Blocked)\\s*$/i;\n\n/** `##` (or the `# ---` join separator) opens a new section; `###` does NOT.\n * Renovate emits `### <category>` subheadings INSIDE a section when\n * dependencyDashboardCategory is set, and treating those as boundaries would\n * truncate the blocked list to nothing. */\nconst SECTION_HEADING_RE = /^#{1,2}\\s+(.*?)\\s*$/;\n\n/**\n * Pull the branches Renovate has given up on out of a Dependency Dashboard\n * body. They live under the blocked heading — \"The following updates have been\n * manually edited so Renovate will no longer make changes.\" — each as a\n * `<!-- rebase-branch=NAME -->` marker.\n *\n * Section-scoped on purpose: `rebase-branch=` markers also appear under \"Open\"\n * (where they mean \"tick to rebase\", i.e. perfectly healthy — 11 fleet repos\n * carry them today), so a whole-body grep would report those repos as blocked.\n * `PR Closed (Blocked)` is deliberately NOT read: it is Renovate's ledger of\n * operator instructions it has honoured (closing a Renovate PR is the\n * documented ignore idiom) and it uses `recreate-branch=`, not `rebase-branch=`.\n */\nexport function parseBlockedBranches(body: string): string[] {\n const found: string[] = [];\n let inBlocked = false;\n for (const line of body.split(\"\\n\")) {\n if (SECTION_HEADING_RE.test(line)) {\n inBlocked = BLOCKED_SECTION_RE.test(line);\n continue;\n }\n if (!inBlocked) continue;\n const marker = line.match(/rebase-branch=([^\\s>]+)/);\n if (marker?.[1]) found.push(marker[1]);\n }\n return [...new Set(found)];\n}\n\n/**\n * Section headings in a dashboard body that Renovate is not known to emit.\n * `parseBlockedBranches` returning `[]` is ambiguous — it means both \"nothing\n * is blocked\" and \"I did not understand this dashboard\" — and this is what\n * separates them, so a heading rename cannot turn the whole fleet green in\n * silence. Same doctrine as workflowHealth: \"couldn't check\" must never read\n * as \"healthy\" downstream.\n */\nexport function parseUnknownSections(body: string): string[] {\n const unknown: string[] = [];\n for (const line of body.split(\"\\n\")) {\n const heading = line.match(/^##\\s+(.*?)\\s*$/);\n const title = heading?.[1];\n if (!title) continue;\n if (!KNOWN_DASHBOARD_SECTIONS.has(title.toLowerCase())) unknown.push(title);\n }\n return [...new Set(unknown)];\n}\n\nexport function makeGitHub(deps: { token: string; spawn?: SpawnFn }): GitHub {\n const spawn = deps.spawn ?? defaultSpawn;\n const env = { ...process.env, GH_TOKEN: deps.token };\n\n async function gh(args: string[]): Promise<string> {\n const r = await spawn(\"gh\", args, { env, timeoutMs: 60_000 });\n if (r.code !== 0) throw new Error(`gh ${args[0]} failed (code ${r.code}): ${r.stderr.trim()}`);\n return r.stdout;\n }\n\n /** `gh api` with a JSON request body. Ruleset payloads have nested arrays/\n * objects that `-f`/`-F` field flags cannot express, and the spawn wrapper\n * has no stdin — so the body rides a private temp file via `--input`. */\n async function ghJson(method: \"POST\" | \"PUT\", path: string, body: unknown): Promise<string> {\n const dir = await mkdtemp(join(tmpdir(), \"gh-json-\"));\n const file = join(dir, \"body.json\");\n try {\n await writeFile(file, JSON.stringify(body), \"utf-8\");\n return await gh([\"api\", \"-X\", method, path, \"--input\", file]);\n } finally {\n await rm(dir, { recursive: true, force: true });\n }\n }\n\n return {\n async openPullRequest(repo, pr) {\n const out = await gh([\n \"pr\",\n \"create\",\n \"--repo\",\n repo,\n \"--head\",\n pr.head,\n \"--base\",\n pr.base,\n \"--title\",\n pr.title,\n \"--body\",\n pr.body,\n ]);\n return { url: out.trim() };\n },\n async enableRepoAutoMerge(repo) {\n await gh([\"api\", \"-X\", \"PATCH\", `repos/${repo}`, \"-F\", \"allow_auto_merge=true\"]);\n },\n async disableRepoAutoMerge(repo) {\n await gh([\"api\", \"-X\", \"PATCH\", `repos/${repo}`, \"-F\", \"allow_auto_merge=false\"]);\n },\n async protectBranch(repo, branch, requiredChecks) {\n assertUrlSegment(\"branch\", branch);\n const args = [\n \"api\",\n \"-X\",\n \"PUT\",\n `repos/${repo}/branches/${branch}/protection`,\n \"-H\",\n \"Accept: application/vnd.github+json\",\n \"-F\",\n \"required_status_checks[strict]=true\",\n ...requiredChecks.flatMap((c) => [\"-f\", `required_status_checks[contexts][]=${c}`]),\n \"-F\",\n \"enforce_admins=true\",\n \"-F\",\n \"required_pull_request_reviews=null\",\n \"-F\",\n \"restrictions=null\",\n ];\n await gh(args);\n },\n async setRepoSecret(repo, name, value) {\n await gh([\"secret\", \"set\", name, \"--repo\", repo, \"--body\", value]);\n },\n async repoExists(repo) {\n const r = await spawn(\"gh\", [\"api\", `repos/${repo}`], { env, timeoutMs: 60_000 });\n return r.code === 0;\n },\n async defaultBranch(repo) {\n const out = await gh([\"api\", `repos/${repo}`, \"--jq\", \".default_branch\"]);\n return out.trim();\n },\n // filesOnBranch and branchProtectionContexts call `spawn` directly (not the\n // throwing `gh()` helper) because a 404 is an expected, meaningful answer —\n // \"file/protection absent\" — not an error. The remaining readers use `gh()`\n // since a non-200 there is a genuine failure (e.g. missing token scope).\n async filesOnBranch(repo, branch, paths) {\n assertUrlSegment(\"branch\", branch);\n const present: string[] = [];\n for (const p of paths) {\n assertUrlSegment(\"path\", p);\n const r = await spawn(\"gh\", [`api`, `repos/${repo}/contents/${p}?ref=${branch}`], {\n env,\n timeoutMs: 60_000,\n });\n if (r.code === 0) present.push(p);\n }\n return present;\n },\n async fileContentsOnBranch(repo, branch, path) {\n // Same spawn-directly rationale as filesOnBranch: a 404 (file absent) is an\n // expected answer (null), not an error. The `raw` media type returns the file\n // bytes verbatim (no base64 envelope) so the result compares directly against a\n // canonical template.\n assertUrlSegment(\"branch\", branch);\n assertUrlSegment(\"path\", path);\n const r = await spawn(\n \"gh\",\n [\n \"api\",\n `repos/${repo}/contents/${path}?ref=${branch}`,\n \"-H\",\n \"Accept: application/vnd.github.raw\",\n ],\n { env, timeoutMs: 60_000 },\n );\n if (r.code !== 0) return null; // 404 = file absent on this branch\n return r.stdout;\n },\n async branchProtectionContexts(repo, branch) {\n assertUrlSegment(\"branch\", branch);\n const r = await spawn(\n \"gh\",\n [\n \"api\",\n `repos/${repo}/branches/${branch}/protection`,\n \"--jq\",\n \".required_status_checks.contexts[]?\",\n ],\n { env, timeoutMs: 60_000 },\n );\n if (r.code !== 0) return []; // 404 = no protection configured\n return r.stdout\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n },\n async secretExists(repo, name) {\n // per_page=100: the REST default of 30 would false-negative on a repo with >30 secrets,\n // wrongly reporting an existing secret absent (→ a needless overwrite).\n const out = await gh([\n \"api\",\n `repos/${repo}/actions/secrets?per_page=100`,\n \"--jq\",\n \".secrets[].name\",\n ]);\n return out\n .split(\"\\n\")\n .map((l) => l.trim())\n .includes(name);\n },\n async autoMergeEnabled(repo) {\n const out = await gh([\"api\", `repos/${repo}`, \"--jq\", \".allow_auto_merge\"]);\n return out.trim() === \"true\";\n },\n async findOpenSelfUpdatingPR(repo) {\n // per_page=100: with the REST default of 30, a repo with >30 open PRs (plausible under\n // Renovate) could page past the existing self-updating PR and open a duplicate.\n const out = await gh([\n \"api\",\n `repos/${repo}/pulls?state=open&per_page=100`,\n \"--jq\",\n '.[] | select(.head.ref | startswith(\"maint/self-updating-\")) | .html_url',\n ]);\n const first = out\n .split(\"\\n\")\n .map((l) => l.trim())\n .find((l) => l.length > 0);\n return first ?? null;\n },\n async openPullRequests(repo) {\n const [owner, name, ...rest] = repo.split(\"/\");\n if (!owner || !name || rest.length > 0) {\n throw new Error(`openPullRequests: expected \"owner/repo\", got \"${repo}\"`);\n }\n const query =\n \"query($owner:String!,$name:String!){repository(owner:$owner,name:$name){\" +\n \"pullRequests(states:OPEN,first:100,orderBy:{field:CREATED_AT,direction:DESC}){nodes{number title url headRefName mergeable \" +\n \"commits(last:1){nodes{commit{statusCheckRollup{state}}}}}}}}\";\n const out = await gh([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `name=${name}`,\n ]);\n const parsed = JSON.parse(out) as {\n data?: {\n repository?: {\n pullRequests?: {\n nodes?: Array<{\n number: number;\n title: string;\n url: string;\n headRefName: string;\n mergeable?: string;\n commits?: {\n nodes?: Array<{ commit?: { statusCheckRollup?: { state?: string } } }>;\n };\n }>;\n };\n };\n };\n };\n const nodes = parsed.data?.repository?.pullRequests?.nodes ?? [];\n return nodes.map((n) => ({\n number: n.number,\n title: n.title,\n url: n.url,\n headRef: n.headRefName,\n ciState: mapRollupState(n.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state),\n mergeable: mapMergeable(n.mergeable),\n }));\n },\n async defaultBranchStatus(repo) {\n const [owner, name, ...rest] = repo.split(\"/\");\n if (!owner || !name || rest.length > 0) {\n throw new Error(`defaultBranchStatus: expected \"owner/repo\", got \"${repo}\"`);\n }\n const query =\n \"query($owner:String!,$name:String!){repository(owner:$owner,name:$name){\" +\n \"defaultBranchRef{target{... on Commit{committedDate statusCheckRollup{state}}}}}}\";\n const out = await gh([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `name=${name}`,\n ]);\n const parsed = JSON.parse(out) as {\n data?: {\n repository?: {\n defaultBranchRef?: {\n target?: { committedDate?: string; statusCheckRollup?: { state?: string } | null };\n } | null;\n };\n };\n };\n const target = parsed.data?.repository?.defaultBranchRef?.target;\n return {\n ciState: mapRollupState(target?.statusCheckRollup?.state),\n lastCommitAt: target?.committedDate ?? null,\n };\n },\n async mergedRenovatePullRequests(repo, sinceIso) {\n const [owner, name, ...rest] = repo.split(\"/\");\n if (!owner || !name || rest.length > 0) {\n throw new Error(`mergedRenovatePullRequests: expected \"owner/repo\", got \"${repo}\"`);\n }\n // per_page=50: comfortably covers a week of Renovate merges on one repo. The\n // list endpoint returns merged_at + head.ref, so the filter is local.\n const out = await gh([\n \"api\",\n `repos/${owner}/${name}/pulls?state=closed&sort=updated&direction=desc&per_page=50`,\n ]);\n const arr = JSON.parse(out) as Array<{\n number: number;\n title: string;\n html_url: string;\n merged_at: string | null;\n head?: { ref?: string };\n }>;\n return arr\n .filter(\n (p) =>\n p.merged_at !== null &&\n p.merged_at >= sinceIso && // ISO8601 UTC strings sort lexicographically\n (p.head?.ref ?? \"\").startsWith(\"renovate/\"),\n )\n .map((p) => ({\n number: p.number,\n title: p.title,\n url: p.html_url,\n mergedAt: p.merged_at as string,\n }));\n },\n async dispatchWorkflow(repo, workflow, ref) {\n const [owner, name, ...rest] = repo.split(\"/\");\n if (!owner || !name || rest.length > 0) {\n throw new Error(`dispatchWorkflow: expected \"owner/repo\", got \"${repo}\"`);\n }\n // Every segment interpolates into the API path, so guard them all like the\n // other write methods do (defense in depth). `owner`/`name` are the most\n // operator-controlled (typed into Airtable's \"Git repo\"); `workflow` is a\n // constant today; `ref` is repo-sourced. A junk value like `repo?x=1` would\n // otherwise smuggle a query string past the bare two-part shape check.\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n assertUrlSegment(\"path\", workflow);\n assertUrlSegment(\"branch\", ref);\n await gh([\n \"api\",\n \"-X\",\n \"POST\",\n `repos/${owner}/${name}/actions/workflows/${workflow}/dispatches`,\n \"-f\",\n `ref=${ref}`,\n ]);\n },\n async listRepoRulesets(repo) {\n // includes_parents=false: only rulesets SOURCED on this repo are ours to\n // heal; an org-level ruleset (paid plans) must never be PUT from here.\n const out = await gh([\n \"api\",\n `repos/${repo}/rulesets?per_page=100&includes_parents=false`,\n \"--jq\",\n '.[] | \"\\\\(.id)\\\\t\\\\(.name)\"',\n ]);\n return out\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.includes(\"\\t\")) // a tab-less line has no id/name split — never fabricate a NaN row from it\n .map((l) => {\n const tab = l.indexOf(\"\\t\");\n return { id: Number(l.slice(0, tab)), name: l.slice(tab + 1) };\n })\n .filter((r) => Number.isSafeInteger(r.id));\n },\n async getRuleset(repo, id) {\n const out = await gh([\"api\", `repos/${repo}/rulesets/${Math.trunc(id)}`]);\n return JSON.parse(out) as ExistingRuleset;\n },\n async createRuleset(repo, payload) {\n await ghJson(\"POST\", `repos/${repo}/rulesets`, payload);\n },\n async updateRuleset(repo, id, payload) {\n // PUT, not PATCH: PATCH on this endpoint 404s (verified live 2026-08-01),\n // which a naive implementation would surface as \"update failed\" at best —\n // or, with a lenient error path, as a silent no-op reported as healed.\n await ghJson(\"PUT\", `repos/${repo}/rulesets/${Math.trunc(id)}`, payload);\n },\n async checkContextObserved(repo, ref, context) {\n assertUrlSegment(\"branch\", ref);\n // spawn-direct (not the throwing gh()): a 404/409 — empty repo, no commits\n // on the ref yet — is an expected answer meaning \"no evidence\", not an\n // error. Any failure degrades to false, i.e. \"don't require the check\n // yet\", which is the safe direction (see checkContextObserved's contract).\n const r = await spawn(\n \"gh\",\n [\n \"api\",\n `repos/${repo}/commits/${ref}/check-runs?per_page=100`,\n \"--jq\",\n \".check_runs[].name\",\n ],\n { env, timeoutMs: 60_000 },\n );\n if (r.code !== 0) return false;\n return r.stdout\n .split(\"\\n\")\n .map((l) => l.trim())\n .includes(context);\n },\n async repoVisibility(repo) {\n const out = await gh([\"api\", `repos/${repo}`, \"--jq\", \".visibility\"]);\n return out.trim();\n },\n async listOrgRepos(org) {\n assertUrlSegment(\"path\", org);\n // --paginate: the org is >27 repos and the default page of 30 is exactly\n // the trap that produced the 2026-07-31 false \"queue is empty\".\n const out = await gh([\n \"api\",\n \"--paginate\",\n `orgs/${org}/repos?per_page=100`,\n \"--jq\",\n '.[] | \"\\\\(.name)\\\\t\\\\(.visibility)\\\\t\\\\(.archived)\\\\t\\\\(.security_and_analysis.secret_scanning.status // \"unavailable\")\\\\t\\\\(.security_and_analysis.secret_scanning_push_protection.status // \"unavailable\")\"',\n ]);\n return out\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0)\n .map((l) => {\n const [name = \"\", visibility = \"\", archived = \"\", ss = \"\", pp = \"\"] = l.split(\"\\t\");\n return {\n name,\n visibility,\n archived: archived === \"true\",\n secretScanning: ss || \"unavailable\",\n pushProtection: pp || \"unavailable\",\n };\n });\n },\n async workflowHealth(repo, filename) {\n // spawn-direct for the workflow GET: 404 (file not registered as a\n // workflow) is an expected ANSWER, not an error. Everything else throws —\n // per the contract, an unreadable workflow must surface as unverified.\n const wf = await spawn(\n \"gh\",\n [\"api\", `repos/${repo}/actions/workflows/${filename}`, \"--jq\", \".state\"],\n { env, timeoutMs: 60_000 },\n );\n if (wf.code !== 0) {\n if (/HTTP 404/.test(wf.stderr)) return { present: false };\n throw new Error(`workflowHealth(${repo}/${filename}) failed: ${wf.stderr.trim()}`);\n }\n // status=success: filter to runs that actually CONCLUDED success. Without\n // it, a schedule whose every run fails (revoked App key, broken config)\n // reads as hours-fresh forever while zero updates flow.\n const runs = await gh([\n \"api\",\n `repos/${repo}/actions/workflows/${filename}/runs?status=success&per_page=1`,\n \"--jq\",\n '.workflow_runs[0].created_at // \"\"',\n ]);\n return { present: true, state: wf.stdout.trim(), lastSuccessAt: runs.trim() || null };\n },\n async dependencyDashboard(repo) {\n // Every open Dependency Dashboard, not just the first: the App-identity\n // migration left several repos holding TWO (alamo-anatomy had #3 and\n // #39), and the stale one is not always the one carrying the blocked\n // section. `select(.pull_request | not)` because /issues returns PRs too.\n //\n // --paginate is load-bearing, not defensive: /issues returns issues AND\n // PRs newest-first, the dashboard is created at onboarding and so is\n // typically the OLDEST open issue in the repo, and the title filter runs\n // client-side in jq AFTER the page cut. Without it, a repo with 100 open\n // items newer than its dashboard reads as {present:false} — which this\n // surface deliberately treats as not-a-gap, i.e. a blocked repo would\n // report clean. (Verified against renovatebot/renovate itself, whose\n // dashboard is issue #2958 from 2018 and carries a live blocked section.)\n const out = await gh([\n \"api\",\n \"--paginate\",\n `repos/${repo}/issues?state=open&per_page=100`,\n \"--jq\",\n '[.[] | select(.pull_request | not) | select(.title == \"Dependency Dashboard\") | (.body // \"\")] | join(\"\\\\n# ---\\\\n\")',\n ]);\n const body = out.trim();\n if (body.length === 0) return { present: false };\n return {\n present: true,\n blockedBranches: parseBlockedBranches(body),\n unknownSections: parseUnknownSections(body),\n };\n },\n async branchTip(repo, branch) {\n // Raw, not percent-encoded: the ref segment legitimately contains `/`\n // (`renovate/all-minor-patch`) and GitHub resolves it as a ref only when\n // the slash is literal. assertUrlSegment is what keeps that safe.\n assertUrlSegment(\"branch\", branch);\n let out: string;\n try {\n out = await gh([\n \"api\",\n `repos/${repo}/commits/${branch}`,\n \"--jq\",\n // .author is the GitHub ACCOUNT (null when the commit email matches\n // none), .commit.author is the raw git identity, which always exists.\n '{type: (.author.type // \"\"), login: (.author.login // \"\"), email: .commit.author.email, name: .commit.author.name, date: .commit.committer.date}',\n ]);\n } catch (e) {\n if (/HTTP 404/.test(e instanceof Error ? e.message : String(e))) return null;\n throw e;\n }\n const raw = JSON.parse(out) as {\n type: string;\n login: string;\n email: string;\n name: string;\n date: string;\n };\n return { authorIsMachine: isMachineAuthor(raw), committedAt: raw.date };\n },\n };\n}\n\n/**\n * Does this commit have a human behind it? Deliberately NOT just\n * `.author.type === \"Bot\"`: the identity whose orphaned branches froze nine\n * repos in August 2026 was `renovate-bot`, a PAT-driven machine account that\n * GitHub reports as type `User`. A rule keyed on the Bot type would have\n * missed the exact incident this audit exists to catch, so the known machine\n * logins are matched by name too.\n *\n * A miss here costs latency, never blindness — renovateBlockedGaps still\n * reports any blocked branch once it goes stale, whoever authored it.\n */\nexport function isMachineAuthor(commit: {\n type: string;\n login: string;\n email: string;\n name: string;\n}): boolean {\n if (commit.type === \"Bot\") return true;\n return [commit.login, commit.email, commit.name].some((f) =>\n /(\\[bot\\]|^renovate-bot$|^renovate-bot@)/i.test(f ?? \"\"),\n );\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/configs/playwright-a11y.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { defineConfig, devices, type PlaywrightTestConfig } from \"@playwright/test\";\n\nexport type A11yRoute = { path: string; name: string };\n\nexport const a11yRoutes: A11yRoute[] = [\n { path: \"/dev/a11y-fixtures\", name: \"a11y fixtures\" },\n { path: \"/dev/animate-in\", name: \"animate-in demo\" },\n];\n\n// Routes smoke-loaded for client-side (hydration) errors only — NOT axe-scanned.\n// Catches the class of bug where build + SSR succeed but client hydration throws\n// and blanks the page (data-dynamiq 2026-06-09: a Svelte 4->5 `run()` referenced\n// a `$state` declared after it → TDZ ReferenceError on hydrate). `/` is the one\n// route every site has; real routes carry a11y debt we don't gate on here, so we\n// assert only that they don't crash on hydrate.\nexport const smokeRoutes: A11yRoute[] = [{ path: \"/\", name: \"home\" }];\n\n// R1.1 (health-gate): the central `smoke` audit (src/audits/smoke.ts) allocates\n// a free port and passes it as REDDOOR_SMOKE_PORT so a zombie vite already\n// squatting the default 5173 can't silently hijack the run and green a stale\n// build. The per-site R1.1 config template honors it, but sites whose\n// playwright.config.ts merely re-exports this shared base (pre-R1.1 adopters\n// the smoke-suite recipe flags-but-never-rewrites) would otherwise ignore it —\n// so honor it here too and every re-exporter inherits the port binding on its\n// next package bump. Unset (local `pnpm test:smoke`) → the fixed 5173.\nconst smokePort = process.env.REDDOOR_SMOKE_PORT;\n\n/**\n * Allocate a free port SYNCHRONOUSLY, for the local path where nothing handed\n * us one. Same trick as src/util/free-port.ts (bind :0, read the assigned port,\n * release it) — but that is async, and this value is needed at module scope\n * while Playwright is still building the config object.\n *\n * It cannot be an async default export instead: sites consume this base by\n * SPREADING it (`{ ...base, use: { ...base.use } }` — see the smoke-suite\n * recipe template). Spreading a Promise yields none of its properties, so the\n * site would get a silently empty config — the exact false-green this whole\n * change exists to remove. The export must stay a plain object.\n *\n * A subprocess is the cost of that constraint: ~30-50ms, once per Playwright\n * run. On any failure we return null and the caller falls back to 5173, which\n * (with reuseExistingServer now false) degrades to a loud \"port already in use\"\n * rather than a silent wrong-server run.\n */\nfunction allocateFreePortSync(): string | null {\n try {\n const out = execFileSync(\n process.execPath,\n [\n \"-e\",\n 'const s=require(\"node:net\").createServer();s.on(\"error\",()=>process.exit(1));' +\n 's.listen(0,\"127.0.0.1\",()=>{const p=s.address().port;s.close(()=>process.stdout.write(String(p)))});',\n ],\n { encoding: \"utf8\", timeout: 5_000, stdio: [\"ignore\", \"pipe\", \"ignore\"] },\n ).trim();\n return /^\\d+$/.test(out) ? out : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the port this run binds to, ONCE per run rather than once per\n * evaluation of this file.\n *\n * REDDOOR_SMOKE_PORT (the central audit already allocated one) wins; otherwise\n * this run allocates its own. The old fallback was the fixed 5173 — the same\n * port a dev server sits on, which is what let `reuseExistingServer` silently\n * hijack local runs (#524).\n *\n * The allocation is pinned back into the environment, and that is the whole\n * point. Playwright re-evaluates the config file in EVERY worker process, not\n * just the main one. Allocating per evaluation gave each worker a different\n * port: the main process started the dev server on one, every worker aimed\n * `baseURL` at another, and the run died with ERR_CONNECTION_REFUSED against a\n * handful of ports nothing was ever serving. Workers are forked and inherit\n * this environment, so writing it back makes every later evaluation agree.\n *\n * It broke on the site suites and not on `audit --only a11y`, which writes its\n * own config and never reads this file — so nothing here caught it.\n */\nfunction resolvePort(): string {\n if (smokePort) return smokePort;\n const allocated = allocateFreePortSync() || \"5173\";\n process.env.REDDOOR_SMOKE_PORT = allocated;\n return allocated;\n}\n\nconst port = resolvePort();\n\n// NOTE: default export only — sites consume this as `import base from\n// \"@reddoorla/maintenance/configs/playwright-a11y\"` (or re-export the default).\n// The old `playwrightA11yConfig` named alias had zero importers and was removed.\nconst playwrightA11yConfig: PlaywrightTestConfig = defineConfig({\n testDir: \"tests\",\n testMatch: /.*\\.spec\\.ts$/,\n fullyParallel: true,\n forbidOnly: !!process.env.CI,\n retries: process.env.CI ? 2 : 0,\n reporter: process.env.CI ? \"github\" : \"list\",\n use: {\n baseURL: `http://localhost:${port}`,\n trace: \"on-first-retry\",\n // Emulate reduced motion fleet-wide: scrollIntoView lands instantly rather\n // than animating, so Playwright's actionability checks don't flake under\n // parallel load, and view transitions fall back to instant. Pairs with the\n // prefers-reduced-motion gate on scroll-behavior in every site's app.css.\n //\n // It MUST sit under `contextOptions` — `reducedMotion` is a\n // BrowserContextOptions member, not a top-level test option. reddoor-starter\n // carried it at the top level of `use` from 2026-06 until 2026-09-01, where\n // Playwright silently ignored it (unknown keys are dropped at runtime) and\n // `pnpm check` never flagged it because svelte-check does not typecheck\n // playwright.config.ts. The emulation was inert that whole time.\n contextOptions: { reducedMotion: \"reduce\" as const },\n },\n projects: [\n {\n name: \"chromium\",\n use: { ...devices[\"Desktop Chrome\"] },\n },\n ],\n webServer: {\n // Portable across pnpm and npm sites — pnpm respects `npm run` too.\n //\n // `--port ... --strictPort` in BOTH cases. It used to be applied only when\n // REDDOOR_SMOKE_PORT allocated one, on the reasoning that we should \"fail\n // loudly rather than let vite drift to a free port the baseURL doesn't\n // point at\" — but that argument covers the unset case just as well. 5173 is\n // equally a fixed port that `baseURL` and the readiness probe below are\n // pinned to, and vite left to itself drifts off it whenever something else\n // holds it.\n //\n // The symptom that exposed this: a non-vite process on 5173 sends vite to\n // 5174 while the probe keeps polling 5173, so the run dies on\n // \"Timed out waiting 120000ms from config.webServer\" — 120 seconds of\n // nothing, naming neither the port nor the squatter. With --strictPort it\n // is an immediate \"Port 5173 is already in use\".\n //\n // --strictPort now only bites if the allocated port is taken in the window\n // between releasing and binding it, which is exactly the case worth failing\n // on.\n command: `npm run vite:dev -- --port ${port} --strictPort`,\n url: `http://localhost:${port}/dev/a11y-fixtures`,\n // NEVER reuse (#524). This used to be `!process.env.CI`, so local runs\n // reused whatever answered the probe URL. The probe only asks \"does this\n // respond?\" — never \"is this serving the code I am about to test?\" — so a\n // dev server left open, or one whose tree changed under it after a\n // checkout, silently became the system under test. That fails in both\n // directions: a false red blamed on the code (beachfront 2026-08-12, where\n // it was investigated as a macOS-vs-Linux difference and written up as one\n // before being caught), and a false green where a passing suite ran against\n // an old build. CI already had it false, and that asymmetry is precisely\n // what made the failure read as a platform bug.\n //\n // The cost is a fresh vite boot per run (~10-20s against a ~2min suite).\n // Because the port above is allocated rather than fixed, your own dev\n // server on 5173 keeps running untouched.\n reuseExistingServer: false,\n timeout: 120_000,\n },\n});\n\nexport default playwrightA11yConfig;\n"],"mappings":";AAAA,SAAS,oBAAoB;AAC7B,SAAS,cAAc,eAA0C;AAI1D,IAAM,aAA0B;AAAA,EACrC,EAAE,MAAM,sBAAsB,MAAM,gBAAgB;AAAA,EACpD,EAAE,MAAM,mBAAmB,MAAM,kBAAkB;AACrD;AAQO,IAAM,cAA2B,CAAC,EAAE,MAAM,KAAK,MAAM,OAAO,CAAC;AAUpE,IAAM,YAAY,QAAQ,IAAI;AAmB9B,SAAS,uBAAsC;AAC7C,MAAI;AACF,UAAM,MAAM;AAAA,MACV,QAAQ;AAAA,MACR;AAAA,QACE;AAAA,QACA;AAAA,MAEF;AAAA,MACA,EAAE,UAAU,QAAQ,SAAS,KAAO,OAAO,CAAC,UAAU,QAAQ,QAAQ,EAAE;AAAA,IAC1E,EAAE,KAAK;AACP,WAAO,QAAQ,KAAK,GAAG,IAAI,MAAM;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAsBA,SAAS,cAAsB;AAC7B,MAAI,UAAW,QAAO;AACtB,QAAM,YAAY,qBAAqB,KAAK;AAC5C,UAAQ,IAAI,qBAAqB;AACjC,SAAO;AACT;AAEA,IAAM,OAAO,YAAY;AAKzB,IAAM,uBAA6C,aAAa;AAAA,EAC9D,SAAS;AAAA,EACT,WAAW;AAAA,EACX,eAAe;AAAA,EACf,YAAY,CAAC,CAAC,QAAQ,IAAI;AAAA,EAC1B,SAAS,QAAQ,IAAI,KAAK,IAAI;AAAA,EAC9B,UAAU,QAAQ,IAAI,KAAK,WAAW;AAAA,EACtC,KAAK;AAAA,IACH,SAAS,oBAAoB,IAAI;AAAA,IACjC,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAYP,gBAAgB,EAAE,eAAe,SAAkB;AAAA,EACrD;AAAA,EACA,UAAU;AAAA,IACR;AAAA,MACE,MAAM;AAAA,MACN,KAAK,EAAE,GAAG,QAAQ,gBAAgB,EAAE;AAAA,IACtC;AAAA,EACF;AAAA,EACA,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAoBT,SAAS,8BAA8B,IAAI;AAAA,IAC3C,KAAK,oBAAoB,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAe7B,qBAAqB;AAAA,IACrB,SAAS;AAAA,EACX;AACF,CAAC;AAED,IAAO,0BAAQ;","names":[]}
@@ -1,11 +0,0 @@
1
- import {
2
- openDb,
3
- readDbConfig
4
- } from "./chunk-22GKJO5F.js";
5
- import "./chunk-OCNSIBMP.js";
6
- import "./chunk-6VAL7XJT.js";
7
- export {
8
- openDb,
9
- readDbConfig
10
- };
11
- //# sourceMappingURL=client-M2V6FHH5.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cli/commands/db.ts"],"sourcesContent":["export type DbCommandOptions = {\n /** Override the libSQL url (tests use \":memory:\"); otherwise read from env. */\n url?: string;\n /** verify-dump: path to the dump file to load into a scratch engine. */\n file?: string;\n /** usage: org slug override; defaults to TURSO_ORG, else discovered. */\n org?: string;\n /** import-airtable / sync: run despite the freeze — a deliberate\n * rollback-window converge from the frozen Airtable shadow. */\n force?: boolean;\n cwd?: string;\n verbose?: boolean;\n};\n\n/** Injected seams — deliberately NOT part of DbCommandOptions, which is the set\n * of things a shell can type (a registration gate asserts exactly that). The\n * platform token lives here rather than on a flag because a secret passed on\n * argv is readable from `ps`. */\nexport type DbCommandDeps = {\n /** Tests pass \"\" to exercise the unconfigured path, so the implementation\n * must use `?? env` and never `|| env`. */\n platformToken?: string;\n fetchImpl?: (url: string, init?: RequestInit) => Promise<Response>;\n now?: Date;\n /** restore: auth token for the TARGET database; defaults to\n * TURSO_RESTORE_AUTH_TOKEN. Deliberately does NOT fall back to the ambient\n * TURSO_AUTH_TOKEN — that one belongs to production, and inheriting it would\n * undo the whole point of making --url explicit. */\n restoreAuthToken?: string;\n};\n\n/** #643 (the freeze): the scheduled import retired with the flip, but the\n * MANUAL import survives as the rollback-window converge tool — and run out of\n * habit it would overwrite authoritative Turso rows with the frozen Airtable\n * archive, including any post-flip write whose best-effort shadow was\n * swallowed. So the writing actions refuse under the freeze unless the\n * operator says `--force`. `parity` stays unguarded: it only compares, and\n * \"did the shadow drift?\" is exactly the rollback-window question.\n *\n * Pure and exported so the test injects BOTH switch states; `runDbCommand`\n * passes the shipped constant. Returns the refusal, or null to proceed. */\nexport function freezeGuardsDbWrite(\n action: string,\n force: boolean,\n authoritative: boolean,\n): { output: string; code: number } | null {\n if (!authoritative) return null;\n if (action !== \"import-airtable\" && action !== \"sync\") return null;\n if (force) return null;\n return {\n output:\n `db ${action} refused: TURSO_IS_AUTHORITATIVE is on (the freeze, 2026-08-31). ` +\n `An import now OVERWRITES authoritative Turso rows with the frozen Airtable ` +\n `archive. Pass --force only for a deliberate rollback-window converge.`,\n code: 1,\n };\n}\n\n/** `db <action>` — migrate | replay-deadletters | import-airtable | parity | sync | dump | verify-dump. The db layer is imported\n * dynamically so a non-db CLI invocation (and `--help`) never loads\n * @libsql/client. Config is resolved inside each branch so an unknown action\n * returns without needing any Turso env. */\nexport async function runDbCommand(\n action: string,\n opts: DbCommandOptions,\n deps: DbCommandDeps = {},\n): Promise<{ output: string; code: number }> {\n if (action === \"migrate\") {\n const { readDbConfig } = await import(\"../../db/client.js\");\n const cfg = opts.url ? { url: opts.url } : readDbConfig();\n const { runMigrations } = await import(\"../../db/migrate.js\");\n const { createClient } = await import(\"@libsql/client\");\n const client = createClient(cfg.url === \":memory:\" ? { url: \":memory:\" } : cfg);\n const ran = await runMigrations(client);\n return {\n output: ran.length ? `Applied migrations: ${ran.join(\", \")}` : \"Already up to date.\",\n code: 0,\n };\n }\n\n // Re-run every lead that dead-lettered during a site-lookup outage (#539\n // Phase 0) through the normal ingest pipeline. Exit 1 while any row is STILL\n // failing — the store hasn't recovered and the operator should not read the\n // run as \"all leads landed\". Zero rows is a clean 0: nothing owed.\n if (action === \"replay-deadletters\") {\n const { readDbConfig, openDb } = await import(\"../../db/client.js\");\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const { getWebsiteBySlug } = await import(\"../../reports/airtable/websites.js\");\n const base = openBase(readAirtableConfig());\n const {\n createSubmission,\n stampNotified,\n stampFanout,\n findRecentDuplicateSubmissions,\n listRecentSubmissionsForEmail,\n markSubmissionsSpamRetro,\n } = await import(\"../../db/submissions.js\");\n const { makeNotify } = await import(\"../../forms/notify.js\");\n const { classifySpam } = await import(\"../../forms/spam-classifier.js\");\n const { forwardNewsletterToWebhook } = await import(\"../../forms/webhook.js\");\n const { addMailchimpMember, mailchimpTagsFor } = await import(\"../../forms/mailchimp.js\");\n const { defaultResendClient } = await import(\"../../reports/send/resend.js\");\n const { replayDeadLetters } = await import(\"../../forms/replay.js\");\n\n // Same degradation as the ingest handler: an unconfigured Resend key means\n // replayed leads land un-emailed (notify=failed) rather than blocking replay.\n let send = null;\n try {\n send = defaultResendClient().send;\n } catch (err) {\n console.error(`[db] Resend unconfigured; replaying without email: ${String(err)}`);\n }\n\n // Mirrors the production handler's deps minus `deadLetter` (replayDeadLetters\n // forbids and strips it — a throwing lookup must retry, not duplicate) and\n // minus `defer` (a CLI has no post-response phase; the inline tail is fine).\n const result = await replayDeadLetters(db, {\n getWebsiteBySlug: (s) => getWebsiteBySlug(base, s),\n createSubmission: (input) => createSubmission(db, input),\n notify: makeNotify(send),\n stampNotified: (id, status, messageId) => stampNotified(db, id, status, messageId),\n now: () => new Date(),\n classifySpam: (n, outcome) =>\n classifySpam({\n name: n.name,\n email: n.email,\n ...(n.message !== undefined ? { message: n.message } : {}),\n formType: n.formType,\n extraFields: n.extraFields,\n turnstile: outcome,\n }),\n findRecentDuplicates: (message, since) =>\n findRecentDuplicateSubmissions(db, message, since.toISOString()),\n listRecentSubmissionsForEmail: (email, since) =>\n listRecentSubmissionsForEmail(db, email, since.toISOString()),\n retroBucket: (ids, reason) => markSubmissionsSpamRetro(db, ids, reason),\n forwardNewsletter: (url, submission, site) =>\n forwardNewsletterToWebhook(url, submission, site),\n addToMailchimp: (site, submission) =>\n addMailchimpMember({\n apiKey: site.mailchimpApiKey ?? \"\",\n audienceId: site.mailchimpAudienceId ?? \"\",\n email: submission.email,\n name: submission.name,\n tags: mailchimpTagsFor(submission.formType),\n }),\n stampFanout: (id, fanoutStatus) => stampFanout(db, id, fanoutStatus),\n });\n\n const lines = [\n ...result.replayed.map(\n (r) => `replayed ${r.id} → ${r.outcome}${r.submissionId ? ` (${r.submissionId})` : \"\"}`,\n ),\n ...result.stillFailing.map((r) => `still failing ${r.id}: ${r.error}`),\n `DEADLETTER_REPLAY replayed=${result.replayed.length} still_failing=${result.stillFailing.length}`,\n ];\n return { output: lines.join(\"\\n\"), code: result.stillFailing.length > 0 ? 1 : 0 };\n }\n\n // Phase 1.3/1.4 of #539. Both read the same two Airtable tables raw (id +\n // fields, no mapRow coercion — the importer's mapping is the authority) and\n // share that mapping, so parity is definitionally checked against what the\n // importer writes.\n if (action === \"import-airtable\" || action === \"parity\" || action === \"sync\") {\n const { TURSO_IS_AUTHORITATIVE } = await import(\"../../db/freeze.js\");\n const refused = freezeGuardsDbWrite(action, opts.force === true, TURSO_IS_AUTHORITATIVE);\n if (refused) return refused;\n const { readDbConfig, openDb } = await import(\"../../db/client.js\");\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const base = openBase(readAirtableConfig());\n const listRaw = async (table: string) =>\n (await base(table).select().all()).map((r) => ({\n id: r.id,\n fields: r.fields as Record<string, unknown>,\n }));\n const io = {\n listWebsiteRecords: () => listRaw(\"Websites\"),\n listReportRecords: () => listRaw(\"Reports\"),\n now: () => new Date(),\n };\n\n if (action === \"import-airtable\") {\n const { importFleetState, formatReapSummary } = await import(\"../../db/import-airtable.js\");\n const summary = await importFleetState(db, {\n ...io,\n // Attachment bodies ride expiring signed URLs; a failed fetch imports the\n // row with rendered_html null and is NAMED in the summary, never silent.\n fetchAttachment: async (url) => {\n try {\n const res = await fetch(url);\n return res.ok ? await res.text() : null;\n } catch {\n return null;\n }\n },\n });\n const lines = [\n `imported ${summary.sites} site(s) → sites/site_health/site_schedule`,\n `imported ${summary.reports} report(s)`,\n ];\n if (summary.renderedHtmlMisses.length > 0) {\n lines.push(\n `⚠ ${summary.renderedHtmlMisses.length} report(s) imported WITHOUT Rendered HTML ` +\n `(fetch failed / URL expired): ${summary.renderedHtmlMisses.join(\", \")}`,\n );\n }\n // The import deletes rows Airtable no longer has, so this one-shot path\n // reports the reap exactly as `db sync` does — same formatter, no second\n // copy to fall out of step.\n lines.push(...formatReapSummary(summary.reaped));\n return { output: lines.join(\"\\n\"), code: 0 };\n }\n\n // Phase 2 backbone (#539): one hourly pass = import (attachment fetches\n // only where the stored row lacks a body) + parity + one retry to absorb\n // the import-read/parity-read race. Exit 1 on persistent mismatch.\n if (action === \"sync\") {\n const { syncFleetState, formatSyncResult } = await import(\"../../db/sync.js\");\n const result = await syncFleetState(db, {\n ...io,\n fetchAttachment: async (url) => {\n try {\n const res = await fetch(url);\n return res.ok ? await res.text() : null;\n } catch {\n return null;\n }\n },\n });\n return {\n output: formatSyncResult(result),\n code: result.parity.mismatches.length > 0 ? 1 : 0,\n };\n }\n\n const { checkFleetParity, formatParityResult } = await import(\"../../db/parity.js\");\n const result = await checkFleetParity(db, io);\n return { output: formatParityResult(result), code: result.mismatches.length > 0 ? 1 : 0 };\n }\n\n // One-shot completion of design D5 (#539 Phase 2): copy every site's CURRENT\n // Airtable \"Header image\" attachment into sites.header_image*. Idempotent —\n // an already-populated BLOB is never overwritten (a re-run must not clobber\n // a freshly generated image with a stale Airtable copy). Exit 1 when any\n // fetch failed, so a partial backfill is never read as complete.\n if (action === \"backfill-header-images\") {\n const { readDbConfig, openDb } = await import(\"../../db/client.js\");\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const base = openBase(readAirtableConfig());\n const { backfillHeaderImages, formatBackfillResult } =\n await import(\"../../db/header-images.js\");\n const result = await backfillHeaderImages(db, {\n listWebsiteRecords: async () =>\n (await base(\"Websites\").select().all()).map((r) => ({\n id: r.id,\n fields: r.fields as Record<string, unknown>,\n })),\n fetchBytes: async (url) => {\n try {\n const res = await fetch(url);\n return res.ok ? new Uint8Array(await res.arrayBuffer()) : null;\n } catch {\n return null;\n }\n },\n });\n return { output: formatBackfillResult(result), code: result.failed.length > 0 ? 1 : 0 };\n }\n\n // #609: one-shot copy of the single Airtable \"Digest State\" row into Turso,\n // so the first digest run after the read repoint sees yesterday's snapshot\n // instead of an empty one. An empty read is not a crash — it badges EVERY\n // item NEW, which lands in the operator's inbox reading as \"the whole fleet\n // degraded overnight\". REFUSES to overwrite a snapshot Turso already holds:\n // a re-run must never replace a fresher snapshot with a stale Airtable copy.\n if (action === \"backfill-digest-state\") {\n const { readDbConfig, openDb } = await import(\"../../db/client.js\");\n const db = await openDb(opts.url ? { url: opts.url } : readDbConfig());\n const { readDigestState: readTurso, writeDigestState: writeTurso } =\n await import(\"../../db/digest-state.js\");\n const existing = await readTurso(db);\n if (Object.keys(existing).length > 0) {\n return {\n output: `DIGEST_BACKFILL skipped=1 reason=turso-already-populated keys=${Object.keys(existing).length}`,\n code: 0,\n };\n }\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const base = openBase(readAirtableConfig());\n const { readDigestState: readAirtable, DIGEST_STATE_TABLE } =\n await import(\"../../alerts/digest-state.js\");\n // `source` separates \"Airtable has no row\" from \"Airtable has a row holding\n // an empty snapshot\" — the reader collapses BOTH to {}, so copied=0 alone\n // cannot tell a quiet fleet from a failed read. Learned by running this: the\n // first real run printed copied=0 and only a hand probe showed the row was\n // there and genuinely empty.\n const rows = await base(DIGEST_STATE_TABLE).select({ maxRecords: 1, pageSize: 1 }).all();\n const snap = await readAirtable(base);\n const keys = Object.keys(snap).length;\n await writeTurso(db, snap);\n // Read it BACK. A returning write is not evidence the row landed — the same\n // rule forms-notify-target learned on 2026-08-03. `stored` counts the ROW,\n // not its keys, so an empty-but-present snapshot verifies as written.\n const stored = (await db.selectFrom(\"digest_state\").selectAll().execute()).length;\n const after = Object.keys(await readTurso(db)).length;\n return {\n output:\n `DIGEST_BACKFILL source=${rows.length > 0 ? \"row\" : \"absent\"} ` +\n `copied=${keys} verified=${after} rows=${stored}`,\n code: after === keys && stored === 1 ? 0 : 1,\n };\n }\n\n // Phase 1.5 of #539: platform-auth-free SQL dump to stdout-adjacent output.\n // The nightly backup workflow redirects this to a file, encrypts, uploads;\n // the rehearsed restore loads it into stock sqlite3 and compares row counts.\n if (action === \"dump\") {\n const { readDbConfig } = await import(\"../../db/client.js\");\n const cfg = opts.url ? { url: opts.url } : readDbConfig();\n const { createClient } = await import(\"@libsql/client\");\n const client = createClient(cfg.url === \":memory:\" ? { url: \":memory:\" } : cfg);\n const { dumpDatabase } = await import(\"../../db/dump.js\");\n const sql = await dumpDatabase(\n {\n execute: async (q) => {\n const r = await client.execute(q);\n return { columns: r.columns, rows: r.rows as Array<Record<string, unknown>> };\n },\n },\n new Date().toISOString(),\n );\n return { output: sql, code: 0 };\n }\n\n // Load a dump back into a REAL libSQL target (#612 review). The nightly\n // rehearsal loads into `:memory:`, which proves the SQL parses — it does not\n // prove you can get the data back into Turso, and that is the operation an\n // actual recovery needs. Replaying ~17 MB of SQL with megabytes of inline hex\n // over HTTP is materially different from an in-process load, and it had never\n // been done. Refuses to touch a database that already holds rows: a restore\n // is for an EMPTY target, and pointing this at production by mistake should\n // cost nothing.\n if (action === \"restore\") {\n const file = opts.file;\n if (!file) return { output: \"restore: pass the dump path via --file\", code: 1 };\n if (!opts.url) {\n return {\n output:\n \"restore: pass the TARGET database via --url (never defaults, to keep production out of reach)\",\n code: 1,\n };\n }\n // Classify the target BEFORE reading the dump, so a missing token names\n // itself instead of arriving as an opaque 401 (or, worse, as an ENOENT that\n // sends you hunting for the dump file). This command built its client from\n // a url alone until 2026-08-26, which worked against every target the tests\n // and rehearsals used — `:memory:` and a local `turso dev` — and failed\n // against every target an actual recovery has.\n const { parseDumpManifest, requiresAuthToken } = await import(\"../../db/dump.js\");\n const authToken = deps.restoreAuthToken ?? process.env.TURSO_RESTORE_AUTH_TOKEN ?? \"\";\n if (requiresAuthToken(opts.url) && !authToken) {\n return { output: \"RESTORE refused=auth-token-absent\", code: 1 };\n }\n const { readFile } = await import(\"node:fs/promises\");\n const sql = await readFile(file, \"utf-8\");\n const manifest = parseDumpManifest(sql);\n if (!manifest) return { output: \"RESTORE refused=manifest-absent\", code: 1 };\n const { createClient } = await import(\"@libsql/client\");\n const target = createClient(authToken ? { url: opts.url, authToken } : { url: opts.url });\n const existing = await target.execute(\n \"SELECT COUNT(*) AS n FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'\",\n );\n if (Number(existing.rows[0]?.n ?? 0) > 0) {\n return { output: \"RESTORE refused=target-not-empty\", code: 1 };\n }\n await target.executeMultiple(sql);\n const { tableCounts, headerImageBytes } = await import(\"../../db/dump.js\");\n const exec = {\n execute: async (q: string) => {\n const r = await target.execute(q);\n return { columns: r.columns, rows: r.rows as Array<Record<string, unknown>> };\n },\n };\n const counts = await tableCounts(exec);\n const bytes = await headerImageBytes(exec);\n // Same origin-anchored comparison as verify-dump: a restore that \"succeeded\"\n // with fewer rows than the origin held is not a restore.\n const bad: string[] = [];\n for (const [t, want] of Object.entries(manifest.tables)) {\n if ((counts[t] ?? 0) !== want) bad.push(`${t}: origin=${want} restored=${counts[t] ?? 0}`);\n }\n if (bytes !== manifest.blobBytes)\n bad.push(`header_image bytes: origin=${manifest.blobBytes} restored=${bytes}`);\n const rows = Object.values(counts).reduce((a, b) => a + b, 0);\n return {\n output: [\n ...bad.map((b) => `✗ ${b}`),\n `RESTORE loaded=true tables=${Object.keys(counts).length} rows=${rows} blob_bytes=${bytes} mismatches=${bad.length}`,\n ].join(\"\\n\"),\n code: bad.length > 0 ? 1 : 0,\n };\n }\n\n // How much of the plan's quota the fleet has burned this billing cycle\n // (#539 HIGH-10). The starter plan carries `overages: false`, so crossing a\n // quota BLOCKS reads and writes rather than billing for them — and once the\n // Airtable cutover lands, Turso is the only store there is. This is the one\n // alarm that fires before a wall rather than after it.\n //\n // Needs a PLATFORM token, which is a different credential from the\n // database-level TURSO_AUTH_TOKEN the rest of the fleet runs on: the database\n // token cannot read quota state at all.\n if (action === \"usage\") {\n const token = deps.platformToken ?? process.env.TURSO_FLEET_USAGE ?? \"\";\n // An unconfigured alarm must not read as a quiet, healthy one. The tell for\n // \"never ran\" has to be a failure, not a missing line (#585).\n if (!token) {\n return {\n output:\n \"FLEET_DB_USAGE verdict=no-token — set TURSO_FLEET_USAGE (turso auth api-tokens mint …). \" +\n \"This is the PLATFORM token, not the database TURSO_AUTH_TOKEN.\",\n code: 1,\n };\n }\n const { collectUsage, assessUsage } = await import(\"../../db/usage.js\");\n const input = await collectUsage({\n token,\n org: opts.org ?? process.env.TURSO_ORG,\n fetchImpl: deps.fetchImpl,\n now: deps.now ?? new Date(),\n });\n const r = assessUsage(input);\n const window = `${input.cycleStart.toISOString().slice(0, 10)} → ${input.cycleEnd\n .toISOString()\n .slice(0, 10)}`;\n return {\n output: [`Turso plan=${input.plan} billing cycle ${window}`, ...r.lines, \"\", r.marker].join(\n \"\\n\",\n ),\n code: r.code,\n };\n }\n\n // The restore rehearsal (Phase 1.5's hard gate), runnable every night: load\n // the dump into a FRESH in-memory engine and compare what came back against\n // the ORIGIN MANIFEST the dump carries.\n //\n // It used to compare against INSERT counts parsed out of the dump text — i.e.\n // the dump against itself. Both sides derived from one artifact, so a dump\n // that collected 5 of 44 sites shrank both numbers together and verified\n // clean. The manifest is read from the live database before any row is\n // serialised, which is the only measurement that can notice a short dump.\n //\n // A dump that cannot restore is not a backup — and per the repo's instrument\n // rule the check emits its machine line on every run, clean included.\n if (action === \"verify-dump\") {\n const file = opts.file;\n if (!file) return { output: \"verify-dump: pass the dump path via --file\", code: 1 };\n const { readFile } = await import(\"node:fs/promises\");\n const sql = await readFile(file, \"utf-8\");\n const { createClient } = await import(\"@libsql/client\");\n const scratch = createClient({ url: \":memory:\" });\n try {\n await scratch.executeMultiple(sql);\n } catch (err) {\n return { output: `DUMP_VERIFY loaded=false error=${String(err)}`, code: 1 };\n }\n const { tableCounts, headerImageBytes, parseDumpManifest } = await import(\"../../db/dump.js\");\n const manifest = parseDumpManifest(sql);\n if (!manifest) {\n // Refuse rather than fall back to self-comparison. Falling back would\n // re-enable exactly the blind spot the manifest exists to close, and it\n // would do so silently on the one artifact nobody was watching.\n return {\n output: \"DUMP_VERIFY loaded=true manifest=absent — dump predates the origin manifest\",\n code: 1,\n };\n }\n const exec = {\n execute: async (q: string) => {\n const r = await scratch.execute(q);\n return { columns: r.columns, rows: r.rows as Array<Record<string, unknown>> };\n },\n };\n const restored = await tableCounts(exec);\n const restoredBlobBytes = await headerImageBytes(exec);\n const mismatches: string[] = [];\n for (const [table, want] of Object.entries(manifest.tables)) {\n if ((restored[table] ?? 0) !== want) {\n mismatches.push(`${table}: origin=${want} restored=${restored[table] ?? 0}`);\n }\n }\n // A table the origin held and the dump never mentioned would otherwise be\n // invisible: absent from `restored` AND absent from the loop above.\n for (const table of Object.keys(restored)) {\n if (!(table in manifest.tables)) mismatches.push(`${table}: not in origin manifest`);\n }\n // Coverage: every table the APP owns must be present. `tables=N` used to be\n // printed and never asserted, so a table a migration failed to create — or\n // that the dump lost — rode green forever. `digest_state` and\n // `prospect_audits` were both absent from every artifact the night this was\n // found, purely because they postdated the last run.\n const { DATABASE_TABLES } = await import(\"../../db/schema.js\");\n for (const table of DATABASE_TABLES) {\n if (!(table in restored)) mismatches.push(`${table}: MISSING from the backup entirely`);\n }\n if (restoredBlobBytes !== manifest.blobBytes) {\n mismatches.push(\n `header_image bytes: origin=${manifest.blobBytes} restored=${restoredBlobBytes}`,\n );\n }\n const total = Object.values(restored).reduce((a, b) => a + b, 0);\n const lines = [\n ...mismatches.map((m) => `✗ ${m}`),\n `DUMP_VERIFY loaded=true tables=${Object.keys(restored).length} rows=${total} blob_bytes=${restoredBlobBytes} mismatches=${mismatches.length}`,\n ];\n return { output: lines.join(\"\\n\"), code: mismatches.length > 0 ? 1 : 0 };\n }\n\n return {\n output: `unknown db action '${action}'. Use: migrate, replay-deadletters, import-airtable, parity, sync, backfill-header-images, backfill-digest-state, dump, verify-dump, restore.`,\n code: 1,\n };\n}\n"],"mappings":"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","names":["result"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/reports/digest.ts"],"sourcesContent":["// src/reports/digest.ts\nimport { openBase, readAirtableConfig, type AirtableBase } from \"./airtable/client.js\";\nimport { listAllReports, isPendingApproval } from \"./airtable/reports.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport { listWebsites, siteSlug, type WebsiteRow } from \"./airtable/websites.js\";\nimport { defaultResendClient, type ResendClient } from \"./send/resend.js\";\nimport { isIdempotencyConflict } from \"./send/idempotency.js\";\nimport {\n collectVulnAlerts,\n collectDeliveryFailures,\n collectPreflightBlocked,\n collectLighthouseAlerts,\n collectRenovateAlerts,\n collectCiAlerts,\n collectAnalyticsFailures,\n collectTurnstileGuardrailAlerts,\n collectNotifyBounceAlerts,\n collectPrismicDriftAlerts,\n NOTIFY_BOUNCE_WINDOW_DAYS,\n} from \"../alerts/digest-collectors.js\";\nimport { diffAttention } from \"../alerts/digest-state.js\";\nimport {\n writeDigestState as writeAirtableDigestState,\n type DigestSnapshot,\n} from \"../alerts/digest-state.js\";\nimport { escapeHtml as esc } from \"../util/html.js\";\nimport { operatorEmail } from \"../util/operator.js\";\nimport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n SubmissionsDigestSection,\n} from \"../alerts/attention.js\";\n// Type-only: erased at compile time, so the kysely/libsql devDependency never loads\n// in a consuming fleet site (the runtime read is the dynamic import in\n// fetchSubmissionsDigestCounts, same rule as fetchNotifyBounceCounts).\nimport type { SiteSubmissionCounts } from \"../db/submissions.js\";\n\n// The attention/digest contract lives in `../alerts/attention.ts` (a dependency-free\n// types module) so the `alerts/*` collectors can depend on it without importing back\n// from this renderer/IO module — see attention.ts for the cycle it breaks. Re-exported\n// here so existing `from \"./digest.js\"` type importers keep resolving.\nexport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n SubmissionsDigestSection,\n} from \"../alerts/attention.js\";\n\nconst GREY = \"#757575\";\nconst RED = \"#C00\";\n\n/** Shared anchor style — Gmail does not inherit font-family into <a> tags. */\nconst ANCHOR_STYLE = `color:${RED};font-family:helvetica,sans-serif`;\n\nfunction readySection(items: ReadyItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Ready for your yes</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">Nothing waiting on you.</p>`;\n }\n const rows = items\n .map((it) => {\n const safeUrl = it.dashboardUrl.startsWith(\"https://\") ? it.dashboardUrl : undefined;\n const link = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">review &amp; approve</a>`\n : `review &amp; approve`;\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">\n <strong style=\"color:#222\">${esc(it.siteName)}</strong> — ${esc(it.reportType)} (${esc(it.period)})\n — ${link}\n </td>\n </tr>`;\n })\n .join(\"\");\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${rows}</table>`;\n}\n\nconst SEVERITY_ORDER: Record<AttentionSeverity, number> = { critical: 0, warning: 1 };\n\n/** Render the per-item status badge (\"NEW\"/\"WORSE\"); standing items get nothing. */\nfunction attentionBadge(status?: AttentionStatus): string {\n if (status === \"new\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">NEW</strong> `;\n if (status === \"worse\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">WORSE</strong> `;\n return \"\";\n}\n\nfunction attentionSection(items: AttentionItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Needs attention</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">All clear — nothing needs attention.</p>`;\n }\n\n // Group by siteName, preserving first-seen site order; sort within a site by\n // severity (critical first).\n const bySite = new Map<string, AttentionItem[]>();\n for (const it of items) {\n const bucket = bySite.get(it.siteName);\n if (bucket) bucket.push(it);\n else bySite.set(it.siteName, [it]);\n }\n\n const groups = [...bySite.entries()]\n .map(([siteName, siteItems]) => {\n const sorted = [...siteItems].sort(\n (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity],\n );\n const rows = sorted\n .map((it) => {\n const safeUrl = it.url?.startsWith(\"https://\") ? it.url : undefined;\n const titleHtml = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">${esc(it.title)}</a>`\n : esc(it.title);\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">${attentionBadge(it.status)}${titleHtml}</td>\n </tr>`;\n })\n .join(\"\");\n return `\n <tr>\n <td style=\"color:#222;font-family:helvetica,sans-serif;font-size:16px;font-weight:700;padding:8px 0 4px\">${esc(siteName)}</td>\n </tr>\n ${rows}`;\n })\n .join(\"\");\n\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${groups}</table>`;\n}\n\n/** The \"Submissions (24h)\" telemetry block: fleet totals + a per-site glance line.\n * Context, not an alarm — absent/null section or all-zero totals render nothing\n * (no-noise), and the section never triggers a send on its own (see runDigest). */\nfunction submissionsSection(s: SubmissionsDigestSection | null | undefined): string {\n if (!s) return \"\";\n if (s.leads === 0 && s.signups === 0 && s.spamAuto === 0) return \"\";\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Submissions (24h)</h2>`;\n // All three terms always render together — omitting a zero term next to a nonzero\n // one reads as \"not measured\" rather than \"none\"; simplest honest form.\n const totals = [\n `${s.leads} new lead${s.leads === 1 ? \"\" : \"s\"}`,\n `${s.signups} newsletter/RSVP signup${s.signups === 1 ? \"\" : \"s\"}`,\n `${s.spamAuto} auto-filtered spam`,\n ].join(\" · \");\n const totalLine = `<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">${totals}</p>`;\n if (s.bySite.length === 0) return `${heading}${totalLine}`;\n const rows = s.bySite\n .map((site) => {\n const parts: string[] = [];\n if (site.leads > 0) parts.push(`${site.leads} lead${site.leads === 1 ? \"\" : \"s\"}`);\n if (site.signups > 0) parts.push(`${site.signups} signup${site.signups === 1 ? \"\" : \"s\"}`);\n if (site.spamAuto > 0) parts.push(`${site.spamAuto} auto-filtered`);\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">\n <strong style=\"color:#222\">${esc(site.siteName)}</strong> — ${parts.join(\" · \")}\n </td>\n </tr>`;\n })\n .join(\"\");\n return `${heading}${totalLine}<table role=\"presentation\" style=\"border-collapse:collapse;margin:8px 0 0\">${rows}</table>`;\n}\n\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\n\n/** UTC \"YYYY-MM-DD\" — the Resend idempotency key suffix, so a same-day cron re-fire dedupes. */\nfunction digestDateKey(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * The gate for \"Ready for your yes\": Draft ready ∧ ¬Approved to send ∧ Sent at BLANK.\n *\n * Implemented as `listAllReports(base).filter(...)` (the authorized deviation from the plan's\n * draft, which pre-dated Slice 1's merged fix): `listAllReports` already calls `mapRow`, which\n * handles `Period` correctly, so no local `rawToReportRow` duplicate is needed. The JS filter\n * does real work — the test fake does NOT evaluate `filterByFormula` — so correctness is\n * test-provable here.\n *\n * Exported: the fleet homepage (Task 3.5b) reuses it for the pending-approval count.\n */\nexport async function listPendingApproval(base: AirtableBase): Promise<ReportRow[]> {\n return (await listAllReports(base)).filter(isPendingApproval);\n}\n\n// ── collectAttention (IO wrapper, sibling to runDigest) ──────────────────────\n\nexport type CollectAttentionDeps = {\n base: AirtableBase;\n /** Same baseUrl value runDigest threads; used for the /s/<slug> links. */\n baseUrl: string;\n /** Pre-fetched Websites rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listWebsites`. */\n websites?: WebsiteRow[];\n /** Pre-fetched Reports rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listAllReports`. */\n reports?: ReportRow[];\n /** Clock for the GitHub-signals staleness gate (collectCiAlerts /\n * collectRenovateAlerts skip a >3-day-stale sweep). Defaults to wall-clock;\n * runDigest threads its run-start `today` so a quiet repo's frozen CI/Renovate\n * signal stops badging once its sweep goes stale. */\n now?: Date;\n /** Pre-fetched per-site bounced-lead-notification counts (tests, or a caller with\n * an open db). When omitted, collectAttention reads them from libSQL itself —\n * defensively, so a missing TURSO env or a Turso blip drops just this signal. */\n notifyBounces?: ReadonlyMap<string, number>;\n};\n\n/** Per-site bounced-notification counts for the notify-bounce collector, read from\n * libSQL over the collector's own window. The db modules are imported DYNAMICALLY:\n * src/db/submissions.ts pulls kysely at top level, and kysely/libsql live in\n * devDependencies consuming fleet sites don't install — a static import here would\n * crash their CLI at require time (same rule as openDb's own lazy loads). Any\n * failure (no TURSO env, Turso down) logs and yields an empty map: the digest\n * must never blank over a missing optional signal. */\nasync function fetchNotifyBounceCounts(now: Date): Promise<ReadonlyMap<string, number>> {\n try {\n const [{ openDb, readDbConfig }, { countNotifyBouncedBySite }, { screenOutsSince }] =\n await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/submissions.js\"),\n import(\"../db/screenouts.js\"),\n ]);\n const db = await openDb(readDbConfig());\n return await countNotifyBouncedBySite(db, screenOutsSince(now, NOTIFY_BOUNCE_WINDOW_DAYS));\n } catch (e) {\n console.warn(`⚠ notify-bounce counts unavailable (libSQL): ${(e as Error).message}`);\n return new Map();\n }\n}\n\n/** The prior snapshot, from Turso (#609). Dynamically imported for the same\n * reason as fetchNotifyBounceCounts: kysely/libsql are devDependencies that\n * consuming fleet sites do not install, so a static import would crash their\n * CLI at require time.\n *\n * NOT defensive, deliberately — the Airtable read it replaces was not either.\n * Swallowing a failure here would silently badge every item NEW, which reads\n * as \"the whole fleet degraded overnight\" in the operator's inbox. A thrown\n * error is the honest outcome. */\nasync function readDigestStateFromDb(): Promise<DigestSnapshot> {\n const [{ openDb, readDbConfig }, { readDigestState }] = await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/digest-state.js\"),\n ]);\n return readDigestState(await openDb(readDbConfig()));\n}\n\n/** Bridge the injectable seam (returns void) onto the three-state contract the\n * log line reports. An injected writer that resolves is a real write. */\nasync function writeRollupOnce(\n options: DigestRunOptions,\n now: Date,\n): Promise<\"written\" | \"absent\"> {\n if (options.cockpitRollup) {\n await options.cockpitRollup.write(now);\n return \"written\";\n }\n return writeCockpitRollupToDb(now);\n}\n\n/** The write half of {@link readDigestStateFromDb}, same lazy-import rule. */\nasync function writeDigestStateToDb(next: DigestSnapshot): Promise<void> {\n const [{ openDb, readDbConfig }, { writeDigestState }] = await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/digest-state.js\"),\n ]);\n await writeDigestState(await openDb(readDbConfig()), next);\n}\n\n/**\n * Compute the cockpit's \"since a window\" roll-ups once, here, instead of on\n * every page load (MED-16 of the 2026-08-26 review).\n *\n * Both aggregates read the whole `submissions` table — the one unbounded-growth\n * table in the schema — and the cockpit root is the operator's most-loaded page.\n * Turso meters row scans, so recomputing them per request is a cost that grows\n * forever against numbers that move once a day.\n *\n * The trade is explicit: the figures become up to 24h old. That is why the\n * payload carries `computedAt` and the windows it used, and why an absent\n * roll-up reads as null rather than zero — a stale or unmeasured number\n * presented as live is worse than no number.\n *\n * Best-effort, like the snapshot write beside it: the digest has already been\n * sent by the time this runs, and a failed roll-up costs one strip on one page\n * until tomorrow, never the digest.\n */\nasync function writeCockpitRollupToDb(now: Date): Promise<\"written\" | \"absent\"> {\n // No Turso configured is a THIRD state, not a failure: it is what every unit\n // test looks like, and reporting it as `rollup=0` would train the eye to\n // ignore the number that is supposed to catch a dead writer. Same three-state\n // vocabulary the site mirrors already use (`mirrored=1|0|absent`).\n if (!process.env.TURSO_DATABASE_URL) return \"absent\";\n const [{ openDb, readDbConfig }, { writeCockpitRollup }, screenouts, submissions, collectors] =\n await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/digest-state.js\"),\n import(\"../db/screenouts.js\"),\n import(\"../db/submissions.js\"),\n import(\"../alerts/digest-collectors.js\"),\n ]);\n const db = await openDb(readDbConfig());\n const SCREEN_OUT_WINDOW_DAYS = 30;\n const screenOutSince = screenouts.screenOutsSince(now, SCREEN_OUT_WINDOW_DAYS);\n const bounceSince = screenouts.screenOutsSince(now, collectors.NOTIFY_BOUNCE_WINDOW_DAYS);\n\n const totals = { honeypot: 0, tooFast: 0, markedSpam: 0 };\n for (const t of (await screenouts.listScreenOutsSince(db, screenOutSince)).values()) {\n totals.honeypot += t.honeypot;\n totals.tooFast += t.tooFast;\n totals.markedSpam += t.markedSpam;\n }\n const bounces = await submissions.countNotifyBouncedBySite(db, bounceSince);\n\n await writeCockpitRollup(db, {\n spamTotals: totals,\n notifyBounces: Object.fromEntries(bounces),\n windowDays: {\n screenOuts: SCREEN_OUT_WINDOW_DAYS,\n bounces: collectors.NOTIFY_BOUNCE_WINDOW_DAYS,\n },\n computedAt: now.toISOString(),\n });\n return \"written\";\n}\n\n/** Persist the next snapshot to BOTH stores (#609 dual-write).\n *\n * Turso is the read side, so a failure there is the one that actually costs\n * correct NEW badges tomorrow — but Airtable keeps being written so the move\n * stays reversible while Phase 5 is in flight. Both are best-effort: the\n * digest has already been sent (or deliberately skipped) by the time this\n * runs, and a snapshot write failure re-news at worst.\n *\n * The DIGEST_STATE_WRITE line exists because of #585: a dual-write that\n * silently stopped running looked identical to a healthy one for weeks. One\n * line, always emitted, naming both halves. */\nasync function persistDigestState(\n base: AirtableBase,\n next: DigestSnapshot,\n writeState?: (snap: DigestSnapshot) => Promise<void>,\n writeRollup?: () => Promise<\"written\" | \"absent\">,\n): Promise<void> {\n let turso = 0;\n let airtable = 0;\n let rollup: \"1\" | \"0\" | \"absent\" = \"0\";\n try {\n await (writeState ?? writeDigestStateToDb)(next);\n turso = 1;\n } catch (e) {\n console.warn(`⚠ digest state write failed (turso): ${(e as Error).message}`);\n }\n try {\n await writeAirtableDigestState(base, next);\n airtable = 1;\n } catch (e) {\n console.warn(`⚠ digest state write failed (airtable): ${(e as Error).message}`);\n }\n // The cockpit roll-up rides the same \"already sent, best-effort\" contract, and\n // gets its own counter on the line for the #585 reason: a dual-write that\n // silently stopped running looked identical to a healthy one for weeks. An\n // ABSENT `rollup=` is a different failure from `rollup=0`.\n if (writeRollup) {\n try {\n rollup = (await writeRollup()) === \"absent\" ? \"absent\" : \"1\";\n } catch (e) {\n rollup = \"0\";\n console.warn(`⚠ cockpit rollup write failed: ${(e as Error).message}`);\n }\n }\n console.log(`DIGEST_STATE_WRITE turso=${turso} airtable=${airtable} rollup=${rollup}`);\n}\n\n/** The \"Submissions (24h)\" telemetry window — a precise 24h ISO-timestamp compare\n * (unlike screenOutsSince's date-only strings). */\nconst SUBMISSIONS_DIGEST_WINDOW_MS = 24 * 60 * 60 * 1000;\n\n/** Per-site submission counts for the digest telemetry section, read from libSQL.\n * Dynamic imports + fail-soft for the same reason as fetchNotifyBounceCounts:\n * kysely/libsql are devDependencies consuming fleet sites don't install, and a\n * Turso blip must drop just this section, never the digest. Returns null (not an\n * empty map) on failure so the renderer can omit the section rather than claim a\n * quiet fleet. */\nasync function fetchSubmissionsDigestCounts(\n now: Date,\n): Promise<ReadonlyMap<string, SiteSubmissionCounts> | null> {\n try {\n const [{ openDb, readDbConfig }, { countSubmissionsSinceBySite }] = await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/submissions.js\"),\n ]);\n const db = await openDb(readDbConfig());\n return await countSubmissionsSinceBySite(\n db,\n new Date(now.getTime() - SUBMISSIONS_DIGEST_WINDOW_MS).toISOString(),\n );\n } catch (e) {\n console.warn(`⚠ submissions telemetry unavailable (libSQL): ${(e as Error).message}`);\n return null;\n }\n}\n\n/** Assemble the \"Submissions (24h)\" section from the per-site counts. PURE (exported\n * for direct tests). Totals sum ALL map entries — an orphan site id still counts in\n * the fleet totals. bySite lists only ids that resolve in `sitesById` AND carry any\n * nonzero count, sorted by total volume desc then siteName A-Z. null counts (libSQL\n * unavailable) → null (section omitted). */\nexport function buildSubmissionsDigestSection(\n counts: ReadonlyMap<string, SiteSubmissionCounts> | null,\n sitesById: ReadonlyMap<string, WebsiteRow>,\n): SubmissionsDigestSection | null {\n if (counts === null) return null;\n let leads = 0;\n let signups = 0;\n let spamAuto = 0;\n const bySite: SubmissionsDigestSection[\"bySite\"] = [];\n for (const [siteId, c] of counts) {\n leads += c.leads;\n signups += c.signups;\n spamAuto += c.spamAuto;\n const site = sitesById.get(siteId);\n if (site && c.leads + c.signups + c.spamAuto > 0) {\n bySite.push({ siteName: site.name, ...c });\n }\n }\n bySite.sort(\n (a, b) =>\n b.leads + b.signups + b.spamAuto - (a.leads + a.signups + a.spamAuto) ||\n a.siteName.localeCompare(b.siteName),\n );\n return { leads, signups, spamAuto, bySite };\n}\n\n/** Run a single collector under a try/catch: a thrown collector logs and yields []\n * so one broken signal never blanks the whole \"Needs attention\" section. */\nfunction runCollector(label: string, fn: () => AttentionItem[]): AttentionItem[] {\n try {\n return fn();\n } catch (e) {\n console.warn(`⚠ attention collector \"${label}\" failed: ${(e as Error).message}`);\n return [];\n }\n}\n\n/**\n * Fetch the free signals once (listAllReports + listWebsites) — or reuse the\n * `reports`/`websites` arrays runDigest already read, so a single run reads each\n * table once — build the sitesById map the delivery collector needs, and run each\n * pure collector isolated. Returns the union of items; diffing/badging happens in\n * runDigest.\n *\n * The Renovate + CI signals come from the SAME persisted collectors the operator\n * cockpit (`buildCockpitModel`) runs — `collectRenovateAlerts` (key\n * `renovate:<siteId>`) and `collectCiAlerts` (key `ci:<siteId>`), reading the\n * nightly-persisted `renovateFailingCis`/`defaultBranchCi` fields. This is the\n * key-space unification: because the digest writes the shared Digest State\n * snapshot with these same keys, the cockpit's NEW/WORSE diff finds them and the\n * two surfaces agree (the prior live per-PR `renovate:<repo>#<n>` sweep never\n * matched the cockpit's keys, so its cards badged NEW forever).\n */\nexport async function collectAttention(deps: CollectAttentionDeps): Promise<AttentionItem[]> {\n const reports = deps.reports ?? (await listAllReports(deps.base));\n const websites = deps.websites ?? (await listWebsites(deps.base));\n const now = deps.now ?? new Date();\n const notifyBounces = deps.notifyBounces ?? (await fetchNotifyBounceCounts(now));\n const sitesById = new Map<string, WebsiteRow>(websites.map((w) => [w.id, w]));\n return [\n ...runCollector(\"vuln\", () => collectVulnAlerts(websites, deps.baseUrl)),\n ...runCollector(\"delivery\", () => collectDeliveryFailures(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"preflight\", () => collectPreflightBlocked(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"lighthouse\", () => collectLighthouseAlerts(websites, deps.baseUrl)),\n ...runCollector(\"renovate\", () => collectRenovateAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"ci\", () => collectCiAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"analytics\", () => collectAnalyticsFailures(websites, deps.baseUrl, now)),\n ...runCollector(\"turnstile\", () =>\n collectTurnstileGuardrailAlerts(websites, deps.baseUrl, now),\n ),\n ...runCollector(\"notify-bounce\", () =>\n collectNotifyBounceAlerts(websites, notifyBounces, deps.baseUrl),\n ),\n // Same collector the cockpit runs, over the same persisted `Prismic Models`\n // columns and the same keys — so the snapshot this digest writes is the one\n // the cockpit diffs NEW/WORSE against, and the two surfaces agree.\n ...runCollector(\"prismic-drift\", () => collectPrismicDriftAlerts(websites, deps.baseUrl, now)),\n ];\n}\n\nexport type DigestRunOptions = {\n resend?: ResendClient;\n /** Dashboard origin for the /s/<slug> links, e.g. \"https://reddoor-maintenance.netlify.app\". */\n baseUrl: string;\n /**\n * Inject a pre-opened Airtable base (tests, server handlers).\n * When omitted, `openBase(readAirtableConfig())` is called from the environment.\n */\n base?: AirtableBase;\n /** Per-site submission counts for the \"Submissions (24h)\" section. undefined =\n * fetch from libSQL; null = simulate unavailable (section omitted); a Map =\n * injected (tests). */\n submissionCounts?: ReadonlyMap<string, SiteSubmissionCounts> | null;\n /**\n * The prior-run snapshot store (#609). Production omits it and gets Turso.\n *\n * Injectable because the read is deliberately NOT defensive: swallowing a\n * failure would badge every item NEW, which lands in the operator's inbox\n * reading as \"the whole fleet degraded overnight\". That makes Turso a hard\n * requirement of a real run — so tests, which have no libSQL, need a seam\n * rather than an env var.\n */\n digestState?: {\n read: () => Promise<DigestSnapshot>;\n write: (snap: DigestSnapshot) => Promise<void>;\n };\n /**\n * The cockpit roll-up writer (MED-16). Same seam and same reason as\n * `digestState`: production omits it and gets Turso, tests inject.\n *\n * Without a seam this would be untestable in exactly the way that matters —\n * `writeCockpitRollupToDb` opens a db from env, so in a test it throws and is\n * caught, and the run reports `rollup=0` forever while looking healthy. That\n * is the #585 shape the counter on the log line exists to expose.\n */\n cockpitRollup?: { write: (now: Date) => Promise<void> };\n /**\n * The run clock. Production omits it and gets `new Date()`.\n *\n * Injectable because several collectors this run feeds are AGE-SENSITIVE —\n * `collectPrismicDriftAlerts` alone changes an item's key and its wording once\n * a verdict crosses `PRISMIC_DRIFT_STALE_DAYS` — so a digest test that cannot\n * fix the clock is asserting on the day it happens to run. Two did, and both\n * went red four days after they were written, reporting the staleness wording\n * as if the drift wiring had broken. `collectAttention` already takes a `now`\n * for exactly this reason; this threads the same lever one layer out.\n */\n now?: Date;\n};\n\nexport async function runDigest(\n options: DigestRunOptions,\n): Promise<{ output: string; code: number }> {\n // Capture clock BEFORE any await so the idempotency key can't roll past midnight mid-run.\n const today = options.now ?? new Date();\n try {\n const base = options.base ?? openBase(readAirtableConfig());\n // Read each table ONCE for the whole run, then thread the arrays into\n // collectAttention so it doesn't re-fetch (was: listWebsites ×2, listAllReports\n // ×2). Pending is derived in-line with listPendingApproval's exact predicate.\n const reports = await listAllReports(base);\n const websites = await listWebsites(base);\n const sites = new Map(websites.map((w) => [w.id, w]));\n\n const pending = reports.filter(isPendingApproval);\n\n const readyForYourYes: ReadyItem[] = [];\n const baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n for (const r of pending) {\n const site = sites.get(r.siteId);\n if (!site) continue; // orphan report → skip rather than render a broken link\n // An empty Name slugs to \"\" → `/s/` is a dead link (getWebsiteBySlug can't\n // match it). Fall back to the fleet homepage so the operator still lands\n // somewhere usable instead of a 404.\n const slug = siteSlug(site.name);\n readyForYourYes.push({\n siteName: site.name,\n reportType: r.reportType,\n period: r.period ?? \"—\",\n dashboardUrl: slug ? `${baseUrl}/s/${slug}` : baseUrl,\n });\n }\n\n // M5: collect the free signals (isolated), diff against yesterday's snapshot.\n // Renovate + CI come from the persisted collectors (the same ones the cockpit\n // runs), so the snapshot this digest writes carries the `renovate:<siteId>` /\n // `ci:<siteId>` keys the cockpit diffs against — no live GitHub sweep here.\n const collected = await collectAttention({\n base,\n baseUrl: options.baseUrl,\n websites,\n reports,\n now: today,\n });\n // #609: Turso is the read side now. Airtable keeps being WRITTEN below\n // (dual-write, Phase 5's pattern everywhere else) so the move stays\n // reversible, but nothing reads it any more.\n const prior = await (options.digestState?.read ?? readDigestStateFromDb)();\n const { tagged, next } = diffAttention(collected, prior, digestDateKey(today));\n // The operator only HEARS about a vuln once Renovate's auto-fix is exhausted\n // (tried and failed a couple of nightly cycles) — before that the fleet is\n // still self-patching and the cockpit's amber Watch band is the only surface.\n // Filter the EMAIL list only: `next` (written below) must keep every vuln key\n // so the cockpit's diff against this same snapshot stays consistent, and so\n // the exhausted-flip diffs as WORSE instead of arriving pre-badged-away.\n const needsAttention = tagged.filter(\n (it) => it.kind !== \"vuln\" || it.autoFixExhausted === true,\n );\n\n // No-noise default: skip entirely when there's nothing to report.\n if (readyForYourYes.length === 0 && needsAttention.length === 0) {\n // Persist `next` even on a skip: a key that resolved on a quiet day clears so a\n // later recurrence diffs as NEW (spec §10), and — since the skip can now fire\n // with muted pre-exhaustion vulns collected — their keys keep snapshotting so\n // the eventual exhausted-flip diffs as WORSE. Wrapped: a write failure can't\n // fail the skip.\n await persistDigestState(base, next, options.digestState?.write, () =>\n writeRollupOnce(options, today),\n );\n return { output: \"Digest skipped (nothing ready, nothing needs attention).\", code: 0 };\n }\n\n // Submissions telemetry rides only on a digest that is ALREADY sending — the\n // skip predicate above is deliberately unchanged (leads already fire their own\n // ingest-time notification; this is context, not an alarm), and fetching AFTER\n // the skip check keeps quiet days from touching libSQL at all.\n const counts =\n options.submissionCounts !== undefined\n ? options.submissionCounts\n : await fetchSubmissionsDigestCounts(today);\n const submissions = buildSubmissionsDigestSection(counts, sites);\n\n const html = renderDigestHtml({ readyForYourYes, needsAttention, submissions });\n const client = options.resend ?? defaultResendClient();\n const to = [operatorEmail()];\n const n = readyForYourYes.length;\n const reportWord = n === 1 ? \"report\" : \"reports\";\n let result: Awaited<ReturnType<typeof client.send>>;\n try {\n result = await client.send({\n from: FROM_ADDRESS,\n to,\n subject: `Your fleet — ${digestDateKey(today)}: ${n} ${reportWord} ready for your yes`,\n html,\n idempotencyKey: `digest-${digestDateKey(today)}`,\n });\n } catch (err) {\n // A same-UTC-day re-run whose content changed re-sends with the same\n // `digest-<date>` idempotency key but a DIFFERENT body. Resend rejects that\n // with a 409 (`invalid_idempotent_request`) — \"This idempotency key has been\n // used ... but the request body was modified ...\". The operator already got\n // today's digest on the first send, so re-sending a changed version same-day\n // would just be a duplicate: treat it as an \"already sent today\" no-op.\n //\n // ResendClient (send/resend.ts) wraps the API error as a plain Error and only\n // preserves the *message* string (no name/statusCode), so the message\n // substring is the only reliable discriminator — match defensively on it.\n // Any OTHER send error re-throws to the outer catch → {code:1}, so a genuine\n // Resend/network failure still fails loudly.\n if (isIdempotencyConflict(err)) {\n // Do NOT write the snapshot: the first send already persisted it; writing\n // this run's `next` would diff against the first run's snapshot and mis-badge.\n return {\n output:\n \"Digest already sent today (content changed since the first send) — skipped to avoid a duplicate.\",\n code: 0,\n };\n }\n throw err;\n }\n // Persist the next snapshot AFTER a successful send. A write failure is caught +\n // logged: the digest already went out, tomorrow re-news at worst. (The send-FAILURE\n // path never reaches here — the outer catch returns code 1 with no write, preserving\n // the NEW badge for the retry.)\n await persistDigestState(base, next, options.digestState?.write, () =>\n writeRollupOnce(options, today),\n );\n return { output: `Digest sent to ${to.join(\", \")} (${result.messageId})`, code: 0 };\n } catch (err) {\n // Re-throw config errors (exitCode=2: missing env vars, bad config) so runOrExit\n // surfaces them with the correct process exit code rather than collapsing to 1.\n if (typeof (err as { exitCode?: unknown }).exitCode === \"number\") {\n throw err;\n }\n const message = err instanceof Error ? err.message : String(err);\n return { output: `digest failed: ${message}`, code: 1 };\n }\n}\n\n/** Pure render of the unified daily operator digest. No IO — the caller (runDigest)\n * collects the rows and decides whether to send. */\nexport function renderDigestHtml(sections: DigestSections): string {\n return `<!doctype html>\n<html>\n <head><meta charset=\"utf-8\"></head>\n <body style=\"margin:0;padding:0;background:#ffffff\">\n <table width=\"100%\" style=\"border-collapse:collapse\">\n <tr>\n <td align=\"center\" style=\"padding:24px\">\n <table width=\"600\" style=\"border-collapse:collapse\">\n <tr>\n <td>\n <h1 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:24px;font-weight:700;margin:0 0 8px\">Your fleet today</h1>\n ${readySection(sections.readyForYourYes)}\n ${attentionSection(sections.needsAttention)}\n ${submissionsSection(sections.submissions)}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>`;\n}\n"],"mappings":"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","names":["writeDigestState"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/db/digest-state.ts"],"sourcesContent":["/** #609 (#539 Phase 5): the digest's prior-run snapshot, on Turso.\n *\n * Unlike every other Phase 5 slice this is a MIGRATION, not a mirror — there\n * was no Turso table to dual-write into, so these REPLACE the Airtable pair in\n * `src/alerts/digest-state.ts` rather than shadowing them. Nothing writes the\n * Airtable \"Digest State\" table after this, so parity does not cover the new\n * one: it has no counterpart to compare against.\n *\n * The pure half — `diffAttention`, and the `DigestSnapshot` type it is written\n * against — stays exactly where it is. Only the IO moved.\n *\n * Stored as ONE row holding the whole snapshot as JSON. Both readers\n * (`runDigest`'s diff and the fleet homepage's NEW badges) need the entire map,\n * so a keyed table would buy nothing on reads and its \"give me everything\"\n * query would be a raw scan needing a justified entry in the EXPLAIN gate's\n * allowlist. One row by primary key needs neither.\n *\n * Worth stating plainly: the homepage's read of this used to be an AIRTABLE\n * call on a request path — a Phase 2 leftover, since digest state was never in\n * that phase's scope. Moving it removes a live dependency, which is the same\n * failure class as the 2026-08-17 quota outage.\n */\nimport type { Db } from \"./client.js\";\nimport type { DigestSnapshot } from \"../alerts/digest-state.js\";\n\n/** The singleton row's key. A constant rather than a magic string at each call\n * site, so the \"there is exactly one row\" invariant is visible in one place. */\nexport const DIGEST_STATE_ID = \"default\";\n\n/**\n * The prior snapshot, or `{}` when there is none.\n *\n * A missing row and a malformed blob BOTH read as `{}` — the Airtable reader's\n * contract, kept verbatim. The digest runs unattended on a cron, so a bad blob\n * must cost accurate NEW badges for one run, never the whole send.\n */\nexport async function readDigestState(db: Db): Promise<DigestSnapshot> {\n const row = await db\n .selectFrom(\"digest_state\")\n .select(\"snapshot\")\n .where(\"id\", \"=\", DIGEST_STATE_ID)\n .executeTakeFirst();\n if (!row) return {};\n try {\n return JSON.parse(row.snapshot) as DigestSnapshot;\n } catch {\n return {};\n }\n}\n\n/**\n * Persist the next snapshot, replacing whatever was there.\n *\n * An upsert on the constant id rather than a get-then-create: the Airtable\n * version needed two round-trips to decide, and here the singleton invariant is\n * enforced by the primary key instead of by the code that happens to run.\n */\nexport async function writeDigestState(\n db: Db,\n snap: DigestSnapshot,\n updatedAt: string = new Date().toISOString(),\n): Promise<void> {\n const row = {\n id: DIGEST_STATE_ID,\n snapshot: JSON.stringify(snap),\n updated_at: updatedAt,\n };\n await db\n .insertInto(\"digest_state\")\n .values(row)\n .onConflict((oc) => oc.column(\"id\").doUpdateSet(row))\n .execute();\n}\n\n/**\n * A SECOND row in the same table, holding the cockpit's \"since a window\"\n * roll-ups. Keyed separately rather than folded into the snapshot: the snapshot\n * is the digest's own diff state and has one writer and one meaning; conflating\n * two unrelated payloads in one blob makes a partial write of either look like a\n * corruption of both.\n */\nexport const COCKPIT_ROLLUP_ID = \"cockpit_rollup\";\n\n/**\n * Fleet-wide counts the cockpit renders, computed once by the nightly digest\n * instead of aggregated on every page load.\n *\n * `computedAt` is not decoration. These figures are up to 24h old by\n * construction, and a stale number rendered as though it were live is worse than\n * no number — so the cockpit shows when it was taken.\n */\nexport type CockpitRollup = {\n spamTotals: { honeypot: number; tooFast: number; markedSpam: number };\n /** site id → notify=bounced count inside the bounce window. */\n notifyBounces: Record<string, number>;\n /** The windows these were computed over, so a changed constant is visible\n * rather than silently re-labelling old numbers. */\n windowDays: { screenOuts: number; bounces: number };\n computedAt: string;\n};\n\n/**\n * The stored roll-up, or **null** when there is none.\n *\n * Null rather than zeros, deliberately. Every one of these numbers has a\n * legitimate zero, so a reader given `{honeypot: 0, …}` cannot tell \"nothing was\n * screened out\" from \"the digest has never run\". That is the same distinction\n * `FLEET_SMOKE_UNMEASURED` exists to preserve, and the cockpit renders the strip\n * as ABSENT for null rather than drawing a row of zeros.\n *\n * A malformed blob is also null: same reasoning, and it must never throw on a\n * request path.\n */\nexport async function readCockpitRollup(db: Db): Promise<CockpitRollup | null> {\n const row = await db\n .selectFrom(\"digest_state\")\n .select(\"snapshot\")\n .where(\"id\", \"=\", COCKPIT_ROLLUP_ID)\n .executeTakeFirst();\n if (!row) return null;\n try {\n const parsed = JSON.parse(row.snapshot) as Partial<CockpitRollup>;\n // Shape-check rather than trust: this blob is written by a different process\n // on a different schedule, so an older writer's payload can outlive a type\n // change here. Anything unrecognized reads as \"not measured\".\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n typeof parsed.computedAt !== \"string\" ||\n typeof parsed.spamTotals !== \"object\" ||\n parsed.spamTotals === null ||\n typeof parsed.notifyBounces !== \"object\" ||\n parsed.notifyBounces === null\n ) {\n return null;\n }\n return parsed as CockpitRollup;\n } catch {\n return null;\n }\n}\n\n/** Upsert the roll-up, same singleton-by-PK shape as the snapshot. */\nexport async function writeCockpitRollup(db: Db, rollup: CockpitRollup): Promise<void> {\n const row = {\n id: COCKPIT_ROLLUP_ID,\n snapshot: JSON.stringify(rollup),\n updated_at: rollup.computedAt,\n };\n await db\n .insertInto(\"digest_state\")\n .values(row)\n .onConflict((oc) => oc.column(\"id\").doUpdateSet(row))\n .execute();\n}\n"],"mappings":";AA2BO,IAAM,kBAAkB;AAS/B,eAAsB,gBAAgB,IAAiC;AACrE,QAAM,MAAM,MAAM,GACf,WAAW,cAAc,EACzB,OAAO,UAAU,EACjB,MAAM,MAAM,KAAK,eAAe,EAChC,iBAAiB;AACpB,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,MAAI;AACF,WAAO,KAAK,MAAM,IAAI,QAAQ;AAAA,EAChC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AASA,eAAsB,iBACpB,IACA,MACA,aAAoB,oBAAI,KAAK,GAAE,YAAY,GAC5B;AACf,QAAM,MAAM;AAAA,IACV,IAAI;AAAA,IACJ,UAAU,KAAK,UAAU,IAAI;AAAA,IAC7B,YAAY;AAAA,EACd;AACA,QAAM,GACH,WAAW,cAAc,EACzB,OAAO,GAAG,EACV,WAAW,CAAC,OAAO,GAAG,OAAO,IAAI,EAAE,YAAY,GAAG,CAAC,EACnD,QAAQ;AACb;AASO,IAAM,oBAAoB;AAgCjC,eAAsB,kBAAkB,IAAuC;AAC7E,QAAM,MAAM,MAAM,GACf,WAAW,cAAc,EACzB,OAAO,UAAU,EACjB,MAAM,MAAM,KAAK,iBAAiB,EAClC,iBAAiB;AACpB,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,IAAI,QAAQ;AAItC,QACE,OAAO,WAAW,YAClB,WAAW,QACX,OAAO,OAAO,eAAe,YAC7B,OAAO,OAAO,eAAe,YAC7B,OAAO,eAAe,QACtB,OAAO,OAAO,kBAAkB,YAChC,OAAO,kBAAkB,MACzB;AACA,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,mBAAmB,IAAQ,QAAsC;AACrF,QAAM,MAAM;AAAA,IACV,IAAI;AAAA,IACJ,UAAU,KAAK,UAAU,MAAM;AAAA,IAC/B,YAAY,OAAO;AAAA,EACrB;AACA,QAAM,GACH,WAAW,cAAc,EACzB,OAAO,GAAG,EACV,WAAW,CAAC,OAAO,GAAG,OAAO,IAAI,EAAE,YAAY,GAAG,CAAC,EACnD,QAAQ;AACb;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/reports/airtable/ensure-site.ts","../src/cli/commands/ensure-site.ts"],"sourcesContent":["import type { FieldSet, Records } from \"airtable\";\nimport type { AirtableBase } from \"./client.js\";\nimport { WEBSITES_TABLE, listWebsites, siteSlug } from \"./websites.js\";\nimport { toAirtableStatus } from \"./site-status.js\";\n\nexport type EnsureSiteInput = {\n /** Canonical slug — matches by siteSlug(Name); the Name on create when no displayName given. */\n slug: string;\n /** Human display name written to Name on create. Name is consumed VERBATIM in\n * client-facing copy (forms auto-reply intro, report subjects), so a bare\n * machine slug there reads as \"Thanks for reaching out to acme-co.\" Must\n * slugify to the same slug or the row wouldn't be found on re-run. */\n displayName?: string;\n url?: string;\n pointOfContact?: string;\n /** owner/repo. Default on create: `reddoorla/<slug>`. */\n gitRepo?: string;\n};\n\n/** #539 Phase 5. Structurally typed rather than importing the db layer's\n * `SiteMirror`, so this module stays the leaf it has always been — a real\n * `SiteMirror` satisfies it. Two ops because `ensureSite` has two write paths,\n * and the create one is the reason this exists: every other site mirror is an\n * UPDATE, which does nothing for a row that does not exist yet. */\nexport type EnsureSiteMirror = {\n created: (rec: { id: string; fields: Record<string, unknown> }) => Promise<void>;\n site: (siteId: string, fields: Record<string, unknown>) => Promise<void>;\n};\n\nexport type EnsureSiteResult = {\n status: \"created\" | \"exists\";\n siteId: string;\n /** Airtable column names written on the exists path (create path writes all). */\n updatedFields: string[];\n /** Inputs that DIFFERED from an existing non-blank cell and were left\n * untouched (fill-blanks-only) — surfaced so a resumed bootstrap with a\n * corrected value doesn't silently discard it. */\n skippedMismatches: string[];\n};\n\n/** Column-name map (load-bearing magic strings — see websites.ts's mapRow header). */\nconst COLS = {\n url: \"url\",\n pointOfContact: \"point of contact\",\n gitRepo: \"Git repo\",\n} as const;\n\n/**\n * Find-or-create the Websites row for a slug so a new site exists in the fleet\n * inventory (audits, form-ingest slug resolution, reports) from day one.\n *\n * Fill-blanks-only on the exists path: this command runs from a bootstrap skill\n * that may be re-run to resume — it must never clobber operator-edited cells.\n * Frequencies are deliberately NOT set (launch flips the lifecycle); Status is\n * only written on create (\"building\" — Airtable's \"in development\" until the\n * stage-2 vocabulary switch flips).\n */\nexport async function ensureSite(\n base: AirtableBase,\n input: EnsureSiteInput,\n mirror?: EnsureSiteMirror,\n): Promise<EnsureSiteResult> {\n const slug = siteSlug(input.slug);\n if (!slug) throw new Error(`ensure-site: '${input.slug}' does not slugify to a usable slug`);\n if (input.displayName && siteSlug(input.displayName) !== slug)\n throw new Error(\n `ensure-site: display name '${input.displayName}' slugifies to '${siteSlug(input.displayName)}', not '${slug}' — the row would not be found on re-run`,\n );\n\n const existing = (await listWebsites(base)).find((w) => siteSlug(w.name) === slug);\n\n if (!existing) {\n const fields: FieldSet = {\n Name: input.displayName ?? slug,\n Status: toAirtableStatus(\"building\"),\n [COLS.gitRepo]: input.gitRepo ?? `reddoorla/${slug}`,\n };\n if (input.url) fields[COLS.url] = input.url;\n if (input.pointOfContact) fields[COLS.pointOfContact] = input.pointOfContact;\n const created = (await base(WEBSITES_TABLE).create([{ fields }])) as Records<FieldSet>;\n const rec = created[0]!;\n // The mirror gets what Airtable STORED, never the `fields` we sent: parity\n // diffs Turso against the stored record, so mapping our own payload would\n // diverge the moment Airtable normalises a value.\n await mirror?.created({ id: rec.id, fields: rec.fields as Record<string, unknown> });\n return {\n status: \"created\",\n siteId: rec.id,\n updatedFields: [],\n skippedMismatches: [],\n };\n }\n\n const updates: FieldSet = {};\n const skippedMismatches: string[] = [];\n const blank = (v: unknown) => v === null || v === undefined || v === \"\";\n const consider = (column: string, provided: string | undefined, current: unknown) => {\n if (!provided) return;\n if (blank(current)) updates[column] = provided;\n else if (current !== provided) skippedMismatches.push(column);\n };\n consider(COLS.url, input.url, existing.url || null);\n consider(COLS.pointOfContact, input.pointOfContact, existing.pointOfContact);\n consider(COLS.gitRepo, input.gitRepo, existing.gitRepo);\n\n const updatedFields = Object.keys(updates);\n if (updatedFields.length > 0) {\n await base(WEBSITES_TABLE).update([{ id: existing.id, fields: updates }]);\n // The fill-blanks path writes real cells too — a resumed bootstrap that only\n // filled a blank `url` would otherwise leave Turso stale until the sync.\n await mirror?.site(existing.id, updates);\n }\n return { status: \"exists\", siteId: existing.id, updatedFields, skippedMismatches };\n}\n","import { openBase, readAirtableConfig } from \"../../reports/airtable/client.js\";\nimport { ensureSite } from \"../../reports/airtable/ensure-site.js\";\n\nexport type EnsureSiteCommandOptions = {\n name?: string;\n url?: string;\n contact?: string;\n gitRepo?: string;\n cwd?: string;\n};\n\n/**\n * `ensure-site <slug>` — create/verify the fleet-inventory row for a new site.\n * Day-one step of the /new-site bootstrap skill. Fill-blanks-only; safe to re-run.\n */\nexport async function runEnsureSiteCommand(\n slug: string | undefined,\n opts: EnsureSiteCommandOptions,\n): Promise<{ output: string; code: number }> {\n if (!slug) return { output: \"Provide a <slug> (e.g. `ensure-site roalson`).\", code: 2 };\n try {\n const base = openBase(readAirtableConfig());\n // #539 Phase 5: bootstrapping a site CREATES the Websites row, so this is\n // the one site path that needs an INSERT mirror — every other one updates.\n const { makeSiteMirror } = await import(\"../../db/site-mirror.js\");\n const result = await ensureSite(\n base,\n {\n slug,\n ...(opts.name ? { displayName: opts.name } : {}),\n ...(opts.url ? { url: opts.url } : {}),\n ...(opts.contact ? { pointOfContact: opts.contact } : {}),\n ...(opts.gitRepo ? { gitRepo: opts.gitRepo } : {}),\n },\n await makeSiteMirror(),\n );\n const filled =\n result.updatedFields.length > 0\n ? ` — filled blank field(s): ${result.updatedFields.join(\", \")}`\n : \"\";\n const skipped =\n result.skippedMismatches.length > 0\n ? ` — differs from existing, left untouched (edit in Airtable): ${result.skippedMismatches.join(\", \")}`\n : \"\";\n const nameNote =\n result.status === \"created\" && !opts.name\n ? ` — Name set to \"${slug}\"; retitle in Airtable before forms/announce go live (or re-create with --name)`\n : \"\";\n return {\n output: `[${slug}] ${result.status} (${result.siteId})${filled}${skipped}${nameNote}`,\n code: 0,\n };\n } catch (err) {\n const e = err as { message?: string; exitCode?: number };\n return { output: e.message ?? String(err), code: e.exitCode ?? 1 };\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cli/commands/header-image.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { openBase, readAirtableConfig } from \"../../reports/airtable/client.js\";\nimport { listWebsites, siteSlug, ACTIVE_STATUSES } from \"../../reports/airtable/websites.js\";\nimport type { WebsiteRow } from \"../../reports/airtable/websites.js\";\nimport { uploadAttachment } from \"../../reports/airtable/attachments.js\";\nimport { generateHeaderImage } from \"../../reports/header-image/index.js\";\nimport type { StoredHeaderImage } from \"../../db/header-images.js\";\n\nexport type HeaderImageOptions = {\n all?: boolean;\n force?: boolean;\n writeAirtable?: boolean;\n outDir?: string;\n settleMs?: string;\n /** Injected Turso store for the dual-write (#539, design D5): while Airtable\n * is still written (until the Phase 5 freeze), every upload ALSO lands the\n * bytes in sites.header_image* so the Turso read layer serves a real image.\n * Absent → Airtable-only (a local run without Turso env still works). */\n storeDb?: (siteId: string, img: StoredHeaderImage) => Promise<void>;\n};\n\n/** Which rows this invocation should act on. Pure, so it is unit-tested. */\nexport function resolveTargets(\n rows: readonly WebsiteRow[],\n // `| undefined` spelled out because `exactOptionalPropertyTypes` otherwise\n // rejects the caller passing through possibly-undefined CLI flags.\n opts: { site?: string | undefined; all?: boolean | undefined; force?: boolean | undefined },\n): WebsiteRow[] {\n if (opts.site) {\n const want = siteSlug(opts.site);\n return rows.filter((r) => siteSlug(r.name) === want);\n }\n if (!opts.all) return [];\n return rows.filter((r) => {\n if (!r.url) return false;\n if (r.status === null || !ACTIVE_STATUSES.has(r.status)) return false;\n return opts.force ? true : !r.headerImage;\n });\n}\n\n/** Parse the `--settle-ms <ms>` flag. Unset → undefined (the generator's own\n * default settle). `Number(\"abc\")` is NaN, which would sail through as a NaN\n * timeout and silently skip the settle entirely, so anything non-finite or\n * negative is rejected up front rather than producing a subtly-wrong capture. */\nexport function parseSettleMs(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const n = Number(value);\n if (!Number.isFinite(n) || n < 0) {\n throw Object.assign(\n new Error(`--settle-ms must be a non-negative number of milliseconds, got \"${value}\"`),\n { exitCode: 2 },\n );\n }\n return n;\n}\n\n/** Injected in tests so the file-writing branch can be exercised without\n * driving a real browser (mirrors `GenerateInput.shooter`). */\ntype Generate = typeof generateHeaderImage;\n\n/**\n * Generate a header image per target row and either upload it to Airtable or\n * write it locally for review. Split out of {@link runHeaderImageCommand} so\n * the local-output path is testable without an Airtable base.\n */\nexport async function generateForTargets(\n targets: readonly WebsiteRow[],\n opts: HeaderImageOptions,\n generate: Generate = generateHeaderImage,\n): Promise<{ output: string; code: number }> {\n const outDir = resolve(opts.outDir ?? \"reports\");\n const settleMs = parseSettleMs(opts.settleMs);\n // `reports/` does not exist on a fresh checkout, so the writeFile() below\n // would throw ENOENT for the first operator to run the default (local) path.\n // Create it once, before the loop, rather than per site.\n if (!opts.writeAirtable) await mkdir(outDir, { recursive: true });\n\n const lines: string[] = [];\n let failed = 0;\n\n for (const row of targets) {\n try {\n const gen = await generate({\n url: row.url,\n slug: siteSlug(row.name),\n // `exactOptionalPropertyTypes` rejects an explicit `settleMs: undefined`\n // against `settleMs?: number`, so omit the key entirely when unset.\n ...(settleMs === undefined ? {} : { settleMs }),\n });\n if (opts.writeAirtable) {\n // replaceIn: the field must hold exactly the current header — see\n // uploadAttachment, where appending left readers on a stale [0].\n await uploadAttachment(row.id, \"Header image\", gen.bytes, gen.filename, gen.contentType, {\n replaceIn: \"Websites\",\n });\n let stored = \"\";\n if (opts.storeDb) {\n // Dual-write. A Turso failure must not void the Airtable upload —\n // but it must be VISIBLE, never a silent divergence.\n try {\n await opts.storeDb(row.id, {\n bytes: gen.bytes,\n filename: gen.filename,\n contentType: gen.contentType,\n generatedAt: new Date().toISOString(),\n });\n stored = \" + turso\";\n } catch (err) {\n stored = ` (⚠ turso store FAILED: ${err instanceof Error ? err.message : String(err)})`;\n }\n }\n lines.push(\n `✔ ${row.name} — uploaded ${gen.filename} (${(gen.bytes.byteLength / 1024 / 1024).toFixed(2)} MB)${stored}`,\n );\n } else {\n const path = resolve(outDir, gen.filename);\n await writeFile(path, gen.bytes);\n lines.push(`✔ ${row.name} — wrote ${path} (review, then re-run with --write-airtable)`);\n }\n } catch (err) {\n failed++;\n lines.push(`✖ ${row.name} — ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n lines.push(`${targets.length - failed}/${targets.length} generated.`);\n return { output: lines.join(\"\\n\"), code: failed > 0 ? 1 : 0 };\n}\n\n/**\n * `header-image [site]` — capture a site's live homepage and composite its\n * report header image. Defaults to writing the JPEG locally so the operator can\n * eyeball it; `--write-airtable` uploads it to the Websites row's Header image\n * field. `--all` backfills every live site that has no header image yet.\n */\nexport async function runHeaderImageCommand(\n site: string | undefined,\n opts: HeaderImageOptions,\n): Promise<{ output: string; code: number }> {\n const base = openBase(readAirtableConfig());\n const rows = await listWebsites(base);\n const targets = resolveTargets(rows, { site, all: opts.all, force: opts.force });\n if (targets.length === 0) {\n return {\n output: site ? `No site matched \"${site}\".` : \"No sites need a header image.\",\n code: 1,\n };\n }\n // Wire the Turso dual-write when the env is present; a local run without it\n // still works Airtable-only (the store is per-site error-isolated above).\n let withStore = opts;\n if (opts.writeAirtable && !opts.storeDb && process.env.TURSO_DATABASE_URL) {\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { storeHeaderImage } = await import(\"../../db/header-images.js\");\n const db = await openDb(readDbConfig());\n withStore = { ...opts, storeDb: (id, img) => storeHeaderImage(db, id, img) };\n }\n return generateForTargets(targets, withStore);\n}\n"],"mappings":"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","names":[]}