@reddoorla/maintenance 0.95.1 → 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 (268) hide show
  1. package/README.md +37 -2
  2. package/dist/airtable-SJV4ZSMS.js +9 -0
  3. package/dist/{announce-CLQYX5I4.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-KJRVMMM3.js → chunk-62YWOFVN.js} +36 -5
  17. package/dist/chunk-62YWOFVN.js.map +1 -0
  18. package/dist/{chunk-VSGMCCGQ.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-BI2VI6EM.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-R5IMFAFR.js → chunk-OTEVKRAH.js} +3 -3
  53. package/dist/{chunk-2ZAZ7ETT.js → chunk-P4A6D6HO.js} +4 -4
  54. package/dist/{chunk-XPJQXSC4.js → chunk-P7323YVW.js} +27 -5
  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-C3IQQKXL.js → chunk-T76MA2FH.js} +110 -1
  64. package/dist/{chunk-C3IQQKXL.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-ZCL7C5LH.js → chunk-Y6FCXRIC.js} +1661 -387
  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/playwright-a11y.d.ts +11 -1
  88. package/dist/configs/playwright-a11y.js +3 -1
  89. package/dist/{convert-to-pnpm-YTFREUJK.js → convert-to-pnpm-4DON7IFS.js} +6 -6
  90. package/dist/{db-XKZYDCKM.js → db-5K4CDEGJ.js} +45 -20
  91. package/dist/db-5K4CDEGJ.js.map +1 -0
  92. package/dist/deadletter-G4M3HSVM.js +17 -0
  93. package/dist/{digest-7UBGUBRK.js → digest-5AJGWS33.js} +45 -22
  94. package/dist/digest-5AJGWS33.js.map +1 -0
  95. package/dist/{digest-collectors-DJFN5ZDK.js → digest-collectors-MC5NWEUA.js} +12 -10
  96. package/dist/{digest-state-3KHZF7GD.js → digest-state-ZDPKR7MW.js} +18 -2
  97. package/dist/digest-state-ZDPKR7MW.js.map +1 -0
  98. package/dist/{ensure-site-OAAE5YE2.js → ensure-site-XESZHZEB.js} +39 -11
  99. package/dist/ensure-site-XESZHZEB.js.map +1 -0
  100. package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-GJWGKTLD.js} +6 -4
  101. package/dist/{forms-notify-target-UC6PF3AE.js → forms-notify-target-4NPLX2UG.js} +5 -5
  102. package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
  103. package/dist/{github-signals-TX5FPYP6.js → github-signals-DMU74SAX.js} +10 -10
  104. package/dist/{header-image-PSPT7CUM.js → header-image-BRTOLHJF.js} +6 -5
  105. package/dist/header-image-BRTOLHJF.js.map +1 -0
  106. package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-UCLMSSCN.js} +8 -7
  107. package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-UCLMSSCN.js.map} +1 -1
  108. package/dist/{health-mirror-XJAKA3EY.js → health-mirror-ONRT4DQ5.js} +7 -7
  109. package/dist/{import-airtable-TAKSN4NV.js → import-airtable-IIK2QUUF.js} +3 -3
  110. package/dist/index.d.ts +135 -4
  111. package/dist/index.js +68 -34
  112. package/dist/index.js.map +1 -1
  113. package/dist/{init-FFE4FCO2.js → init-3LMZTCVD.js} +19 -18
  114. package/dist/{init-FFE4FCO2.js.map → init-3LMZTCVD.js.map} +1 -1
  115. package/dist/{launch-UD36AHUN.js → launch-XTDMC6LD.js} +209 -61
  116. package/dist/launch-XTDMC6LD.js.map +1 -0
  117. package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
  118. package/dist/{lighthouse-airtable-YMH4UA7R.js → lighthouse-airtable-3NOFB42B.js} +3 -3
  119. package/dist/{match-harness-QRG6XZAR.js → match-harness-OW45CB5H.js} +14 -7
  120. package/dist/match-harness-OW45CB5H.js.map +1 -0
  121. package/dist/migrate-NE3DUMTM.js +7 -0
  122. package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +3 -3
  123. package/dist/{onboard-TM2KESGF.js → onboard-CZYBWVWH.js} +6 -6
  124. package/dist/{orchestrate-HYVQPRCE.js → orchestrate-TGDFGY52.js} +8 -8
  125. package/dist/{parity-CAJ477PS.js → parity-AEKYSLN6.js} +5 -5
  126. package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
  127. package/dist/{preflight-RYIP54NY.js → preflight-POZZI3GN.js} +9 -9
  128. package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-AHY5ULJR.js} +38 -8
  129. package/dist/prismic-ci-AHY5ULJR.js.map +1 -0
  130. package/dist/{prismic-models-P6FWWVRS.js → prismic-models-URYGODNY.js} +9 -9
  131. package/dist/prospect/types.d.ts +72 -0
  132. package/dist/prospect/types.js +1 -1
  133. package/dist/{prospect-audit-PRHU53HS.js → prospect-audit-AJEE3Z4L.js} +16 -7
  134. package/dist/prospect-audit-AJEE3Z4L.js.map +1 -0
  135. package/dist/{prospect-audits-R3LWF6U7.js → prospect-audits-IZVQY3OQ.js} +4 -2
  136. package/dist/protection-audit-3J3UEOVT.js +413 -0
  137. package/dist/protection-audit-3J3UEOVT.js.map +1 -0
  138. package/dist/recipes/sync-configs.js +3 -3
  139. package/dist/{render-YGNGTX3W.js → render-MHUUHPLN.js} +36 -9
  140. package/dist/render-MHUUHPLN.js.map +1 -0
  141. package/dist/{render-from-row-IFSBHWDH.js → render-from-row-65DC6ZP3.js} +4 -4
  142. package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-ONJMBUHJ.js} +3 -3
  143. package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
  144. package/dist/replay-4FVFOMHH.js.map +1 -0
  145. package/dist/{report-LQS4ECTM.js → report-RC5LGPU3.js} +22 -20
  146. package/dist/report-RC5LGPU3.js.map +1 -0
  147. package/dist/{report-mirror-HDTOJE64.js → report-mirror-KKZZ6RMZ.js} +13 -9
  148. package/dist/{report-mirror-HDTOJE64.js.map → report-mirror-KKZZ6RMZ.js.map} +1 -1
  149. package/dist/{schema-A4WCPIF4.js → schema-KWUQEZ5O.js} +1 -1
  150. package/dist/schema-KWUQEZ5O.js.map +1 -0
  151. package/dist/{self-updating-MWXIZLL7.js → self-updating-RKKIYNV4.js} +8 -7
  152. package/dist/{self-updating-MWXIZLL7.js.map → self-updating-RKKIYNV4.js.map} +1 -1
  153. package/dist/{selftest-VXTHRPRN.js → selftest-AFLR7YV7.js} +10 -10
  154. package/dist/site-lookup-DTOLJ4EA.js +27 -0
  155. package/dist/site-lookup-DTOLJ4EA.js.map +1 -0
  156. package/dist/{site-mirror-WON4FTJ2.js → site-mirror-H2FYMN55.js} +23 -9
  157. package/dist/site-mirror-H2FYMN55.js.map +1 -0
  158. package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-BWHR3XX5.js} +8 -7
  159. package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-BWHR3XX5.js.map} +1 -1
  160. package/dist/{submissions-OVZPVBYG.js → submissions-II5J35QM.js} +4 -4
  161. package/dist/{submissions-3I3GAQAX.js → submissions-JDZP35JM.js} +49 -9
  162. package/dist/submissions-JDZP35JM.js.map +1 -0
  163. package/dist/{svelte-codemods-WSV2FGNG.js → svelte-codemods-QWGIRNN6.js} +6 -6
  164. package/dist/{sync-K3PKHVLH.js → sync-NKPDCMYW.js} +5 -5
  165. package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-HGLQV55H.js} +6 -6
  166. package/dist/{upgrade-VTD6V4HS.js → upgrade-WZ44IARG.js} +6 -6
  167. package/dist/util/git.d.ts +48 -9
  168. package/dist/util/git.js +1 -1
  169. package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
  170. package/dist/{websites-TB4TCAWM.js → websites-6DHIHJ2D.js} +4 -2
  171. package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-BIK6LNVC.js} +5 -5
  172. package/dist/write-audits-to-airtable-BIK6LNVC.js.map +1 -0
  173. package/package.json +3 -2
  174. package/dist/airtable-JGCPS7JH.js +0 -9
  175. package/dist/chunk-5UINJNAJ.js.map +0 -1
  176. package/dist/chunk-AVEQUR44.js.map +0 -1
  177. package/dist/chunk-BVEDKMNG.js.map +0 -1
  178. package/dist/chunk-BYGPUUCT.js.map +0 -1
  179. package/dist/chunk-CWXTNPNZ.js.map +0 -1
  180. package/dist/chunk-DFKBTBO3.js.map +0 -1
  181. package/dist/chunk-FSGICFXB.js.map +0 -1
  182. package/dist/chunk-GASBX52O.js +0 -12
  183. package/dist/chunk-GASBX52O.js.map +0 -1
  184. package/dist/chunk-KJRVMMM3.js.map +0 -1
  185. package/dist/chunk-LM2NHRCJ.js.map +0 -1
  186. package/dist/chunk-QF6IQQFP.js.map +0 -1
  187. package/dist/chunk-TO7DJYLZ.js.map +0 -1
  188. package/dist/chunk-TR3HFH6J.js.map +0 -1
  189. package/dist/chunk-WJMPCVOR.js.map +0 -1
  190. package/dist/chunk-WSJ7RO54.js.map +0 -1
  191. package/dist/chunk-WVNDEYXL.js.map +0 -1
  192. package/dist/chunk-XHHMM5HX.js.map +0 -1
  193. package/dist/chunk-XPJQXSC4.js.map +0 -1
  194. package/dist/chunk-YLYXW5PH.js.map +0 -1
  195. package/dist/chunk-ZCL7C5LH.js.map +0 -1
  196. package/dist/client-G2WGWPCA.js +0 -11
  197. package/dist/db-XKZYDCKM.js.map +0 -1
  198. package/dist/digest-7UBGUBRK.js.map +0 -1
  199. package/dist/digest-state-3KHZF7GD.js.map +0 -1
  200. package/dist/ensure-site-OAAE5YE2.js.map +0 -1
  201. package/dist/header-image-PSPT7CUM.js.map +0 -1
  202. package/dist/launch-UD36AHUN.js.map +0 -1
  203. package/dist/match-harness-QRG6XZAR.js.map +0 -1
  204. package/dist/migrate-Y7R4CESL.js +0 -7
  205. package/dist/prismic-ci-6GKKZGB5.js.map +0 -1
  206. package/dist/prospect-audit-PRHU53HS.js.map +0 -1
  207. package/dist/protection-audit-SJO53MEQ.js +0 -168
  208. package/dist/protection-audit-SJO53MEQ.js.map +0 -1
  209. package/dist/render-YGNGTX3W.js.map +0 -1
  210. package/dist/replay-PCSQOGCZ.js.map +0 -1
  211. package/dist/report-LQS4ECTM.js.map +0 -1
  212. package/dist/schema-A4WCPIF4.js.map +0 -1
  213. package/dist/site-mirror-WON4FTJ2.js.map +0 -1
  214. package/dist/submissions-3I3GAQAX.js.map +0 -1
  215. /package/dist/{airtable-JGCPS7JH.js.map → airtable-SJV4ZSMS.js.map} +0 -0
  216. /package/dist/{announce-CLQYX5I4.js.map → announce-45HVP4J4.js.map} +0 -0
  217. /package/dist/{blux-4KNXUP2K.js.map → blux-RUFMNOUJ.js.map} +0 -0
  218. /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-EL3TKMIL.js.map} +0 -0
  219. /package/dist/{chunk-XN37VTDB.js.map → chunk-37B4LVB3.js.map} +0 -0
  220. /package/dist/{chunk-WMBLR5VA.js.map → chunk-44SDGPGQ.js.map} +0 -0
  221. /package/dist/{chunk-VSGMCCGQ.js.map → chunk-6INOBNRO.js.map} +0 -0
  222. /package/dist/{chunk-5GUJ36TL.js.map → chunk-74QRWE6G.js.map} +0 -0
  223. /package/dist/{chunk-S7RYFUIK.js.map → chunk-CA6ADGOC.js.map} +0 -0
  224. /package/dist/{chunk-KZRR4NFG.js.map → chunk-D3CTOCUR.js.map} +0 -0
  225. /package/dist/{chunk-BCKEP5S6.js.map → chunk-ECDJDHKT.js.map} +0 -0
  226. /package/dist/{chunk-BI2VI6EM.js.map → chunk-EQQ4H6L3.js.map} +0 -0
  227. /package/dist/{chunk-Z2K4JPQ3.js.map → chunk-H445EH33.js.map} +0 -0
  228. /package/dist/{chunk-FUWFLAWQ.js.map → chunk-IMYL67JV.js.map} +0 -0
  229. /package/dist/{chunk-R5IMFAFR.js.map → chunk-OTEVKRAH.js.map} +0 -0
  230. /package/dist/{chunk-2ZAZ7ETT.js.map → chunk-P4A6D6HO.js.map} +0 -0
  231. /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
  232. /package/dist/{chunk-2LYOAB2E.js.map → chunk-RRE74VWI.js.map} +0 -0
  233. /package/dist/{chunk-ZQV7UQNE.js.map → chunk-SQ4FSNYL.js.map} +0 -0
  234. /package/dist/{chunk-6R56OKJZ.js.map → chunk-UIITN37J.js.map} +0 -0
  235. /package/dist/{chunk-CJ2RMTT7.js.map → chunk-VEIIUCYL.js.map} +0 -0
  236. /package/dist/{chunk-HTABUCF4.js.map → chunk-Y5BVR4HW.js.map} +0 -0
  237. /package/dist/{chunk-5EEMTJMT.js.map → chunk-ZH5LOQET.js.map} +0 -0
  238. /package/dist/{client-G2WGWPCA.js.map → client-L7NAUG6J.js.map} +0 -0
  239. /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-4DON7IFS.js.map} +0 -0
  240. /package/dist/{digest-collectors-DJFN5ZDK.js.map → deadletter-G4M3HSVM.js.map} +0 -0
  241. /package/dist/{fleet-state-UEVZ3X3J.js.map → digest-collectors-MC5NWEUA.js.map} +0 -0
  242. /package/dist/{freeze-A6MUCZU4.js.map → fleet-state-GJWGKTLD.js.map} +0 -0
  243. /package/dist/{forms-notify-target-UC6PF3AE.js.map → forms-notify-target-4NPLX2UG.js.map} +0 -0
  244. /package/dist/{import-airtable-TAKSN4NV.js.map → freeze-GUQ43EAU.js.map} +0 -0
  245. /package/dist/{github-signals-TX5FPYP6.js.map → github-signals-DMU74SAX.js.map} +0 -0
  246. /package/dist/{health-mirror-XJAKA3EY.js.map → health-mirror-ONRT4DQ5.js.map} +0 -0
  247. /package/dist/{lighthouse-VHI77PFB.js.map → import-airtable-IIK2QUUF.js.map} +0 -0
  248. /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
  249. /package/dist/{migrate-Y7R4CESL.js.map → lighthouse-airtable-3NOFB42B.js.map} +0 -0
  250. /package/dist/{notify-IISTHNBR.js.map → migrate-NE3DUMTM.js.map} +0 -0
  251. /package/dist/{orchestrate-HYVQPRCE.js.map → notify-K3WVH3DM.js.map} +0 -0
  252. /package/dist/{onboard-TM2KESGF.js.map → onboard-CZYBWVWH.js.map} +0 -0
  253. /package/dist/{parity-CAJ477PS.js.map → orchestrate-TGDFGY52.js.map} +0 -0
  254. /package/dist/{pipeline-YROAY6DT.js.map → parity-AEKYSLN6.js.map} +0 -0
  255. /package/dist/{prospect-audits-R3LWF6U7.js.map → pipeline-UUPXOS2J.js.map} +0 -0
  256. /package/dist/{preflight-RYIP54NY.js.map → preflight-POZZI3GN.js.map} +0 -0
  257. /package/dist/{prismic-models-P6FWWVRS.js.map → prismic-models-URYGODNY.js.map} +0 -0
  258. /package/dist/{render-from-row-IFSBHWDH.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
  259. /package/dist/{websites-TB4TCAWM.js.map → render-from-row-65DC6ZP3.js.map} +0 -0
  260. /package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-ONJMBUHJ.js.map} +0 -0
  261. /package/dist/{selftest-VXTHRPRN.js.map → selftest-AFLR7YV7.js.map} +0 -0
  262. /package/dist/{submissions-OVZPVBYG.js.map → submissions-II5J35QM.js.map} +0 -0
  263. /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-QWGIRNN6.js.map} +0 -0
  264. /package/dist/{sync-K3PKHVLH.js.map → sync-NKPDCMYW.js.map} +0 -0
  265. /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-HGLQV55H.js.map} +0 -0
  266. /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-WZ44IARG.js.map} +0 -0
  267. /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
  268. /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → websites-6DHIHJ2D.js.map} +0 -0
@@ -50,6 +50,14 @@ var DEVICE_SCALE_FACTOR = 2;
50
50
  var DEFAULT_SETTLE_MS = 2500;
51
51
  var NAV_TIMEOUT_MS = 6e4;
52
52
  var IDLE_BUDGET_MS = 3e3;
53
+ var CONSENT_SELECTOR = '[class*="cookie" i],[id*="cookie" i],[class*="consent" i],[id*="consent" i]';
54
+ var CONSENT_BUTTON_NAME = /^(accept( all)?( cookies)?|allow( all)?|reject( all)?|decline|got it|ok(ay)?|i (agree|understand)|agree)$/i;
55
+ var CONSENT_CLICK_TIMEOUT_MS = 1500;
56
+ function consentHideRule(consentSelector) {
57
+ const extra = consentSelector?.trim();
58
+ const selector = extra ? `${CONSENT_SELECTOR},${extra}` : CONSENT_SELECTOR;
59
+ return `${selector}{display:none!important;visibility:hidden!important;pointer-events:none!important}`;
60
+ }
53
61
  async function captureHomepage(url, options = {}) {
54
62
  const shooter = options.shooter ?? await defaultShooter();
55
63
  return shooter.shoot({
@@ -57,7 +65,8 @@ async function captureHomepage(url, options = {}) {
57
65
  width: VIEWPORT.width,
58
66
  height: VIEWPORT.height,
59
67
  deviceScaleFactor: DEVICE_SCALE_FACTOR,
60
- settleMs: options.settleMs ?? DEFAULT_SETTLE_MS
68
+ settleMs: options.settleMs ?? DEFAULT_SETTLE_MS,
69
+ ...options.consentSelector !== void 0 ? { consentSelector: options.consentSelector } : {}
61
70
  });
62
71
  }
63
72
  async function defaultShooter() {
@@ -74,6 +83,9 @@ async function defaultShooter() {
74
83
  await page.waitForLoadState("networkidle", { timeout: IDLE_BUDGET_MS }).catch(() => {
75
84
  });
76
85
  await page.evaluate("document.fonts && document.fonts.ready");
86
+ await page.getByRole("button", { name: CONSENT_BUTTON_NAME }).first().click({ timeout: CONSENT_CLICK_TIMEOUT_MS }).catch(() => {
87
+ });
88
+ await page.addStyleTag({ content: consentHideRule(opts.consentSelector) });
77
89
  await page.waitForTimeout(opts.settleMs);
78
90
  const buf = await page.screenshot({ type: "png" });
79
91
  return new Uint8Array(buf);
@@ -217,7 +229,8 @@ async function applyReportTypeHeadline(header, reportType) {
217
229
  async function generateHeaderImage(input) {
218
230
  const screenshot = await captureHomepage(input.url, {
219
231
  ...input.shooter !== void 0 ? { shooter: input.shooter } : {},
220
- ...input.settleMs !== void 0 ? { settleMs: input.settleMs } : {}
232
+ ...input.settleMs !== void 0 ? { settleMs: input.settleMs } : {},
233
+ ...input.consentSelector !== void 0 ? { consentSelector: input.consentSelector } : {}
221
234
  });
222
235
  await assertNotBlank(screenshot);
223
236
  const domain = domainFromUrl(input.url);
@@ -237,4 +250,4 @@ export {
237
250
  applyReportTypeHeadline,
238
251
  generateHeaderImage
239
252
  };
240
- //# sourceMappingURL=chunk-5UINJNAJ.js.map
253
+ //# sourceMappingURL=chunk-MZZDMBDT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/header-image/assets/index.ts","../src/reports/header-image/index.ts","../src/reports/header-image/capture.ts","../src/reports/header-image/compose.ts","../src/reports/header-image/geometry.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst PLATE = \"plate-clean.png\";\n\n/** Per-report-type headline overlays, stamped over the CLEAN plate at send time\n * (see orchestrate.ts). Types with no entry go out on the clean plate.\n *\n * Source: Figma \"Web-Maintenance-Email\" (mQ3hy2d9JnOG9ljCzbZS8j) —\n * Maintenance is node 158:15, Testing is 153:721 (\"maintenance & testing\",\n * which is what a Testing report actually covers). Export each at 4x; Figma\n * crops a text export to its ink box, which is why these are 1328x660 /\n * 1715x664 rather than the 1840x720 text box.\n *\n * ⚠️ A Figma MCP `download_assets` export arrives FLATTENED — never\n * transparent — and it flattens onto WHATEVER SITS BEHIND THE NODE, which is\n * not one fixed colour: Maintenance/Testing live inside a white Header frame\n * and come back on white, while Announcement/Launch sit on the bare canvas and\n * come back on Figma's canvas grey (30,30,30). Stamping either unprocessed\n * paints a solid slab over the paper texture — that, not a bad design file, is\n * what made the 2026-08-20 Testing asset unusable.\n *\n * Recover the alpha instead of re-exporting by hand: for ink C over backdrop B,\n * a = (px - B) / (C - B). DETECT B (the export's most common colour) rather\n * than assuming white, and pick the channel with the largest |C - B| — green\n * separates the brand red from white by 221 levels but from canvas grey by only\n * 4, which would quantise the alpha to noise. Verified against the known-good\n * Maintenance asset at mean abs alpha diff 0.022, 0.0037% of pixels off by\n * more than 8.\n *\n * Editing this copy needs Figma desktop, not the Plugin API: `characters`\n * requires loadFontAsync and the MCP font environment carries no Helvetica at\n * all, so the licensed Helvetica Neue LT Std can never load there. */\nconst HEADLINE_FILES = {\n Maintenance: \"headline-maintenance.png\",\n Testing: \"headline-testing.png\",\n Announcement: \"headline-announcement.png\",\n Launch: \"headline-launch.png\",\n} as const;\n\nexport type HeadlineKind = keyof typeof HEADLINE_FILES;\n\n/** The headline kind for a report type, or null for types that ship clean. */\nexport function headlineKindFor(reportType: string): HeadlineKind | null {\n return Object.hasOwn(HEADLINE_FILES, reportType) ? (reportType as HeadlineKind) : null;\n}\n\n// Walk up from this module's URL looking for the assets dir in either the dev\n// layout (src/reports/header-image/assets/) or the published layout\n// (dist/reports/header-image/assets/). REQUIRED because tsup inlines this module\n// into dist/cli/bin.js — so import.meta.url-based sibling resolution looks in\n// dist/cli/ and fails with ENOENT. This exact bug shipped in 0.10.0-0.10.1 for\n// the maintenance-email assets; tests passed in dev because vitest evaluates the\n// source file, where import.meta.url is already correct. Only `pnpm test:dist`\n// catches a regression here.\nlet cachedAssetsDir: string | null = null;\nfunction resolveAssetsDir(): string {\n if (cachedAssetsDir) return cachedAssetsDir;\n let dir = dirname(fileURLToPath(import.meta.url));\n for (;;) {\n for (const layout of [\"src\", \"dist\"]) {\n const candidate = join(dir, layout, \"reports\", \"header-image\", \"assets\", PLATE);\n if (existsSync(candidate)) {\n cachedAssetsDir = dirname(candidate);\n return cachedAssetsDir;\n }\n }\n const parent = dirname(dir);\n if (parent === dir) {\n throw new Error(\n `loadPlate: could not locate the header-image assets dir by walking up from ${fileURLToPath(import.meta.url)}. Checked both src/ and dist/ layouts.`,\n );\n }\n dir = parent;\n }\n}\n\n/** Read the bundled 2400x3200 CLEAN plate (no headline — the report type's\n * headline is stamped at send time, so one stored image serves every type). */\nexport async function loadPlate(): Promise<Uint8Array> {\n return new Uint8Array(await readFile(join(resolveAssetsDir(), PLATE)));\n}\n\n/** Read a headline overlay (transparent PNG, ink-box cropped). */\nexport async function loadHeadline(kind: HeadlineKind): Promise<Uint8Array> {\n return new Uint8Array(await readFile(join(resolveAssetsDir(), HEADLINE_FILES[kind])));\n}\n","import sharp from \"sharp\";\nimport { captureHomepage, type Shooter } from \"./capture.js\";\nimport { composeHeaderImage, stampHeadline, headlineInkCount } from \"./compose.js\";\nimport { loadPlate, loadHeadline, headlineKindFor } from \"./assets/index.js\";\nimport { CANVAS } from \"./geometry.js\";\n\n/** Headline-red pixels tolerated in the band before a stored header is treated\n * as already carrying a headline. A clean-plate header measures 0 and a baked\n * one ~62,000, so this sits far from both — it exists only to absorb stray\n * JPEG artefacts, not to tune a boundary. */\nconst HEADLINE_INK_TOLERANCE = 500;\n\nexport type GenerateInput = {\n /** The site's production URL. */\n url: string;\n /** Airtable site slug; drives the attachment filename. */\n slug?: string;\n /** Injected browser IO (tests). */\n shooter?: Shooter;\n /** Per-site settle override for animation-heavy homepages. */\n settleMs?: number;\n /** Extra CSS selector(s) hidden before the shutter, for consent or\n * interstitial UI the capture's class/id heuristic misses (#654). */\n consentSelector?: string;\n};\n\nexport type GeneratedHeaderImage = {\n bytes: Uint8Array;\n /** The domain as printed on the plate. */\n domain: string;\n filename: string;\n contentType: \"image/jpeg\";\n};\n\n/** Strip scheme, leading www and any path — the plate prints a bare domain. */\nexport function domainFromUrl(url: string): string {\n return new URL(url).hostname.replace(/^www\\./, \"\");\n}\n\n/**\n * A capture is \"blank\" when it is almost entirely near-white — a consent gate,\n * a failed render (blank canvas), or a shot fired before first paint. The\n * operator reviews the rendered email before sending, so this is a backstop\n * rather than the only gate; its job is to stop an obviously broken shot from\n * replacing a good stored header.\n *\n * Uniformity alone is NOT the signal: a homepage with a large solid-colour\n * hero (any brand colour) is legitimately uniform but not blank. Only\n * near-white uniformity is flagged, since every real failure mode here\n * (consent overlay, unrendered canvas, pre-paint flash) lands on a white\n * background.\n */\nasync function assertNotBlank(shot: Uint8Array): Promise<void> {\n const { data, info } = await sharp(Buffer.from(shot))\n .resize(64, 40, { fit: \"fill\" })\n .removeAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true });\n const total = info.width * info.height;\n const at = (i: number): number => data[i] ?? 0;\n\n let sumR = 0;\n let sumG = 0;\n let sumB = 0;\n for (let i = 0; i < data.length; i += info.channels) {\n sumR += at(i);\n sumG += at(i + 1);\n sumB += at(i + 2);\n }\n const meanR = sumR / total;\n const meanG = sumG / total;\n const meanB = sumB / total;\n\n let near = 0;\n for (let i = 0; i < data.length; i += info.channels) {\n if (\n Math.abs(at(i) - meanR) < 8 &&\n Math.abs(at(i + 1) - meanG) < 8 &&\n Math.abs(at(i + 2) - meanB) < 8\n ) {\n near++;\n }\n }\n\n const uniform = near / total > 0.95;\n const nearWhite = meanR > 240 && meanG > 240 && meanB > 240;\n if (uniform && nearWhite) {\n throw new Error(\n \"header-image: capture looks blank (near-white, >95% one colour) — keeping the existing header\",\n );\n }\n}\n\n/**\n * Stamp the report type's headline onto a stored (clean) header at send time.\n * Types with no headline registered pass through untouched, as does any header\n * that must not be stamped:\n *\n * - wrong dimensions (a legacy hand-made header), where fixed coordinates\n * would misplace the text;\n * - a header that ALREADY CARRIES A HEADLINE, i.e. one built on the old baked\n * plate. Stamping those prints the new headline directly over the baked one\n * and the two overprint into unreadable pulp — which shipped in a real\n * announcement on 2026-08-24. An earlier version of this comment claimed\n * such headers were safe because they'd \"keep their baked headline\"; that\n * was wrong. They are canvas-sized, so nothing stopped them being stamped.\n *\n * The band is empty on a clean-plate header and holds ~62k red px on a baked\n * one, so the check is unambiguous rather than a tuned threshold. A skipped\n * header still goes out looking correct (its baked headline is a real headline)\n * and self-heals on the site's next draft, which regenerates it clean;\n * `header-image --all --force` does the whole fleet at once.\n */\nexport async function applyReportTypeHeadline(\n header: Uint8Array,\n reportType: string,\n): Promise<Uint8Array> {\n const kind = headlineKindFor(reportType);\n if (!kind) return header;\n // Best-effort by design: the headline is cosmetic, so anything unexpected —\n // undecodable bytes included — warns and sends the header as stored rather\n // than failing the send. Mandatory processing (and its hard failures) lives\n // in prepareHeaderImage downstream.\n try {\n const meta = await sharp(Buffer.from(header)).metadata();\n if (meta.width !== CANVAS.width || meta.height !== CANVAS.height) {\n console.warn(\n `⚑ header headline skipped: stored header is ${meta.width}x${meta.height}, not ${CANVAS.width}x${CANVAS.height} — sending it as stored`,\n );\n return header;\n }\n const existing = await headlineInkCount(header);\n if (existing > HEADLINE_INK_TOLERANCE) {\n console.warn(\n `⚑ header headline skipped: the stored header already carries a headline (${existing} ink px in the band) — it predates the clean-plate switch, so stamping would overprint. Sending it as stored; run \\`header-image ${\"<site>\"} --write-airtable\\` to regenerate it clean.`,\n );\n return header;\n }\n return await stampHeadline(header, await loadHeadline(kind));\n } catch (err) {\n console.warn(\n `⚑ header headline skipped (${err instanceof Error ? err.message : String(err)}) — sending the header as stored`,\n );\n return header;\n }\n}\n\n/** Capture a site's homepage and composite its header image. */\nexport async function generateHeaderImage(input: GenerateInput): Promise<GeneratedHeaderImage> {\n const screenshot = await captureHomepage(input.url, {\n ...(input.shooter !== undefined ? { shooter: input.shooter } : {}),\n ...(input.settleMs !== undefined ? { settleMs: input.settleMs } : {}),\n ...(input.consentSelector !== undefined ? { consentSelector: input.consentSelector } : {}),\n });\n await assertNotBlank(screenshot);\n\n const domain = domainFromUrl(input.url);\n const bytes = await composeHeaderImage({\n plate: await loadPlate(),\n screenshot,\n domain,\n });\n const base = input.slug ?? domain.split(\".\")[0];\n return { bytes, domain, filename: `${base}Header.jpg`, contentType: \"image/jpeg\" };\n}\n","/** Viewport matching the plate's MacBook screen aspect (16:10), so the crop in\n * compose.ts is a no-op for a well-behaved homepage. */\nconst VIEWPORT = { width: 1600, height: 1000 } as const;\n/** 2x so the 1349px-wide screen rect is fed real pixels, not upscaled ones. */\nconst DEVICE_SCALE_FACTOR = 2;\n/** Entrance animations and webfonts settle before the shutter. Measured against\n * live fleet sites; overridable per site because consent gates and long\n * animations vary. */\nconst DEFAULT_SETTLE_MS = 2500;\n/** Budget for the `load` milestone — the navigation we actually gate on.\n *\n * Do NOT put `waitUntil: \"networkidle\"` back on the `goto`. Measured across all\n * 14 live fleet sites (30s timeout): `load` succeeded on 14/14 in 346–1205ms,\n * while `networkidle` NEVER fired on 4 of them (ERP Industrials, Vineyard,\n * Revogen, 1836dig) — chat widgets, analytics polling and websockets hold the\n * connection open forever, so `goto` threw and those sites could never get a\n * header image at all. */\nconst NAV_TIMEOUT_MS = 60_000;\n/** Best-effort extra wait for network idle, applied *after* `load` and with its\n * rejection swallowed. The 10 sites that do settle still get the benefit; the 4\n * that never will just proceed. 3s is sized from the measurement above: the\n * slowest site that does reach idle took 4078ms from `goto` start, and by the\n * time this runs the `load` milestone (~0.3–1.2s) has already passed, so 3s\n * covers the real remaining settle time without stalling the never-idle sites. */\nconst IDLE_BUDGET_MS = 3_000;\n\n/** Attribute heuristic for a cookie / consent banner: any element whose class or\n * id mentions \"cookie\" or \"consent\", case-insensitively. Sonder's header shipped\n * its consent panel and the panel's scrim over the hero (#654) — the banner\n * never leaves on its own, so settle time was irrelevant and hiding it is the\n * whole fix. Kept narrow on purpose: a broader net (e.g. \"modal\", \"overlay\")\n * would start hiding hero art on sites that use those words for content. */\nexport const CONSENT_SELECTOR =\n '[class*=\"cookie\" i],[id*=\"cookie\" i],[class*=\"consent\" i],[id*=\"consent\" i]';\n\n/** Accessible names of the buttons a consent banner offers. Clicking one lets the\n * site run its own dismissal, which is the only thing that reliably removes a\n * scrim living OUTSIDE the banner element (a `body::before`, a sibling overlay). */\nexport const CONSENT_BUTTON_NAME =\n /^(accept( all)?( cookies)?|allow( all)?|reject( all)?|decline|got it|ok(ay)?|i (agree|understand)|agree)$/i;\n\n/** Budget for the best-effort consent click. A site with no banner has no button,\n * so the click times out on every capture there — it must be short, and it is\n * swallowed. Sized so the 14 consent-free fleet sites pay ≤1.5s each. */\nconst CONSENT_CLICK_TIMEOUT_MS = 1_500;\n\n/**\n * The CSS rule that hides consent UI before the shutter. `consentSelector` is a\n * per-site addition for banners the heuristic misses (a newsletter interstitial,\n * a GDPR shield with a bespoke class); it is joined onto the heuristic, never a\n * replacement for it. Pure, so it can be tested without a browser.\n */\nexport function consentHideRule(consentSelector?: string): string {\n const extra = consentSelector?.trim();\n const selector = extra ? `${CONSENT_SELECTOR},${extra}` : CONSENT_SELECTOR;\n return `${selector}{display:none!important;visibility:hidden!important;pointer-events:none!important}`;\n}\n\nexport type ShootOptions = {\n url: string;\n width: number;\n height: number;\n deviceScaleFactor: number;\n settleMs: number;\n /** Extra CSS selector(s) hidden before the shutter, for a site whose consent\n * or interstitial UI the class/id heuristic misses. */\n consentSelector?: string;\n};\n\n/** Injected browser IO. The real impl drives Playwright; tests pass a fake. */\nexport type Shooter = {\n shoot: (opts: ShootOptions) => Promise<Uint8Array>;\n};\n\nexport type CaptureOptions = {\n shooter?: Shooter;\n settleMs?: number;\n consentSelector?: string;\n};\n\n/**\n * Screenshot a site's homepage for the header image.\n *\n * Viewport-only, never `fullPage`: the plate shows a laptop screen, not a\n * scroll. Failures propagate — a caller must be able to keep the site's existing\n * header rather than overwrite it with a broken shot.\n */\nexport async function captureHomepage(\n url: string,\n options: CaptureOptions = {},\n): Promise<Uint8Array> {\n const shooter = options.shooter ?? (await defaultShooter());\n return shooter.shoot({\n url,\n width: VIEWPORT.width,\n height: VIEWPORT.height,\n deviceScaleFactor: DEVICE_SCALE_FACTOR,\n settleMs: options.settleMs ?? DEFAULT_SETTLE_MS,\n ...(options.consentSelector !== undefined ? { consentSelector: options.consentSelector } : {}),\n });\n}\n\n/** Real Playwright shooter. Lazily imported so unit tests never load it and the\n * static import graph stays central-dep-free for `test:dist`. */\nexport async function defaultShooter(): Promise<Shooter> {\n const { chromium } = await import(\"@playwright/test\");\n return {\n async shoot(opts) {\n const browser = await chromium.launch();\n try {\n const page = await browser.newPage({\n viewport: { width: opts.width, height: opts.height },\n deviceScaleFactor: opts.deviceScaleFactor,\n });\n await page.goto(opts.url, { waitUntil: \"load\", timeout: NAV_TIMEOUT_MS });\n // Best-effort only — a site that never idles must still be captured.\n await page.waitForLoadState(\"networkidle\", { timeout: IDLE_BUDGET_MS }).catch(() => {});\n await page.evaluate(\"document.fonts && document.fonts.ready\");\n // Dismiss consent UI, strictly best-effort — a header capture must never\n // fail because a site has no banner. The click goes FIRST: once the rule\n // below hides the button it is no longer actionable, and the site's own\n // dismissal is what unwinds a scrim that lives outside the banner. Then\n // the style tag catches a banner with no matching button, or one that\n // fades out slower than the settle. Both run before the settle wait so\n // the settle absorbs whatever animation the dismissal starts.\n await page\n .getByRole(\"button\", { name: CONSENT_BUTTON_NAME })\n .first()\n .click({ timeout: CONSENT_CLICK_TIMEOUT_MS })\n .catch(() => {});\n await page.addStyleTag({ content: consentHideRule(opts.consentSelector) });\n await page.waitForTimeout(opts.settleMs);\n const buf = await page.screenshot({ type: \"png\" });\n return new Uint8Array(buf);\n } finally {\n await browser.close();\n }\n },\n };\n}\n","import sharp from \"sharp\";\nimport type { OverlayOptions } from \"sharp\";\nimport {\n CANVAS,\n SCREEN,\n DOMAIN,\n PAPER,\n HEADLINE,\n HEADLINE_BAND,\n HEADLINE_INK,\n} from \"./geometry.js\";\n\nexport type ComposeInput = {\n /** The bundled 2400x3200 plate. */\n plate: Uint8Array;\n /** A homepage screenshot, any size. Cropped to fill the screen rect. */\n screenshot: Uint8Array;\n /** The site's public domain, drawn bottom-left (e.g. \"gallerysonder.com\"). */\n domain: string;\n};\n\n/** JPEG quality. The hand-made headers are ~2.4MB; 88 lands near 0.7MB with no\n * visible difference at the 600px the email actually renders. */\nconst JPEG_QUALITY = 88;\n\n/** Escape the five XML metacharacters. The domain is interpolated into an SVG,\n * and a bare `&` yields invalid XML that sharp rejects outright. */\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\n/**\n * The domain line as a transparent SVG overlay the size of the full canvas, so\n * it can be composited at 0,0 and positioned by its own coordinates.\n *\n * ⚠️ FONT AVAILABILITY IS ENVIRONMENT-DEPENDENT. sharp renders SVG through\n * librsvg + fontconfig, so the family below resolves against INSTALLED fonts.\n * On macOS it resolves to real Helvetica Neue, which reproduces the hand-made\n * reference exactly (verified: ink box dx=0 dy=0 dw=0 dh=0). A stock\n * ubuntu-latest runner has none of Helvetica Neue / Helvetica / Arial, so it\n * falls through to `sans-serif` — typically DejaVu Sans, which is wider and\n * would render a visibly different domain line.\n *\n * This matters because report drafting (which regenerates the header) runs on\n * ubuntu-latest in `.github/workflows/daily-reports.yml`. That workflow now\n * installs `fonts-urw-base35`, which provides Nimbus Sans — metrically\n * equivalent to Helvetica and third in the stack below — so a CI-generated\n * header's domain line matches a locally-generated one. Any OTHER environment\n * that runs this code still needs a Helvetica-metric font installed.\n */\nfunction domainSvg(domain: string): Buffer {\n return Buffer.from(\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${CANVAS.width}\" height=\"${CANVAS.height}\">\n <text x=\"${DOMAIN.x}\" y=\"${DOMAIN.baseline}\"\n font-family=\"Helvetica Neue, Helvetica, Nimbus Sans, Arial, sans-serif\"\n font-size=\"${DOMAIN.size}\" font-weight=\"${DOMAIN.weight}\"\n fill=\"${DOMAIN.color}\">${escapeXml(domain)}</text>\n </svg>`,\n \"utf-8\",\n );\n}\n\n/**\n * Composite one site's header image: paste its homepage screenshot into the\n * plate's laptop screen and draw its domain underneath.\n *\n * PURE — no browser, no network, no filesystem. Fidelity lives here, so it must\n * be verifiable in a unit test.\n *\n * The screenshot is cropped to FILL the 16:10 screen from the top: a taller\n * homepage loses its fold rather than being squashed, which would be obvious\n * and wrong. `flatten` guards against a transparent source punching a hole in\n * the plate.\n */\nexport async function composeHeaderImage(input: ComposeInput): Promise<Uint8Array> {\n const screen = await sharp(Buffer.from(input.screenshot))\n .resize(SCREEN.w, SCREEN.h, { fit: \"cover\", position: \"top\" })\n .flatten({ background: PAPER })\n .toBuffer();\n\n const layers: OverlayOptions[] = [{ input: screen, left: SCREEN.x, top: SCREEN.y }];\n if (input.domain.trim().length > 0) {\n layers.push({ input: domainSvg(input.domain), left: 0, top: 0 });\n }\n\n const out = await sharp(Buffer.from(input.plate))\n .composite(layers)\n .jpeg({ quality: JPEG_QUALITY })\n .toBuffer();\n return new Uint8Array(out);\n}\n\n/**\n * Count headline-red pixels already present in the headline band.\n *\n * A header composed on the clean plate has EXACTLY ZERO (measured), because the\n * band sits between the logo and the laptop and nothing else paints there. A\n * header built on the old baked plate has ~62,000 — its headline is part of the\n * image. That gap is what lets {@link stampHeadline}'s caller refuse to print a\n * second headline over the first. PURE.\n */\nexport async function headlineInkCount(header: Uint8Array): Promise<number> {\n const { data, info } = await sharp(Buffer.from(header))\n .extract({\n left: HEADLINE_BAND.x,\n top: HEADLINE_BAND.y,\n width: HEADLINE_BAND.w,\n height: HEADLINE_BAND.h,\n })\n .removeAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true });\n let n = 0;\n for (let i = 0; i < data.length; i += info.channels) {\n const r = data[i] ?? 0;\n const g = data[i + 1] ?? 0;\n const b = data[i + 2] ?? 0;\n // Generous band around the brand red rather than an exact match: the stored\n // header is a JPEG, so its ink has been through lossy re-encoding.\n if (r > HEADLINE_INK.r - 60 && g < HEADLINE_INK.g + 60 && b < HEADLINE_INK.b + 60) n++;\n }\n return n;\n}\n\n/**\n * Stamp a report-type headline overlay onto a finished (clean-plate) header at\n * the measured HEADLINE origin. PURE and strict: the header must be exactly\n * CANVAS-sized — a legacy hand-made header at other dimensions would misplace\n * the text, so the caller (applyReportTypeHeadline) size-guards and skips\n * instead of calling in. One extra JPEG generation at quality 88 is invisible\n * at the email's 600px display size.\n */\nexport async function stampHeadline(header: Uint8Array, headline: Uint8Array): Promise<Uint8Array> {\n const meta = await sharp(Buffer.from(header)).metadata();\n if (meta.width !== CANVAS.width || meta.height !== CANVAS.height) {\n throw new Error(\n `stampHeadline: header is ${meta.width}x${meta.height}, expected ${CANVAS.width}x${CANVAS.height}`,\n );\n }\n const out = await sharp(Buffer.from(header))\n .composite([{ input: Buffer.from(headline), left: HEADLINE.x, top: HEADLINE.y }])\n .jpeg({ quality: JPEG_QUALITY })\n .toBuffer();\n return new Uint8Array(out);\n}\n","// Measured off the real plate, not guessed.\n//\n// SCREEN is the hole INSIDE the laptop bezel. It is derived from the bezel —\n// a perfectly flat black frame (luminance 0.0) — because that is\n// content-independent. Deriving it from content instead (diffing two headers for\n// pixels that vary) yields x=282 y=1856 w=1394 h=871, which is WRONG: that rect\n// spans the laptop's outer panel, covering the bezel on three sides while\n// falling ~29px short at the bottom, so the plate's baked-in screenshot shows\n// as a strip under every site's content.\n//\n// Cross-checked: the photo-to-flat-black transition at the bottom of the plate\n// lands at y=2756, exactly SCREEN.y + SCREEN.h - 1.\n//\n// Re-measure only if the Figma template changes — and if you do, find the bezel.\n\n/** The plate's pixel dimensions — Figma frame 600x800 exported at 4x. */\nexport const CANVAS = { width: 2400, height: 3200 } as const;\n\n/** The MacBook Pro screen the site screenshot is pasted into — the area inside\n * the bezel, so the bezel stays visible around the content exactly as it does\n * in the hand-made headers. Aspect 1.5983 (16:10 to within 0.1%). */\nexport const SCREEN = { x: 302, y: 1913, w: 1349, h: 844 } as const;\n\n/** Where a report-type headline overlay lands on the clean plate.\n *\n * Measured, not guessed: red ink present in the baked maintenance plate but\n * absent from plate-clean spans x=277 y=998 w=1326 h=658 — and\n * headline-maintenance.png is that ink box cropped to its own edges\n * (1328x660, ±1px of anti-aliasing at the threshold), so compositing the\n * asset at this origin reproduces the baked plate's headline exactly. */\nexport const HEADLINE = { x: 277, y: 998 } as const;\n\n/** The band a headline occupies, sized to the WIDEST registered overlay\n * (Testing, 1715x664). Used to detect a header that already carries a baked-in\n * headline, so it can't be stamped a second time. Deliberately clear of the\n * logo above (ends y≈506) and the laptop screen below (starts y=1913), so the\n * only ink that can land here is a headline. */\nexport const HEADLINE_BAND = { x: HEADLINE.x, y: HEADLINE.y, w: 1715, h: 664 } as const;\n\n/** The headline ink colour (brand red), and how far a pixel may drift from it\n * and still count — JPEG re-encoding moves it by a level or two. */\nexport const HEADLINE_INK = { r: 209, g: 34, b: 62 } as const;\n\n/** The per-site domain line, bottom left.\n *\n * Solved against the hand-made reference rather than guessed: rendering\n * \"gallerysonder.com\" with these values reproduces Sonder's original ink box\n * (x=283 y=2963 w=645 h=74) EXACTLY — dx=0 dy=0 dw=0 dh=0. A sweep over sizes\n * 62-88 at weights 300/400 has a single exact solution at size 80 / weight 400.\n *\n * `baseline` is the text baseline in canvas pixels; the ink box sits above it\n * by the cap height and below by the descender. */\nexport const DOMAIN = {\n x: 280,\n baseline: 3020,\n size: 80,\n weight: 400,\n color: \"#747474\",\n} as const;\n\n/** Flat paper tone, used as the compositing background so a transparent or\n * short source can never punch a hole in the plate. */\nexport const PAPER = \"#fcfcfc\" as const;\n"],"mappings":"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","names":["sharp","sharp"]}
@@ -1,10 +1,16 @@
1
1
  import {
2
2
  PRETTIER_FLAG_NOTE,
3
- formatWithPrettier
4
- } from "./chunk-R53VL3RI.js";
3
+ formatWithPrettier,
4
+ resolveTargetPrettier
5
+ } from "./chunk-5CJVUPJU.js";
6
+ import {
7
+ RESTORED_NOTE,
8
+ refusedByGit,
9
+ undoRefusedWrites
10
+ } from "./chunk-4UTJ4G6P.js";
5
11
  import {
6
12
  withRecipe
7
- } from "./chunk-ZQV7UQNE.js";
13
+ } from "./chunk-SQ4FSNYL.js";
8
14
  import {
9
15
  defaultSpawn
10
16
  } from "./chunk-LBYOLBW7.js";
@@ -182,6 +188,12 @@ export default defineConfig({
182
188
  `;
183
189
 
184
190
  // src/recipes/smoke-suite/index.ts
191
+ var PRETTIER_TIMEOUT_MS = 6e4;
192
+ var SMOKE_SUITE_INSTALLED_PATHS = [
193
+ SMOKE_ROUTES_RELATIVE,
194
+ SMOKE_SPEC_RELATIVE,
195
+ PLAYWRIGHT_CONFIG_RELATIVE
196
+ ];
185
197
  async function fileExists(path) {
186
198
  try {
187
199
  await access(path);
@@ -241,6 +253,7 @@ async function smokeSuite(site, deps = { spawn: defaultSpawn }) {
241
253
  apply: async (planned, { commit, cwd }) => {
242
254
  const notes = [];
243
255
  const written = [];
256
+ const before = /* @__PURE__ */ new Map();
244
257
  let routesTemplate = SMOKE_ROUTES_TEMPLATE;
245
258
  if (!await fileExists(join(cwd, SMOKE_ROUTES_RELATIVE))) {
246
259
  const marker = await detectHydrationMarker(cwd);
@@ -264,6 +277,7 @@ async function smokeSuite(site, deps = { spawn: defaultSpawn }) {
264
277
  await mkdir(dirname(target), { recursive: true });
265
278
  await writeFile(target, tmpl, "utf-8");
266
279
  written.push(rel);
280
+ before.set(rel, null);
267
281
  }
268
282
  }
269
283
  const cfgPath = join(cwd, PLAYWRIGHT_CONFIG_RELATIVE);
@@ -271,10 +285,12 @@ async function smokeSuite(site, deps = { spawn: defaultSpawn }) {
271
285
  if (existingCfg === null) {
272
286
  await writeFile(cfgPath, PLAYWRIGHT_CONFIG_TEMPLATE, "utf-8");
273
287
  written.push(PLAYWRIGHT_CONFIG_RELATIVE);
288
+ before.set(PLAYWRIGHT_CONFIG_RELATIVE, null);
274
289
  } else if (existingCfg.includes("REDDOOR_SMOKE_PORT")) {
275
290
  } else if (existingCfg.trim() === PLAYWRIGHT_CONFIG_PRE_R11.trim()) {
276
291
  await writeFile(cfgPath, PLAYWRIGHT_CONFIG_TEMPLATE, "utf-8");
277
292
  written.push(PLAYWRIGHT_CONFIG_RELATIVE);
293
+ before.set(PLAYWRIGHT_CONFIG_RELATIVE, existingCfg);
278
294
  } else {
279
295
  notes.push(
280
296
  "playwright.config.ts exists without REDDOOR_SMOKE_PORT \u2014 add the R1.1 port block manually"
@@ -317,10 +333,23 @@ async function smokeSuite(site, deps = { spawn: defaultSpawn }) {
317
333
  return { kind: "failed", notes: `pnpm install failed (exit ${res.code})` };
318
334
  }
319
335
  }
320
- if (!await formatWithPrettier(deps.spawn, cwd, written)) {
321
- notes.push(PRETTIER_FLAG_NOTE);
336
+ if (written.length > 0) {
337
+ const bin = await (deps.resolvePrettier ?? resolveTargetPrettier)(cwd);
338
+ if (bin === null) {
339
+ notes.push(PRETTIER_FLAG_NOTE);
340
+ } else if (!await formatWithPrettier(deps.spawn, cwd, written, {
341
+ bin,
342
+ timeoutMs: PRETTIER_TIMEOUT_MS
343
+ })) {
344
+ notes.push(PRETTIER_FLAG_NOTE);
345
+ }
322
346
  }
323
347
  await commit("feat: add smoke suite (test:smoke + playwright config + /health smoke routes)");
348
+ const refusal = await refusedByGit(cwd, SMOKE_SUITE_INSTALLED_PATHS, "the smoke suite");
349
+ if (refusal) {
350
+ await undoRefusedWrites(cwd, before, refusal.missing);
351
+ return { kind: "failed", notes: refusal.notes + RESTORED_NOTE };
352
+ }
324
353
  return notes.length > 0 ? { kind: "ok", notes: notes.join("; ") } : { kind: "ok" };
325
354
  }
326
355
  });
@@ -329,4 +358,4 @@ async function smokeSuite(site, deps = { spawn: defaultSpawn }) {
329
358
  export {
330
359
  smokeSuite
331
360
  };
332
- //# sourceMappingURL=chunk-DFKBTBO3.js.map
361
+ //# sourceMappingURL=chunk-NVLREB5R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/recipes/smoke-suite/index.ts","../src/recipes/smoke-suite/template.ts"],"sourcesContent":["import { access, mkdir, readdir, readFile, 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, resolveTargetPrettier, PRETTIER_FLAG_NOTE } from \"../_prettier.js\";\nimport { refusedByGit, undoRefusedWrites, RESTORED_NOTE } from \"../_head-guard.js\";\nimport {\n SMOKE_ROUTES_RELATIVE,\n SMOKE_ROUTES_TEMPLATE,\n SMOKE_SPEC_RELATIVE,\n SMOKE_SPEC_TEMPLATE,\n PLAYWRIGHT_CONFIG_RELATIVE,\n PLAYWRIGHT_CONFIG_TEMPLATE,\n PLAYWRIGHT_CONFIG_PRE_R11,\n} from \"./template.js\";\n\nexport type SmokeSuiteDeps = {\n spawn: SpawnFn;\n /** Resolve the TARGET repo's own prettier. Injected so a test can assert the\n * absolute-path spawn without a populated `node_modules`. */\n resolvePrettier?: (repoRoot: string) => Promise<string | null>;\n};\n\n/** Same budget as prismic-ci and match-harness. Without one the default spawn\n * never detaches and never kills, so a hung formatter runs unbounded. */\nconst PRETTIER_TIMEOUT_MS = 60_000;\n\n/** What \"the smoke suite is installed\" means, as paths a fresh clone must get.\n * package.json is deliberately absent: it is already tracked on every site the\n * recipe runs on, so it can never be the path git silently drops — and it is\n * exactly what makes the drop invisible, because its edit alone produces a\n * commit and therefore an \"applied\". */\nexport const SMOKE_SUITE_INSTALLED_PATHS = [\n SMOKE_ROUTES_RELATIVE,\n SMOKE_SPEC_RELATIVE,\n PLAYWRIGHT_CONFIG_RELATIVE,\n] as const;\n\ntype PackageJson = {\n scripts?: Record<string, string>;\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\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 readIfExists(path: string): Promise<string | null> {\n try {\n return await readFile(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n\n/** The template sentence explaining the default `footer` marker — swapped for a\n * fallback explanation when the marker deviates. Must match template.ts. */\nconst FOOTER_MARKER_SENTENCE =\n \"The hydration marker `footer` is the shared\\n// layout footer, present on every page including the error page.\";\n\nfunction fallbackMarkerSentence(marker: string): string {\n return (\n `The hydration marker \\`${marker}\\` is a\\n` +\n \"// fallback: no <footer> element exists in this site's Svelte source. Add a\\n\" +\n \"// semantic <footer> landmark and point the marker back at it when possible.\"\n );\n}\n\n/** The starter's default `footer` hydration marker needs the element to exist,\n * or EVERY route check false-fails (la-homelessness-initiative red'd the first\n * fleet-smoke run exactly this way). Deviate only on positive evidence of a\n * bespoke build: svelte files exist under src/ and none renders a literal\n * lowercase `<footer` element (a capital-F `<Footer` component tag proves\n * nothing — the element the browser paints lives inside that component). No\n * svelte files at all → no signal → keep the starter default. */\nasync function detectHydrationMarker(cwd: string): Promise<\"footer\" | \"main\" | \"body\"> {\n let entries: string[];\n try {\n entries = (await readdir(join(cwd, \"src\"), { recursive: true })) as string[];\n } catch {\n return \"footer\";\n }\n const svelteFiles = entries.filter((p) => p.endsWith(\".svelte\"));\n if (svelteFiles.length === 0) return \"footer\";\n let sawMain = false;\n for (const rel of svelteFiles) {\n const text = (await readIfExists(join(cwd, \"src\", rel))) ?? \"\";\n if (/<footer[\\s>/]/.test(text)) return \"footer\";\n if (/<main[\\s>/]/.test(text)) sawMain = true;\n }\n return sawMain ? \"main\" : \"body\";\n}\n\n/**\n * Adds the smoke suite to a site: the `tests/smoke/*` specs, a `test:smoke` /\n * `test:unit` script split, `@playwright/test`, and a `playwright.config.ts`\n * that honors `REDDOOR_SMOKE_PORT` (R1.1). Conservative + partial-apply — every\n * file is noop-if-exists, every `package.json` script/dep is add-if-absent, and\n * an existing config that isn't a recognizable shared-base shape is left\n * untouched and flagged for a manual R1.1 patch (never a destructive write).\n * A site with no `package.json` noops (not a node project). Branch+commit per site.\n */\nexport async function smokeSuite(\n site: Site,\n deps: SmokeSuiteDeps = { spawn: defaultSpawn },\n): Promise<RecipeResult> {\n const pkgPath = join(site.path, \"package.json\");\n return withRecipe<{ pkgPath: string; pkg: PackageJson }>({\n name: \"smoke-suite\",\n site,\n plan: async () => {\n // Parse in the read-only plan phase so a missing OR unparseable\n // package.json noops cleanly (spec: structurally unrecognizable → noop),\n // BEFORE apply writes any file. A bad JSON that only threw in apply would\n // leave the (safely-restored) branch as a `failed`, not the graceful noop.\n const raw = await readIfExists(pkgPath);\n if (raw === null) {\n return { kind: \"noop\", notes: \"no package.json (not a node project)\" };\n }\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(raw) as PackageJson;\n } catch {\n return { kind: \"noop\", notes: \"unparseable package.json — skipped\" };\n }\n return { kind: \"apply\", plan: { pkgPath, pkg } };\n },\n apply: async (planned, { commit, cwd }) => {\n const notes: string[] = [];\n // Relative paths this run actually wrote/changed — prettier-formatted to the\n // site's own config before committing (never operator files we left alone).\n const written: string[] = [];\n // ...and what each of those paths held BEFORE this run, so a path git then\n // refuses can be put back exactly as it was (null = did not exist).\n const before = new Map<string, string | null>();\n\n // 1. Spec files — write if absent (never clobber operator edits). The\n // routes manifest ships the starter-verbatim `footer` marker only when\n // the site actually renders one; bespoke builds fall back to `main`,\n // then `body`, so the suite proves paint instead of false-failing.\n let routesTemplate = SMOKE_ROUTES_TEMPLATE;\n if (!(await fileExists(join(cwd, SMOKE_ROUTES_RELATIVE)))) {\n const marker = await detectHydrationMarker(cwd);\n if (marker !== \"footer\") {\n routesTemplate = SMOKE_ROUTES_TEMPLATE.replace(\n 'hydrationMarker: \"footer\"',\n `hydrationMarker: \"${marker}\"`,\n ).replace(FOOTER_MARKER_SENTENCE, fallbackMarkerSentence(marker));\n notes.push(\n `no <footer> element in src/**/*.svelte — hydration marker set to \"${marker}\" ` +\n \"(add a semantic <footer> landmark to restore the default)\",\n );\n }\n }\n const specFiles: Array<[string, string]> = [\n [SMOKE_ROUTES_RELATIVE, routesTemplate],\n [SMOKE_SPEC_RELATIVE, SMOKE_SPEC_TEMPLATE],\n ];\n for (const [rel, tmpl] of specFiles) {\n const target = join(cwd, rel);\n if (!(await fileExists(target))) {\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, tmpl, \"utf-8\");\n written.push(rel);\n before.set(rel, null);\n }\n }\n\n // 2. playwright.config.ts — four cases, never an in-place edit: absent →\n // write R1.1; already has REDDOOR_SMOKE_PORT → leave; exact pre-R1.1\n // shared-base → safe-replace wholesale; anything else → flag for manual.\n const cfgPath = join(cwd, PLAYWRIGHT_CONFIG_RELATIVE);\n const existingCfg = await readIfExists(cfgPath);\n if (existingCfg === null) {\n await writeFile(cfgPath, PLAYWRIGHT_CONFIG_TEMPLATE, \"utf-8\");\n written.push(PLAYWRIGHT_CONFIG_RELATIVE);\n before.set(PLAYWRIGHT_CONFIG_RELATIVE, null);\n } else if (existingCfg.includes(\"REDDOOR_SMOKE_PORT\")) {\n // Already R1.1-aware; leave it.\n } else if (existingCfg.trim() === PLAYWRIGHT_CONFIG_PRE_R11.trim()) {\n await writeFile(cfgPath, PLAYWRIGHT_CONFIG_TEMPLATE, \"utf-8\");\n written.push(PLAYWRIGHT_CONFIG_RELATIVE);\n before.set(PLAYWRIGHT_CONFIG_RELATIVE, existingCfg);\n } else {\n notes.push(\n \"playwright.config.ts exists without REDDOOR_SMOKE_PORT — add the R1.1 port block manually\",\n );\n }\n\n // 3. package.json — add-if-absent scripts + @playwright/test. Only rewrite\n // when something changed, so a re-run of a fully-adopted site noops\n // instead of churning the file. (Parsed in plan; see above.)\n const pkg = planned.pkg;\n let pkgChanged = false;\n pkg.scripts ??= {};\n if (!pkg.scripts[\"test:smoke\"]) {\n pkg.scripts[\"test:smoke\"] = \"playwright install chromium && playwright test\";\n pkgChanged = true;\n }\n // Unit-test scripts (`test:unit` / `test` → \"vitest run\") only when the site\n // actually runs vitest — either it depends on vitest, or it already defines a\n // `test` script. Adding `test: \"vitest run\"` to a site WITHOUT vitest makes\n // the shared CI's \"run the test script if present\" step fail with\n // `vitest: not found` (the whole fleet-smoke batch red'd on exactly this).\n const hasVitest = !!(pkg.devDependencies?.vitest || pkg.dependencies?.vitest);\n const existingTest = pkg.scripts[\"test\"];\n if (hasVitest || existingTest !== undefined) {\n if (!pkg.scripts[\"test:unit\"]) {\n pkg.scripts[\"test:unit\"] = existingTest ?? \"vitest run\";\n pkgChanged = true;\n }\n if (pkg.scripts[\"test\"] === undefined) {\n pkg.scripts[\"test\"] = \"vitest run\";\n pkgChanged = true;\n }\n }\n let depsChanged = false;\n const hasPlaywright =\n !!pkg.devDependencies?.[\"@playwright/test\"] || !!pkg.dependencies?.[\"@playwright/test\"];\n if (!hasPlaywright) {\n pkg.devDependencies ??= {};\n pkg.devDependencies[\"@playwright/test\"] = \"^1.60.0\";\n depsChanged = true;\n pkgChanged = true;\n }\n if (pkgChanged) {\n await writeFile(planned.pkgPath, JSON.stringify(pkg, null, 2) + \"\\n\", \"utf-8\");\n written.push(\"package.json\");\n }\n\n // 4. Install only when a dep was added (streaming, so the operator sees\n // progress; matches bump-deps). A failed install aborts the recipe.\n if (depsChanged) {\n const res = await deps.spawn(\"pnpm\", [\"install\"], { cwd, streaming: true });\n if (res.code !== 0) {\n return { kind: \"failed\", notes: `pnpm install failed (exit ${res.code})` };\n }\n }\n\n // 5. Format everything this run wrote to the site's own prettier config, so\n // fleet CI's format check stays green across heterogeneous configs\n // (quotes/tabs/printWidth vary). Best-effort — a site without prettier\n // just commits unformatted with a flag note.\n //\n // Resolved AFTER step 4 so a site that just gained its devDependencies\n // has a prettier to run — and resolved POSITIVELY, invoked by absolute\n // path. `pnpm exec prettier` would, on a clone with no node_modules\n // (the NORMAL path on `--fleet`, because `prepareFleetSites` clones and\n // never installs), first run a full unbounded `pnpm install` in the\n // client's checkout and then, in a repo whose install left no prettier\n // of its own, fall through to the CALLING repo's binary and exit 0. A\n // clone with no prettier gets the flag note and no spawn at all (#737).\n if (written.length > 0) {\n const bin = await (deps.resolvePrettier ?? resolveTargetPrettier)(cwd);\n if (bin === null) {\n notes.push(PRETTIER_FLAG_NOTE);\n } else if (\n !(await formatWithPrettier(deps.spawn, cwd, written, {\n bin,\n timeoutMs: PRETTIER_TIMEOUT_MS,\n }))\n ) {\n notes.push(PRETTIER_FLAG_NOTE);\n }\n }\n\n // 6. Commit. If nothing was written/changed the commit stages nothing and\n // withRecipe reports noop (the flag note, if any, is still surfaced).\n await commit(\"feat: add smoke suite (test:smoke + playwright config + /health smoke routes)\");\n\n // 7. Did git TAKE the suite? This is the sharpest case of the class\n // (#741): package.json is tracked and always changes, so `commit()`\n // returns a SHA and the result reads \"applied\" even when the two spec\n // files the suite CONSISTS OF were dropped by a `tests/` rule. CI then\n // runs `test:smoke` against a suite that exists on nobody's disk.\n const refusal = await refusedByGit(cwd, SMOKE_SUITE_INSTALLED_PATHS, \"the smoke suite\");\n if (refusal) {\n await undoRefusedWrites(cwd, before, refusal.missing);\n return { kind: \"failed\", notes: refusal.notes + RESTORED_NOTE };\n }\n\n return notes.length > 0 ? { kind: \"ok\", notes: notes.join(\"; \") } : { kind: \"ok\" };\n },\n });\n}\n","// GENERATED verbatim from the reddoor-starter working tree (tests/smoke/*,\n// playwright.config.ts). Byte-fidelity matters — the smoke spec + config are\n// written into each site unchanged. Regenerate with scratchpad/gen-smoke-template.mjs\n// if the starter's smoke suite changes. Do NOT hand-edit the string bodies.\n\nexport const SMOKE_ROUTES_RELATIVE = \"tests/smoke/routes.ts\";\nexport const SMOKE_ROUTES_TEMPLATE = `// Committed per-site smoke manifest. \\`tests/smoke/pages.spec.ts\\` iterates this\n// list, asserting each route returns its expected status and paints a hydration\n// marker with no console errors. This ships the SAFE DEFAULT every reddoor-starter\n// clone inherits; each site's figma-slices build grows the list as real routes\n// land (add \\`{ path, name, hydrationMarker }\\` entries).\n//\n// NOTE on the default \\`/\\` entry: it expects 200, which holds once the clone is\n// wired to a real Prismic repo (getByUID(\"page\",\"home\") resolves). On the bare\n// placeholder starter, \\`/\\` returns 404 (the Prismic lookup throws → error(404)),\n// so the \\`/\\` case only goes green after Prismic is wired — by design, since the\n// gate is about real site health. The hydration marker \\`footer\\` is the shared\n// layout footer, present on every page including the error page.\n\nexport type SmokeRoute = {\n /** Route path to visit, e.g. \"/\" or \"/about\". */\n path: string;\n /** Human-readable label used in the test title. */\n name: string;\n /** CSS selector asserted visible after load (hydration proof). Default: skip. */\n hydrationMarker?: string;\n /** Expected HTTP status. Default: 200. */\n expectStatus?: number;\n};\n\nexport const smokeRoutes: SmokeRoute[] = [\n { path: \"/\", name: \"home\", hydrationMarker: \"footer\" },\n];\n`;\n\nexport const SMOKE_SPEC_RELATIVE = \"tests/smoke/pages.spec.ts\";\nexport const SMOKE_SPEC_TEMPLATE = `import { test, expect, type Page, type ConsoleMessage } from \"@playwright/test\";\nimport { smokeRoutes } from \"./routes\";\n\n// Console messages we don't care about. Add patterns here only after seeing them\n// in CI and confirming they aren't actionable. Patterns are matched against both\n// the message text and the offending resource URL — Chromium's \"Failed to load\n// resource\" text omits the URL, so URL matching catches third-party network noise.\nconst ALLOWED_CONSOLE_PATTERNS: RegExp[] = [\n // Vimeo iframe embeds + their CDN telemetry endpoints occasionally 403 from\n // cloud IPs due to bot detection.\n /vimeo/i,\n // Turnstile (Cloudflare) telemetry occasionally surfaces in console — a 403\n // from a cloud IP, a beacon that didn't land. Console-level ONLY: see\n // ALLOWED_PAGEERROR_PATTERNS below for why this does not extend to throws.\n /turnstile|challenges\\\\.cloudflare/i,\n];\n\n// Uncaught exceptions are held to a stricter bar than console noise, and\n// Turnstile is deliberately NOT on this list. A widget whose sitekey is not\n// allowlisted for the hostname it is served from throws an uncaught\n// \\`TurnstileError: [Cloudflare Turnstile] Error: 110200\\` and mints no token at\n// all — on a \\`Require Turnstile\\` site that buckets 100% of real leads as spam.\n// It shipped that way once (2026-09-04) and this suite watched it happen: the\n// pattern above matched the throw's message and the run stayed green. Telemetry\n// that merely logs is noise; a widget that THROWS is the failure itself.\nconst ALLOWED_PAGEERROR_PATTERNS: RegExp[] = [/vimeo/i];\n\nfunction attachConsoleWatcher(page: Page, extraAllowed: RegExp[] = []) {\n const errors: string[] = [];\n const allowed = [...ALLOWED_CONSOLE_PATTERNS, ...extraAllowed];\n const allowedThrows = [...ALLOWED_PAGEERROR_PATTERNS, ...extraAllowed];\n const isAllowed = (s: string) => !!s && allowed.some((re) => re.test(s));\n const isAllowedThrow = (s: string) => !!s && allowedThrows.some((re) => re.test(s));\n\n page.on(\"console\", (msg: ConsoleMessage) => {\n if (msg.type() !== \"error\") return;\n const text = msg.text();\n const url = msg.location()?.url ?? \"\";\n if (isAllowed(text) || isAllowed(url)) return;\n errors.push(\\`[console.error] \\${text}\\${url ? \\` (\\${url})\\` : \"\"}\\`);\n });\n\n page.on(\"pageerror\", (err) => {\n if (isAllowedThrow(err.message)) return;\n errors.push(\\`[pageerror] \\${err.message}\\`);\n });\n\n return errors;\n}\n\nfor (const route of smokeRoutes) {\n test(\\`\\${route.path} (\\${route.name}) loads with no console errors\\`, async ({\n page,\n }) => {\n const errors = attachConsoleWatcher(page);\n const response = await page.goto(route.path, {\n waitUntil: \"domcontentloaded\",\n });\n expect(response?.status(), \\`HTTP status for \\${route.path}\\`).toBe(\n route.expectStatus ?? 200,\n );\n if (route.hydrationMarker) {\n await expect(\n page.locator(route.hydrationMarker),\n \\`hydration marker \"\\${route.hydrationMarker}\" on \\${route.path}\\`,\n ).toBeVisible();\n }\n expect(errors, \\`console errors on \\${route.path}\\`).toEqual([]);\n });\n}\n\ntest(\"404 page renders the custom error component\", async ({ page }) => {\n // The browser logs a top-level \"Failed to load resource: 404\" for the page\n // itself — expected on a 404 route, not a bug. Allow it here.\n const errors = attachConsoleWatcher(page, [/Failed to load resource.*404/i]);\n const response = await page.goto(\"/this-uid-does-not-exist\", {\n waitUntil: \"domcontentloaded\",\n });\n expect(response?.status()).toBe(404);\n // src/routes/+error.svelte renders \\`<h1>{page.status}</h1>\\` → \"404\".\n await expect(page.getByText(\"404\", { exact: false }).first()).toBeVisible();\n expect(errors).toEqual([]);\n});\n`;\n\nexport const PLAYWRIGHT_CONFIG_RELATIVE = \"playwright.config.ts\";\n\n/** The R1.1 config: reads REDDOOR_SMOKE_PORT and binds --strictPort. Written\n * when a site has no playwright.config.ts. */\nexport const PLAYWRIGHT_CONFIG_TEMPLATE = `import { defineConfig } from \"@playwright/test\";\nimport base from \"@reddoorla/maintenance/configs/playwright-a11y\";\n\n// Emulate reduced motion in tests: instant scrollIntoView (no long animated\n// smooth-scroll that flakes Playwright's actionability checks under parallel\n// load) and view transitions fall back to instant. Pairs with the\n// prefers-reduced-motion gate on scroll-behavior in src/app.css.\n//\n// R1.1 (health-gate): the central \\`smoke\\` audit (reddoor-maintenance\n// src/audits/smoke.ts) allocates a free port and passes it as\n// REDDOOR_SMOKE_PORT so a zombie vite already squatting the default 5173 can't\n// silently hijack the run and green a stale build. When it's set, bind vite to\n// exactly that port with --strictPort (forwarded through \\`npm run vite:dev\\` so\n// it stays portable across pnpm/npm) and aim Playwright's baseURL + readiness\n// probe at it. Unset (local \\`pnpm test:smoke\\`) → the shared base's fixed 5173.\nconst smokePort = process.env.REDDOOR_SMOKE_PORT;\n\nexport default defineConfig({\n ...base,\n use: {\n ...base.use,\n reducedMotion: \"reduce\",\n ...(smokePort ? { baseURL: \\`http://localhost:\\${smokePort}\\` } : {}),\n },\n ...(smokePort\n ? {\n webServer: {\n command: \\`npm run vite:dev -- --port \\${smokePort} --strictPort\\`,\n url: \\`http://localhost:\\${smokePort}/dev/a11y-fixtures\\`,\n // Never reuse: the readiness probe cannot tell the server under test\n // from any other server answering that URL. The shared base does the\n // same, and allocates its own port when REDDOOR_SMOKE_PORT is unset.\n reuseExistingServer: false,\n timeout: 120_000,\n },\n }\n : {}),\n});\n`;\n\n/** The pre-R1.1 shared-base config (no port block). Used ONLY to recognize a\n * site that adopted the shared base before R1.1, so it can be safely replaced\n * wholesale with PLAYWRIGHT_CONFIG_TEMPLATE. Any other existing config is left\n * untouched and flagged for manual patch. */\nexport const PLAYWRIGHT_CONFIG_PRE_R11 = `import { defineConfig } from \"@playwright/test\";\nimport base from \"@reddoorla/maintenance/configs/playwright-a11y\";\n\n// Emulate reduced motion in tests: instant scrollIntoView (no long animated\n// smooth-scroll that flakes Playwright's actionability checks under parallel\n// load) and view transitions fall back to instant. Pairs with the\n// prefers-reduced-motion gate on scroll-behavior in src/app.css.\nexport default defineConfig({\n ...base,\n use: { ...base.use, reducedMotion: \"reduce\" },\n});\n`;\n"],"mappings":"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","names":[]}
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  isProbablyEmail,
7
7
  parseAddresses
8
- } from "./chunk-2ZAZ7ETT.js";
8
+ } from "./chunk-P4A6D6HO.js";
9
9
  import {
10
10
  listAllReports
11
11
  } from "./chunk-F3JWOPTG.js";
@@ -20,7 +20,7 @@ import {
20
20
  import {
21
21
  listWebsites,
22
22
  siteSlug
23
- } from "./chunk-3Z6X3BHU.js";
23
+ } from "./chunk-KN52PM63.js";
24
24
 
25
25
  // src/reports/preflight.ts
26
26
  var OPERATOR_DOMAINS = ["reddoorla.com", "tuckerlemos.com"];
@@ -341,4 +341,4 @@ export {
341
341
  preflight,
342
342
  approveBlockers
343
343
  };
344
- //# sourceMappingURL=chunk-R5IMFAFR.js.map
344
+ //# sourceMappingURL=chunk-OTEVKRAH.js.map
@@ -17,17 +17,17 @@ import {
17
17
  import {
18
18
  renderReportFromRow,
19
19
  requireLighthouse
20
- } from "./chunk-HTABUCF4.js";
20
+ } from "./chunk-Y5BVR4HW.js";
21
21
  import {
22
22
  listWebsites,
23
23
  updateLaunched
24
- } from "./chunk-3Z6X3BHU.js";
24
+ } from "./chunk-KN52PM63.js";
25
25
  import {
26
26
  defaultResendClient
27
27
  } from "./chunk-WEKL2HYJ.js";
28
28
  import {
29
29
  recordFleetEventsBestEffort
30
- } from "./chunk-BI2VI6EM.js";
30
+ } from "./chunk-EQQ4H6L3.js";
31
31
 
32
32
  // src/reports/send/idempotency.ts
33
33
  function isIdempotencyConflict(err) {
@@ -227,4 +227,4 @@ export {
227
227
  parseAddresses,
228
228
  isProbablyEmail
229
229
  };
230
- //# sourceMappingURL=chunk-2ZAZ7ETT.js.map
230
+ //# sourceMappingURL=chunk-P4A6D6HO.js.map
@@ -21,6 +21,10 @@ function siteKey(url) {
21
21
  return raw;
22
22
  }
23
23
  }
24
+ function jsonListOrNull(xs) {
25
+ const cleaned = (xs ?? []).map((x) => x.trim()).filter((x) => x !== "");
26
+ return cleaned.length === 0 ? null : JSON.stringify(cleaned);
27
+ }
24
28
  async function createProspectAudit(db, audit) {
25
29
  const id = newProspectAuditId();
26
30
  const token = generateToken();
@@ -32,7 +36,9 @@ async function createProspectAudit(db, audit) {
32
36
  business: audit.business,
33
37
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
34
38
  status: audit.status ?? "complete",
35
- result_json: audit.resultJson
39
+ result_json: audit.resultJson,
40
+ chosen_terms: jsonListOrNull(audit.chosenTerms),
41
+ chosen_questions: jsonListOrNull(audit.chosenQuestions)
36
42
  }).execute();
37
43
  return { id, token };
38
44
  }
@@ -46,7 +52,9 @@ async function getProspectAuditByToken(db, token) {
46
52
  "result_json",
47
53
  "overrides_json",
48
54
  "edited_at",
49
- "opened_at"
55
+ "opened_at",
56
+ "chosen_terms",
57
+ "chosen_questions"
50
58
  ]).where("token", "=", token).executeTakeFirst();
51
59
  return row ?? null;
52
60
  }
@@ -56,7 +64,17 @@ function clampLimit(limit) {
56
64
  return Math.min(Math.max(n, 1), MAX_RECENT_PROSPECT_AUDITS);
57
65
  }
58
66
  async function listRecentProspectAudits(db, limit) {
59
- return db.selectFrom("prospect_audits").select(["id", "token", "url", "business", "status", "created_at", "edited_at", "opened_at"]).orderBy("created_at", "desc").limit(clampLimit(limit)).execute();
67
+ return db.selectFrom("prospect_audits").select([
68
+ "id",
69
+ "token",
70
+ "url",
71
+ "business",
72
+ "status",
73
+ "created_at",
74
+ "edited_at",
75
+ "opened_at",
76
+ "chosen_terms"
77
+ ]).orderBy("created_at", "desc").limit(clampLimit(limit)).execute();
60
78
  }
61
79
  var OVERRIDES_MAX_LEN = 512e3;
62
80
  function buildOverrideMap(v) {
@@ -92,8 +110,11 @@ async function setProspectAuditOverrides(db, token, overrides) {
92
110
  await db.updateTable("prospect_audits").set({ overrides_json: serialised, edited_at: (/* @__PURE__ */ new Date()).toISOString() }).where("token", "=", token).execute();
93
111
  return { status: "updated", token };
94
112
  }
113
+ var OPENED_AT_COALESCE_MS = 5 * 60 * 1e3;
95
114
  async function touchProspectAuditOpened(db, token) {
96
- await db.updateTable("prospect_audits").set({ opened_at: (/* @__PURE__ */ new Date()).toISOString() }).where("token", "=", token).execute();
115
+ const now = /* @__PURE__ */ new Date();
116
+ const staleBefore = new Date(now.getTime() - OPENED_AT_COALESCE_MS).toISOString();
117
+ await db.updateTable("prospect_audits").set({ opened_at: now.toISOString() }).where("token", "=", token).where((eb) => eb.or([eb("opened_at", "is", null), eb("opened_at", "<", staleBefore)])).execute();
97
118
  }
98
119
 
99
120
  export {
@@ -107,6 +128,7 @@ export {
107
128
  listRecentProspectAudits,
108
129
  OVERRIDES_MAX_LEN,
109
130
  setProspectAuditOverrides,
131
+ OPENED_AT_COALESCE_MS,
110
132
  touchProspectAuditOpened
111
133
  };
112
- //# sourceMappingURL=chunk-XPJQXSC4.js.map
134
+ //# sourceMappingURL=chunk-P7323YVW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/prospect-audits.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport type { Db } from \"./client.js\";\n\n/** 16 random bytes → 22-char base64url string: unguessable, URL-safe. */\nexport function generateToken(): string {\n return randomBytes(16).toString(\"base64url\");\n}\n\n/** Anything not exactly generateToken()-shaped is a probe, not a report. */\nexport function isValidToken(s: string): boolean {\n return /^[A-Za-z0-9_-]{22}$/.test(s);\n}\n\n/** Opaque, collision-free id (mirrors newSubmissionId / newDeadLetterId).\n * crypto is a Node 20 global — no new dep. Nothing reads this format; `token`\n * is the public handle. */\nexport function newProspectAuditId(): string {\n return `pa_${crypto.randomUUID()}`;\n}\n\n/** \"complete\" when every pipeline stage succeeded; \"partial\" when any stage\n * failed or was deliberately skipped (StageResult's `ok: false` covers\n * both — the pipeline doesn't distinguish them at this layer). The report\n * itself already degrades each failed section to \"not measured\"; this is\n * what lets a future dashboard tell complete from partial without\n * deserializing the whole result_json. */\nexport type ProspectAuditStatus = \"complete\" | \"partial\";\n\n/**\n * The lineage handle for a site: one key for every way of writing its address.\n *\n * `url` is kept exactly as it was given, because what we audited is a fact\n * about the run. It is the wrong thing to group by, though, and the corpus\n * already proves it — `beachfrontdentistry.com` and `beachfrontdentistry.com/`\n * are two separate histories in the table today, as are the two spellings of\n * ludlowkingsley.com. Nothing has broken only because nothing yet compares two\n * audits of one site. The moment the report offers a before and an after, a\n * trailing slash turns a returning client into a stranger.\n *\n * Scheme, a leading \"www.\", a trailing slash, a query string and a fragment all\n * go: none of them distinguish one site from another. The path stays, because a\n * site living in a subdirectory genuinely is a different site.\n *\n * Never throws. A key we cannot compute must not take an audit down with it —\n * an unparseable input falls back to its trimmed self, which groups nothing and\n * so is no worse than having no key at all.\n */\nexport function siteKey(url: string): string {\n const raw = url.trim();\n if (raw === \"\") return \"\";\n try {\n const u = new URL(/^[a-z][a-z0-9+.-]*:\\/\\//i.test(raw) ? raw : `https://${raw}`);\n const host = u.hostname.toLowerCase().replace(/^www\\./, \"\");\n const path = u.pathname.replace(/\\/+$/, \"\");\n return `${host}${path}`;\n } catch {\n return raw;\n }\n}\n\nexport type NewProspectAudit = {\n url: string;\n business: string | null;\n resultJson: string;\n /** #676. What the operator chose by hand, if anything. Absent or empty means\n * the audit generated its own — stored as NULL, which is deliberately\n * distinct from an empty operator list. */\n chosenTerms?: string[] | null;\n chosenQuestions?: string[] | null;\n /** Defaults to \"complete\" — the column's own SQL default — for callers\n * (tests, ad-hoc scripts) that don't track per-stage outcomes. The\n * prospect-audit CLI, the only real writer, always computes and passes\n * this explicitly. */\n status?: ProspectAuditStatus;\n};\n\n/** A chosen list as stored: JSON, or NULL when nothing usable was chosen.\n * Blank entries are dropped first, so a textarea holding only whitespace\n * stores NULL rather than an array of empty strings. */\nfunction jsonListOrNull(xs: string[] | null | undefined): string | null {\n const cleaned = (xs ?? []).map((x) => x.trim()).filter((x) => x !== \"\");\n return cleaned.length === 0 ? null : JSON.stringify(cleaned);\n}\n\nexport async function createProspectAudit(\n db: Db,\n audit: NewProspectAudit,\n): Promise<{ id: string; token: string }> {\n const id = newProspectAuditId();\n const token = generateToken();\n await db\n .insertInto(\"prospect_audits\")\n .values({\n id,\n token,\n url: audit.url,\n site_key: siteKey(audit.url),\n business: audit.business,\n created_at: new Date().toISOString(),\n status: audit.status ?? \"complete\",\n result_json: audit.resultJson,\n chosen_terms: jsonListOrNull(audit.chosenTerms),\n chosen_questions: jsonListOrNull(audit.chosenQuestions),\n })\n .execute();\n return { id, token };\n}\n\nexport type ProspectAuditRow = {\n id: string;\n url: string;\n business: string | null;\n created_at: string;\n status: string;\n result_json: string;\n /** The operator's edits as stored JSON, or null when this report has never\n * been edited. Deliberately NOT parsed here: the JSON route will place this\n * string into its response body as-is, exactly as it already does with\n * `result_json`, so the row hands back what was validated on the way in. */\n overrides_json: string | null;\n edited_at: string | null;\n opened_at: string | null;\n /** #676. JSON arrays, or null when the audit chose its own. */\n chosen_terms: string | null;\n chosen_questions: string | null;\n};\n\nexport async function getProspectAuditByToken(\n db: Db,\n token: string,\n): Promise<ProspectAuditRow | null> {\n const row = await db\n .selectFrom(\"prospect_audits\")\n .select([\n \"id\",\n \"url\",\n \"business\",\n \"created_at\",\n \"status\",\n \"result_json\",\n \"overrides_json\",\n \"edited_at\",\n \"opened_at\",\n \"chosen_terms\",\n \"chosen_questions\",\n ])\n .where(\"token\", \"=\", token)\n .executeTakeFirst();\n return row ?? null;\n}\n\n/** The columns the cockpit's /audits listing page shows. Deliberately NOT\n * `result_json` — large, and useless to a list — but `token` IS selected:\n * the listing needs it to build each row's `/r/{token}` link, so it's\n * rendered only into an href, never displayed as a raw value. */\nexport type ProspectAuditListItem = {\n id: string;\n token: string;\n url: string;\n business: string | null;\n status: string;\n created_at: string;\n /** Whether an operator has edited this report, and when someone last opened\n * it — both cheap TEXT columns, and the two facts the listing needs to show\n * an \"edited\" marker without reading `overrides_json`. */\n edited_at: string | null;\n opened_at: string | null;\n /** #676. Present so the listing can mark which audits used chosen terms\n * WITHOUT reading `result_json`, which it deliberately never selects. */\n chosen_terms: string | null;\n};\n\n/** Ceiling on `listRecentProspectAudits`' `limit`, enforced defensively (a\n * caller passing 10_000 must not get 10_000 rows) — well past anything a\n * listing page would ever render on one screen. */\nexport const MAX_RECENT_PROSPECT_AUDITS = 100;\n\n/** Clamp a caller-supplied limit into [1, MAX_RECENT_PROSPECT_AUDITS], with a\n * safe fallback of 1 for anything non-finite (NaN, ±Infinity) rather than\n * letting a bad input reach the query unclamped. */\nfunction clampLimit(limit: number): number {\n const n = Number.isFinite(limit) ? Math.trunc(limit) : 1;\n return Math.min(Math.max(n, 1), MAX_RECENT_PROSPECT_AUDITS);\n}\n\n/** Newest-first audits for the cockpit's /audits listing page, capped both\n * defensively (see clampLimit) and by an index (migration 0010) so the\n * ORDER BY never falls back to a raw scan + a temp b-tree sort — see the\n * EXPLAIN-query-plan gate, tests/db/query-plans.test.ts. */\nexport async function listRecentProspectAudits(\n db: Db,\n limit: number,\n): Promise<ProspectAuditListItem[]> {\n return db\n .selectFrom(\"prospect_audits\")\n .select([\n \"id\",\n \"token\",\n \"url\",\n \"business\",\n \"status\",\n \"created_at\",\n \"edited_at\",\n \"opened_at\",\n \"chosen_terms\",\n ])\n .orderBy(\"created_at\", \"desc\")\n .limit(clampLimit(limit))\n .execute();\n}\n\n/** One replaced string and the generated text it replaced. */\nexport type Override = { original: string; text: string };\nexport type OverrideMap = Record<string, Override>;\n\n/**\n * Ceiling on one report's override map, measured on the SERIALISED JSON rather\n * than on the entry count — one entry holding a megabyte is the case that\n * matters, and the string is what actually gets stored and re-served.\n *\n * The arithmetic, not a round number: the report carries roughly 227 editable\n * strings (the check battery alone, before fixes, claims and goal copy). An\n * operator who replaced every one of them, at around 300 characters of\n * `original` plus 300 of `text` each, writes on the order of 136 KB. This is\n * about four times that worst realistic case, which leaves the honest editor\n * unbounded in practice while still bounding a hand-crafted POST.\n *\n * The bound matters more here than it does for the sibling `setReportCommentary`\n * (2 000 characters): commentary is read when a report is composed, whereas this\n * value is served back inline on every fetch of the report, out of a metered\n * store. 2 288 891 characters were accepted in a single write before this cap.\n */\nexport const OVERRIDES_MAX_LEN = 512_000;\n\nexport type SetOverridesResult =\n | { status: \"updated\"; token: string }\n | { status: \"invalid\"; token: string }\n | { status: \"not-found\"; token: string };\n\n/**\n * The map that will actually be stored — a fresh `{ original, text }` pair per\n * entry, built out of the validated strings themselves — or null for anything\n * that is not a flat map of `{ original, text }` string pairs.\n *\n * Validating and BUILDING are one step deliberately, and this function returns\n * the map rather than a type guard for that reason. Every defect this code has\n * had was the same shape: one object was checked and a different one was handed\n * to `JSON.stringify`. `stringify` honours a `toJSON` method, so the entry\n * `{ original: \"a\", text: \"b\", toJSON: () => 5 }` validated as a pair and stored\n * `{\"k\":5}` — bytes this function never saw, in the one place that claims to be\n * the only gate they can be caught at. Reading each field once and keeping what\n * was read closes the gap by construction: what is stored IS what was approved,\n * and a getter that would answer differently on a second read has no second\n * read. Key order comes straight from `Object.entries`, which is the order\n * `JSON.stringify` would have used, so a round trip is stable and diffs stay\n * legible.\n *\n * EXTRA KEYS ON AN ENTRY ARE DROPPED, and that is a POLICY, not something\n * building the map here forces. A third design satisfies both halves at once:\n * build from the validated fields AND reject any entry carrying a key other than\n * `original` and `text`. Under it the `toJSON` gap, the double-read gap and the\n * extra keys all close by construction exactly as they do now, and a cycle or a\n * BigInt in a third key comes back `invalid` — because the third key is itself\n * the rejection reason, not because `JSON.stringify` threw on it. Dropping and\n * rejecting are therefore independent of building; the choice is DROP, and this\n * is the argument for it rather than a consequence of the rewrite.\n *\n * The consumer reads `original` and `text` and nothing else — the website\n * matches `original` against the generated string before it substitutes `text`.\n * An entry key nobody reads is dead weight re-served on every fetch of the\n * report out of a metered store, which is the same cost `OVERRIDES_MAX_LEN`\n * exists to bound; so the answer to a third key is to stop storing it, not to\n * refuse the operator's words over it. Dropping also keeps the cheap\n * forward-compatible case working: an editor that starts sending a field this\n * schema has not learned yet still saves what was typed.\n *\n * The line that drop stops at is the one this file already draws: it is the\n * ENTRY's own keys that are dropped. An unknown TOP-LEVEL key is a whole\n * override — operator content — and is validated like any other, never silently\n * discarded. The `__proto__` rule below is that same distinction reaching the\n * opposite verdict, for the same reason.\n *\n * The drop IS silent, and that is the cost being accepted: a map is stored that\n * is not byte-identical to the one sent, and the caller is told `updated` all\n * the same. It is acceptable only because what differs is always fields nobody\n * reads — it would not be acceptable for anything a reader could miss.\n *\n * This runs BEFORE the read, the same order `setReportCommentary` uses, so a\n * malformed body costs no round trip.\n *\n * It is also the ONLY place a malformed value can be caught. The JSON route\n * never parses what it serves — today it returns `result_json` untouched, and\n * once it carries overrides too it will place this string into the body the\n * same way, for the reason its own comment gives: parsing and re-serialising\n * there would add a failure mode between the database and the consumer for no\n * gain. Nothing downstream will notice a non-JSON value here; it would simply\n * break every subsequent fetch of that report. So: validate on the way in,\n * once, and never store anything this function has not approved.\n *\n * \"Only place\" is about MALFORMEDNESS, and is not a claim that no other limit is\n * needed. `OVERRIDES_MAX_LEN` bounds the string that gets STORED, never the\n * request body: a POST carrying megabytes of junk in keys nobody reads is parsed\n * in full and walked in full, and then stores 33 bytes and answers `updated`.\n * Bounding the body is the HTTP route's job, with a body limit there — not a\n * second check here.\n *\n * Both null guards are load-bearing, because `typeof null === \"object\"`: the\n * outer one keeps `Object.entries(null)` from throwing, and the inner one keeps\n * a null ENTRY from being read for `.original` — the exact input that crashed\n * the website's own override layer during its review.\n *\n * The PROTOTYPE check is the one that stops this function passing vacuously.\n * `Object.entries()` on a Map, a Set or a Date returns `[]`, so an entry walk\n * never runs its body and approved all three: a Map stored `{}` and silently\n * discarded every override in it, and a Date stored a bare JSON string where a\n * map belongs — each reporting \"updated\" and stamping `edited_at`.\n * Arrays fall out of the same check (`Array.prototype !== Object.prototype`), so\n * there is no separate `Array.isArray` guard on the outer value.\n *\n * `Object.create(null)` IS accepted: a null-prototype object serialises exactly\n * like `{}`, which is the only property this validator's consumer cares about,\n * and refusing it would reject a legitimate map for a reason invisible in JSON.\n *\n * A CROSS-REALM plain object is rejected, though, because its prototype is the\n * OTHER realm's `Object.prototype` and an identity comparison sees a stranger.\n * Nothing produces one today — a route handler's `JSON.parse` builds its objects\n * in this realm — and rejecting is the safe direction, but it would be a mystery\n * to a future worker-thread or `vm` consumer handed a map across the boundary.\n *\n * A `__proto__` KEY is rejected rather than stripped. `JSON.parse` makes it an\n * ordinary own property, and stored verbatim a consumer doing\n * `Object.assign({}, parsed)` does get its prototype replaced — the consumer\n * here is the website, a separate codebase, and this function's whole premise is\n * that nothing downstream will check.\n *\n * It is rejected rather than stripped for the distinction above, not for any\n * general rule against silent drops — such a rule would condemn the entry-key\n * drop in the same breath. `__proto__` at the TOP level is a whole override, so\n * stripping it would discard operator content and still report `updated`, on a\n * report that quietly lost an edit. An entry key is read by nobody, so dropping\n * one loses nothing. One rule, applied consistently, with opposite outcomes for\n * a stated reason.\n */\nfunction buildOverrideMap(v: unknown): OverrideMap | null {\n if (v === null || typeof v !== \"object\") return null;\n const proto = Object.getPrototypeOf(v) as unknown;\n if (proto !== Object.prototype && proto !== null) return null;\n if (Object.hasOwn(v, \"__proto__\")) return null;\n const built: OverrideMap = {};\n for (const [key, entry] of Object.entries(v as Record<string, unknown>)) {\n if (entry === null || typeof entry !== \"object\" || Array.isArray(entry)) return null;\n // One read each, and the values that were read are the values that are kept.\n const { original, text } = entry as Partial<Override>;\n if (typeof original !== \"string\" || typeof text !== \"string\") return null;\n built[key] = { original, text };\n }\n return built;\n}\n\n/**\n * The exact string that will be stored, or null when the input is malformed.\n *\n * Building and serialising are one step, inside one try, because READING the\n * caller's object can throw on an input that is otherwise well-formed. There are\n * three such paths through `buildOverrideMap`, and the try covers all of them:\n *\n * 1. `Object.entries(v)` — an enumerable getter on the map itself, or a\n * Proxy's `ownKeys` / `getOwnPropertyDescriptor` trap.\n * 2. `const { original, text } = entry` — a getter on the ENTRY, which throws\n * after the map has been walked and before either field is validated.\n * 3. `built[key] = { original, text }` — `built` starts as `{}`, so the\n * assignment walks `Object.prototype`, and a setter poisoned there under\n * that key name throws on the WRITE rather than on any read.\n *\n * `Object.getPrototypeOf(v)` and `Object.hasOwn(v, \"__proto__\")` are two more,\n * both trappable on a Proxy and both ahead of the walk.\n *\n * `setProspectAuditOverrides` is typed to return one of three statuses, and an\n * escaping TypeError is a fourth outcome its callers have no branch for. A value\n * that cannot be read is malformed input, not an exceptional condition — and the\n * caller already has a branch for malformed input.\n *\n * `JSON.stringify` itself can no longer throw here: it is handed a plain object\n * of plain `{ original, text }` string pairs, which has no cycle, no BigInt and\n * no `toJSON` to honour. That used to be the other half of this try — a circular\n * reference or a BigInt in a valid-looking entry threw — and both are now simply\n * dropped with the rest of the entry's extra keys. The try stays wrapped around\n * the whole thing anyway: the day a field that is not a string is added, this\n * still returns `invalid` rather than throwing past the return type.\n *\n * The size cap is measured HERE, on the CONSTRUCTED string: that is the string\n * that gets stored and re-served, an entry count says nothing about it, and the\n * caller's object may be arbitrarily larger than the pairs taken out of it.\n * Which is also what it is NOT: a request-body limit. An oversized body is\n * parsed and walked in full before 33 bytes are stored and `updated` returned,\n * so whoever writes the HTTP route still owes it a body limit of its own.\n */\nfunction serialiseOverrides(overrides: OverrideMap): string | null {\n let json: string;\n try {\n const built = buildOverrideMap(overrides);\n if (built === null) return null;\n json = JSON.stringify(built);\n } catch {\n return null;\n }\n return json.length > OVERRIDES_MAX_LEN ? null : json;\n}\n\n/**\n * Replace one report's override map wholesale and stamp `edited_at`.\n *\n * Whole-map rather than per-key: the caller always holds the complete map, and\n * a partial write has no way to express a deletion. An empty map is legitimate\n * and clears every override — an editor that can set but not unset traps the\n * operator in whatever they first typed.\n *\n * What is stored is REBUILT from the validated strings, not the object passed\n * in, so an entry keeps its `original` and its `text` and nothing else: any\n * other key on it is dropped rather than written through. See\n * `buildOverrideMap` for why that is the contract.\n *\n * LAST WRITE WINS, and two writers silently clobber each other: the second\n * write replaces the first's whole map, with nothing to tell either of them it\n * happened. That is accepted rather than overlooked — there is one operator\n * team, and an editor is open for minutes, so the window is small and the cost\n * of losing it is a paragraph retyped. If a second editor ever exists, the\n * column an optimistic check would key on is `edited_at`: read it with the\n * report, send it back with the write, refuse the write if it moved.\n *\n * `result_json` stays untouched. There is still no way to write it.\n */\nexport async function setProspectAuditOverrides(\n db: Db,\n token: string,\n overrides: OverrideMap,\n): Promise<SetOverridesResult> {\n const serialised = serialiseOverrides(overrides);\n if (serialised === null) return { status: \"invalid\", token };\n\n const existing = await getProspectAuditByToken(db, token);\n if (!existing) return { status: \"not-found\", token };\n\n await db\n .updateTable(\"prospect_audits\")\n .set({ overrides_json: serialised, edited_at: new Date().toISOString() })\n .where(\"token\", \"=\", token)\n .execute();\n return { status: \"updated\", token };\n}\n\n/**\n * How stale `opened_at` must be before an open rewrites it.\n *\n * The operator signal is \"has the prospect read this, and roughly when\" — a\n * question five-minute resolution answers exactly as well as per-request\n * resolution does. Everything below the window is refresh noise.\n */\nexport const OPENED_AT_COALESCE_MS = 5 * 60 * 1000;\n\n/**\n * Record that a report was opened by someone who is not editing it.\n *\n * Best effort by contract: the caller must not let a failure here fail the\n * response. Knowing when a prospect last looked is useful; it is not worth\n * turning a read route into one that can 500.\n *\n * COALESCED, and that is a safety property rather than an optimisation. The\n * route that calls this is unauthenticated and rate-limited at 120 req/min per\n * IP, so without the window one token holder can drive ~172 000 writes a day\n * from a single address — into a Turso project shared by the entire fleet,\n * configured `overages: false`, where crossing quota BLOCKS READS AND WRITES\n * for every site at once and where capacity is not alarmed. A read route that\n * amplifies into unbounded writes against that is an outage vector, not a\n * billing detail. With the window, a refresh costs nothing and the amplifier\n * is gone.\n *\n * The window lives in the WHERE clause, not in a read-then-write in the caller:\n * one round trip, and two concurrent opens cannot both decide to write.\n */\nexport async function touchProspectAuditOpened(db: Db, token: string): Promise<void> {\n const now = new Date();\n const staleBefore = new Date(now.getTime() - OPENED_AT_COALESCE_MS).toISOString();\n await db\n .updateTable(\"prospect_audits\")\n .set({ opened_at: now.toISOString() })\n .where(\"token\", \"=\", token)\n .where((eb) => eb.or([eb(\"opened_at\", \"is\", null), eb(\"opened_at\", \"<\", staleBefore)]))\n .execute();\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  hostsMatch
3
- } from "./chunk-QF6IQQFP.js";
3
+ } from "./chunk-Z7FDJ35L.js";
4
4
  import {
5
5
  operatorEmail
6
6
  } from "./chunk-5ML7FMBD.js";
@@ -401,4 +401,4 @@ export {
401
401
  notifySubmission,
402
402
  makeNotify
403
403
  };
404
- //# sourceMappingURL=chunk-4763KQ6O.js.map
404
+ //# sourceMappingURL=chunk-PXRN2MER.js.map