@reddoorla/maintenance 0.95.1 → 0.97.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 (316) hide show
  1. package/README.md +41 -7
  2. package/dist/{announce-CLQYX5I4.js → announce-5ZD2SBGD.js} +39 -31
  3. package/dist/announce-5ZD2SBGD.js.map +1 -0
  4. package/dist/attachments-JFGFQRXQ.js +10 -0
  5. package/dist/{blux-4KNXUP2K.js → blux-NXQLMS4W.js} +1 -1
  6. package/dist/{bump-deps-C5G2EFBB.js → bump-deps-775VLDYB.js} +6 -6
  7. package/dist/{chunk-FUWFLAWQ.js → chunk-3RLDX3Q2.js} +5 -5
  8. package/dist/{chunk-XHHMM5HX.js → chunk-43SW3FGW.js} +63 -1
  9. package/dist/chunk-43SW3FGW.js.map +1 -0
  10. package/dist/{chunk-R53VL3RI.js → chunk-5CJVUPJU.js} +1 -1
  11. package/dist/{chunk-R53VL3RI.js.map → chunk-5CJVUPJU.js.map} +1 -1
  12. package/dist/{chunk-S7RYFUIK.js → chunk-6PGXKN6F.js} +2 -2
  13. package/dist/{chunk-HTABUCF4.js → chunk-6THEZQLA.js} +3 -3
  14. package/dist/chunk-A5QKNOXW.js +72 -0
  15. package/dist/chunk-A5QKNOXW.js.map +1 -0
  16. package/dist/chunk-ASLDCWBF.js +41 -0
  17. package/dist/chunk-ASLDCWBF.js.map +1 -0
  18. package/dist/{chunk-CWXTNPNZ.js → chunk-AYDQQBNG.js} +3 -3
  19. package/dist/chunk-AYDQQBNG.js.map +1 -0
  20. package/dist/{chunk-WVNDEYXL.js → chunk-BINFZQ2T.js} +57 -6
  21. package/dist/chunk-BINFZQ2T.js.map +1 -0
  22. package/dist/{chunk-2NQUCS7O.js → chunk-BIYQN7FH.js} +3 -1
  23. package/dist/{chunk-2NQUCS7O.js.map → chunk-BIYQN7FH.js.map} +1 -1
  24. package/dist/{chunk-YLYXW5PH.js → chunk-CXICCLNM.js} +34 -4
  25. package/dist/chunk-CXICCLNM.js.map +1 -0
  26. package/dist/{chunk-BYGPUUCT.js → chunk-DRNZVORX.js} +66 -30
  27. package/dist/chunk-DRNZVORX.js.map +1 -0
  28. package/dist/{chunk-LM2NHRCJ.js → chunk-DYTYPKFN.js} +4 -4
  29. package/dist/chunk-DYTYPKFN.js.map +1 -0
  30. package/dist/{chunk-DFKBTBO3.js → chunk-EN6CTBUN.js} +35 -6
  31. package/dist/chunk-EN6CTBUN.js.map +1 -0
  32. package/dist/{chunk-5EEMTJMT.js → chunk-FHFXJ3EM.js} +8 -6
  33. package/dist/chunk-FHFXJ3EM.js.map +1 -0
  34. package/dist/{chunk-Z2K4JPQ3.js → chunk-FP6HOW6J.js} +10 -7
  35. package/dist/chunk-FP6HOW6J.js.map +1 -0
  36. package/dist/{chunk-2LYOAB2E.js → chunk-HSMN35LU.js} +2 -2
  37. package/dist/chunk-ICSRWW5Q.js +148 -0
  38. package/dist/chunk-ICSRWW5Q.js.map +1 -0
  39. package/dist/{chunk-FSGICFXB.js → chunk-IDBPII6G.js} +17 -5
  40. package/dist/chunk-IDBPII6G.js.map +1 -0
  41. package/dist/{chunk-C3IQQKXL.js → chunk-IOTOOTII.js} +121 -1
  42. package/dist/{chunk-C3IQQKXL.js.map → chunk-IOTOOTII.js.map} +1 -1
  43. package/dist/{chunk-BCKEP5S6.js → chunk-JHJNNJLG.js} +2 -2
  44. package/dist/{chunk-TO7DJYLZ.js → chunk-L3S3QZXL.js} +178 -39
  45. package/dist/chunk-L3S3QZXL.js.map +1 -0
  46. package/dist/{chunk-WSJ7RO54.js → chunk-LAG6IP7Y.js} +23 -5
  47. package/dist/chunk-LAG6IP7Y.js.map +1 -0
  48. package/dist/{chunk-WJMPCVOR.js → chunk-LAJ3FF54.js} +12 -4
  49. package/dist/chunk-LAJ3FF54.js.map +1 -0
  50. package/dist/{chunk-2ZAZ7ETT.js → chunk-LDHRIQUW.js} +32 -18
  51. package/dist/chunk-LDHRIQUW.js.map +1 -0
  52. package/dist/{chunk-6R56OKJZ.js → chunk-LF2TJN4X.js} +2 -2
  53. package/dist/chunk-LF2TJN4X.js.map +1 -0
  54. package/dist/{chunk-ZQV7UQNE.js → chunk-LUF52EIY.js} +2 -2
  55. package/dist/{chunk-5UINJNAJ.js → chunk-LZMAG73U.js} +34 -21
  56. package/dist/chunk-LZMAG73U.js.map +1 -0
  57. package/dist/{chunk-VSGMCCGQ.js → chunk-MMSMZC7S.js} +2 -2
  58. package/dist/chunk-MVKNU4QD.js +37 -0
  59. package/dist/chunk-MVKNU4QD.js.map +1 -0
  60. package/dist/{chunk-XPJQXSC4.js → chunk-P7323YVW.js} +27 -5
  61. package/dist/chunk-P7323YVW.js.map +1 -0
  62. package/dist/{chunk-KJRVMMM3.js → chunk-PARKXDXI.js} +36 -5
  63. package/dist/chunk-PARKXDXI.js.map +1 -0
  64. package/dist/{chunk-4763KQ6O.js → chunk-PXRN2MER.js} +2 -2
  65. package/dist/{chunk-WMBLR5VA.js → chunk-PZZ6HM5T.js} +3 -3
  66. package/dist/{chunk-TR3HFH6J.js → chunk-QFSL2L2B.js} +67 -10
  67. package/dist/chunk-QFSL2L2B.js.map +1 -0
  68. package/dist/chunk-RQJFGK3A.js +51 -0
  69. package/dist/chunk-RQJFGK3A.js.map +1 -0
  70. package/dist/{chunk-3Z6X3BHU.js → chunk-TC4GANMX.js} +53 -150
  71. package/dist/chunk-TC4GANMX.js.map +1 -0
  72. package/dist/{chunk-F3JWOPTG.js → chunk-TEMQPSCM.js} +45 -105
  73. package/dist/chunk-TEMQPSCM.js.map +1 -0
  74. package/dist/chunk-TJLKWQOX.js +52 -0
  75. package/dist/chunk-TJLKWQOX.js.map +1 -0
  76. package/dist/{chunk-CJ2RMTT7.js → chunk-TV76A6GN.js} +2 -2
  77. package/dist/{chunk-KZRR4NFG.js → chunk-TVZ6QCOE.js} +3 -3
  78. package/dist/{chunk-BVEDKMNG.js → chunk-U2ZI6G7H.js} +29 -11
  79. package/dist/chunk-U2ZI6G7H.js.map +1 -0
  80. package/dist/chunk-UPE6D4AT.js +69 -0
  81. package/dist/chunk-UPE6D4AT.js.map +1 -0
  82. package/dist/{chunk-R5IMFAFR.js → chunk-VMGHQUTV.js} +7 -16
  83. package/dist/chunk-VMGHQUTV.js.map +1 -0
  84. package/dist/chunk-VQMAAQ5M.js +53 -0
  85. package/dist/chunk-VQMAAQ5M.js.map +1 -0
  86. package/dist/{chunk-AVEQUR44.js → chunk-WMF5S2MB.js} +60 -17
  87. package/dist/chunk-WMF5S2MB.js.map +1 -0
  88. package/dist/{chunk-SVAAONYB.js → chunk-WQLNUEZL.js} +12 -2
  89. package/dist/{chunk-SVAAONYB.js.map → chunk-WQLNUEZL.js.map} +1 -1
  90. package/dist/{chunk-XN37VTDB.js → chunk-X56LAKPR.js} +3 -3
  91. package/dist/chunk-X56LAKPR.js.map +1 -0
  92. package/dist/{chunk-ZCL7C5LH.js → chunk-Y6FCXRIC.js} +1661 -387
  93. package/dist/chunk-Y6FCXRIC.js.map +1 -0
  94. package/dist/{chunk-2SLED3NB.js → chunk-YDZ7XGPZ.js} +1 -1
  95. package/dist/{chunk-2SLED3NB.js.map → chunk-YDZ7XGPZ.js.map} +1 -1
  96. package/dist/{chunk-IY6256GK.js → chunk-YJBSB4LP.js} +6 -1
  97. package/dist/chunk-YJBSB4LP.js.map +1 -0
  98. package/dist/{chunk-BI2VI6EM.js → chunk-YXWBM26P.js} +2 -2
  99. package/dist/{chunk-QF6IQQFP.js → chunk-Z7FDJ35L.js} +18 -12
  100. package/dist/chunk-Z7FDJ35L.js.map +1 -0
  101. package/dist/cli/bin.js +111 -51
  102. package/dist/cli/bin.js.map +1 -1
  103. package/dist/cli/commands/audit.d.ts +33 -3
  104. package/dist/cli/commands/audit.js +28 -25
  105. package/dist/cli/commands/audit.js.map +1 -1
  106. package/dist/client-DGTO65WB.js +11 -0
  107. package/dist/configs/playwright-a11y.d.ts +11 -1
  108. package/dist/configs/playwright-a11y.js +3 -1
  109. package/dist/{convert-to-pnpm-YTFREUJK.js → convert-to-pnpm-KLWA53N5.js} +6 -6
  110. package/dist/{db-XKZYDCKM.js → db-II5WAK4D.js} +45 -20
  111. package/dist/db-II5WAK4D.js.map +1 -0
  112. package/dist/deadletter-G4M3HSVM.js +17 -0
  113. package/dist/{digest-7UBGUBRK.js → digest-CWKK2DMR.js} +56 -32
  114. package/dist/digest-CWKK2DMR.js.map +1 -0
  115. package/dist/{digest-collectors-DJFN5ZDK.js → digest-collectors-CYTN2EUX.js} +17 -12
  116. package/dist/{digest-state-3KHZF7GD.js → digest-state-ZDPKR7MW.js} +18 -2
  117. package/dist/digest-state-ZDPKR7MW.js.map +1 -0
  118. package/dist/ensure-site-2QZHFVQ3.js +70 -0
  119. package/dist/ensure-site-2QZHFVQ3.js.map +1 -0
  120. package/dist/ensure-site-JEKV2ODE.js +105 -0
  121. package/dist/ensure-site-JEKV2ODE.js.map +1 -0
  122. package/dist/{ensure-site-OAAE5YE2.js → ensure-site-OO4UPWNQ.js} +49 -44
  123. package/dist/ensure-site-OO4UPWNQ.js.map +1 -0
  124. package/dist/fleet-events-CkOZpMVC.d.ts +435 -0
  125. package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-RJROYXKO.js} +19 -7
  126. package/dist/{forms-notify-target-UC6PF3AE.js → forms-notify-target-X6UEOI2M.js} +19 -15
  127. package/dist/forms-notify-target-X6UEOI2M.js.map +1 -0
  128. package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
  129. package/dist/{github-signals-TX5FPYP6.js → github-signals-QIRTHHET.js} +22 -17
  130. package/dist/github-signals-QIRTHHET.js.map +1 -0
  131. package/dist/{header-image-PSPT7CUM.js → header-image-H7ZJAV26.js} +15 -18
  132. package/dist/header-image-H7ZJAV26.js.map +1 -0
  133. package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-BJ6BKRHF.js} +8 -7
  134. package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-BJ6BKRHF.js.map} +1 -1
  135. package/dist/{health-mirror-XJAKA3EY.js → health-mirror-KU3G7V2Y.js} +10 -10
  136. package/dist/{import-airtable-TAKSN4NV.js → import-airtable-UBF5GYAL.js} +3 -3
  137. package/dist/index.d.ts +158 -419
  138. package/dist/index.js +91 -46
  139. package/dist/index.js.map +1 -1
  140. package/dist/{init-FFE4FCO2.js → init-GOWDA5QS.js} +19 -18
  141. package/dist/{init-FFE4FCO2.js.map → init-GOWDA5QS.js.map} +1 -1
  142. package/dist/{launch-UD36AHUN.js → launch-7N3X2MXB.js} +229 -79
  143. package/dist/launch-7N3X2MXB.js.map +1 -0
  144. package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
  145. package/dist/lighthouse-airtable-M45OPVRV.js +14 -0
  146. package/dist/match-harness-GBCDSVPS.js +417 -0
  147. package/dist/match-harness-GBCDSVPS.js.map +1 -0
  148. package/dist/migrate-XZ7U5DFC.js +7 -0
  149. package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +4 -4
  150. package/dist/{onboard-TM2KESGF.js → onboard-JQQIMQW7.js} +6 -6
  151. package/dist/{orchestrate-HYVQPRCE.js → orchestrate-L2A5BZFY.js} +13 -10
  152. package/dist/parity-2JGVUCZD.js +14 -0
  153. package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
  154. package/dist/{preflight-RYIP54NY.js → preflight-O7Q6FWHU.js} +35 -11
  155. package/dist/preflight-O7Q6FWHU.js.map +1 -0
  156. package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-NAPWOXXD.js} +39 -9
  157. package/dist/prismic-ci-NAPWOXXD.js.map +1 -0
  158. package/dist/{prismic-models-P6FWWVRS.js → prismic-models-LL6ORTQ2.js} +20 -17
  159. package/dist/prismic-models-LL6ORTQ2.js.map +1 -0
  160. package/dist/prospect/types.d.ts +72 -0
  161. package/dist/prospect/types.js +1 -1
  162. package/dist/{prospect-audit-PRHU53HS.js → prospect-audit-FMPGQIRQ.js} +16 -7
  163. package/dist/prospect-audit-FMPGQIRQ.js.map +1 -0
  164. package/dist/{prospect-audits-R3LWF6U7.js → prospect-audits-IZVQY3OQ.js} +4 -2
  165. package/dist/protection-audit-YXJKFU6O.js +413 -0
  166. package/dist/protection-audit-YXJKFU6O.js.map +1 -0
  167. package/dist/recipes/sync-configs.js +3 -3
  168. package/dist/{render-YGNGTX3W.js → render-MHUUHPLN.js} +36 -9
  169. package/dist/render-MHUUHPLN.js.map +1 -0
  170. package/dist/{render-from-row-IFSBHWDH.js → render-from-row-75JTED2Z.js} +6 -4
  171. package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-2IHAZDVA.js} +14 -9
  172. package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-2IHAZDVA.js.map} +1 -1
  173. package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
  174. package/dist/replay-4FVFOMHH.js.map +1 -0
  175. package/dist/{report-LQS4ECTM.js → report-IAHXN5CW.js} +103 -57
  176. package/dist/report-IAHXN5CW.js.map +1 -0
  177. package/dist/{report-mirror-HDTOJE64.js → report-mirror-4QM57LMS.js} +34 -12
  178. package/dist/report-mirror-4QM57LMS.js.map +1 -0
  179. package/dist/roster-GDNSIRAK.js +19 -0
  180. package/dist/roster-GDNSIRAK.js.map +1 -0
  181. package/dist/{schema-A4WCPIF4.js → schema-KWUQEZ5O.js} +1 -1
  182. package/dist/schema-KWUQEZ5O.js.map +1 -0
  183. package/dist/{self-updating-MWXIZLL7.js → self-updating-ZUWPGYUP.js} +8 -7
  184. package/dist/{self-updating-MWXIZLL7.js.map → self-updating-ZUWPGYUP.js.map} +1 -1
  185. package/dist/{selftest-VXTHRPRN.js → selftest-G4W3LIBQ.js} +21 -17
  186. package/dist/{selftest-VXTHRPRN.js.map → selftest-G4W3LIBQ.js.map} +1 -1
  187. package/dist/site-create-ZU3ACBTO.js +45 -0
  188. package/dist/site-create-ZU3ACBTO.js.map +1 -0
  189. package/dist/site-lookup-DTOLJ4EA.js +27 -0
  190. package/dist/site-lookup-DTOLJ4EA.js.map +1 -0
  191. package/dist/{site-mirror-WON4FTJ2.js → site-mirror-WR7ACXOV.js} +26 -12
  192. package/dist/site-mirror-WR7ACXOV.js.map +1 -0
  193. package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-WAUPI6EJ.js} +8 -7
  194. package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-WAUPI6EJ.js.map} +1 -1
  195. package/dist/{submissions-OVZPVBYG.js → submissions-2SHCFOKD.js} +12 -7
  196. package/dist/submissions-2SHCFOKD.js.map +1 -0
  197. package/dist/{submissions-3I3GAQAX.js → submissions-JDZP35JM.js} +49 -9
  198. package/dist/submissions-JDZP35JM.js.map +1 -0
  199. package/dist/{svelte-codemods-WSV2FGNG.js → svelte-codemods-VOLOKKFI.js} +6 -6
  200. package/dist/{sync-K3PKHVLH.js → sync-F4USL4K5.js} +6 -6
  201. package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-OXSWKVZ2.js} +6 -6
  202. package/dist/turso-KQKBBQCK.js +24 -0
  203. package/dist/turso-KQKBBQCK.js.map +1 -0
  204. package/dist/{upgrade-VTD6V4HS.js → upgrade-EVAL3JTO.js} +6 -6
  205. package/dist/util/git.d.ts +49 -10
  206. package/dist/util/git.js +1 -1
  207. package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
  208. package/dist/{websites-TB4TCAWM.js → websites-Y6FN2XFD.js} +27 -20
  209. package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-TI3JMWSY.js} +7 -5
  210. package/package.json +3 -2
  211. package/dist/airtable-JGCPS7JH.js +0 -9
  212. package/dist/announce-CLQYX5I4.js.map +0 -1
  213. package/dist/attachments-NLZIJ5NU.js +0 -9
  214. package/dist/chunk-2ZAZ7ETT.js.map +0 -1
  215. package/dist/chunk-3Z6X3BHU.js.map +0 -1
  216. package/dist/chunk-5EEMTJMT.js.map +0 -1
  217. package/dist/chunk-5GUJ36TL.js +0 -53
  218. package/dist/chunk-5GUJ36TL.js.map +0 -1
  219. package/dist/chunk-5UINJNAJ.js.map +0 -1
  220. package/dist/chunk-6R56OKJZ.js.map +0 -1
  221. package/dist/chunk-AVEQUR44.js.map +0 -1
  222. package/dist/chunk-BVEDKMNG.js.map +0 -1
  223. package/dist/chunk-BYGPUUCT.js.map +0 -1
  224. package/dist/chunk-CWXTNPNZ.js.map +0 -1
  225. package/dist/chunk-DFKBTBO3.js.map +0 -1
  226. package/dist/chunk-F3JWOPTG.js.map +0 -1
  227. package/dist/chunk-FSGICFXB.js.map +0 -1
  228. package/dist/chunk-GASBX52O.js +0 -12
  229. package/dist/chunk-GASBX52O.js.map +0 -1
  230. package/dist/chunk-IY6256GK.js.map +0 -1
  231. package/dist/chunk-KJRVMMM3.js.map +0 -1
  232. package/dist/chunk-LM2NHRCJ.js.map +0 -1
  233. package/dist/chunk-QF6IQQFP.js.map +0 -1
  234. package/dist/chunk-R5IMFAFR.js.map +0 -1
  235. package/dist/chunk-TO7DJYLZ.js.map +0 -1
  236. package/dist/chunk-TR3HFH6J.js.map +0 -1
  237. package/dist/chunk-WJMPCVOR.js.map +0 -1
  238. package/dist/chunk-WSJ7RO54.js.map +0 -1
  239. package/dist/chunk-WVNDEYXL.js.map +0 -1
  240. package/dist/chunk-XHHMM5HX.js.map +0 -1
  241. package/dist/chunk-XN37VTDB.js.map +0 -1
  242. package/dist/chunk-XPJQXSC4.js.map +0 -1
  243. package/dist/chunk-YFAXOD7Z.js +0 -42
  244. package/dist/chunk-YFAXOD7Z.js.map +0 -1
  245. package/dist/chunk-YLYXW5PH.js.map +0 -1
  246. package/dist/chunk-Z2K4JPQ3.js.map +0 -1
  247. package/dist/chunk-ZCL7C5LH.js.map +0 -1
  248. package/dist/client-G2WGWPCA.js +0 -11
  249. package/dist/db-XKZYDCKM.js.map +0 -1
  250. package/dist/digest-7UBGUBRK.js.map +0 -1
  251. package/dist/digest-state-3KHZF7GD.js.map +0 -1
  252. package/dist/ensure-site-OAAE5YE2.js.map +0 -1
  253. package/dist/fleet-events-cF2Om88w.d.ts +0 -24
  254. package/dist/forms-notify-target-UC6PF3AE.js.map +0 -1
  255. package/dist/github-signals-TX5FPYP6.js.map +0 -1
  256. package/dist/header-image-PSPT7CUM.js.map +0 -1
  257. package/dist/launch-UD36AHUN.js.map +0 -1
  258. package/dist/lighthouse-airtable-YMH4UA7R.js +0 -12
  259. package/dist/match-harness-QRG6XZAR.js +0 -70
  260. package/dist/match-harness-QRG6XZAR.js.map +0 -1
  261. package/dist/migrate-Y7R4CESL.js +0 -7
  262. package/dist/parity-CAJ477PS.js +0 -14
  263. package/dist/preflight-RYIP54NY.js.map +0 -1
  264. package/dist/prismic-ci-6GKKZGB5.js.map +0 -1
  265. package/dist/prismic-models-P6FWWVRS.js.map +0 -1
  266. package/dist/prospect-audit-PRHU53HS.js.map +0 -1
  267. package/dist/protection-audit-SJO53MEQ.js +0 -168
  268. package/dist/protection-audit-SJO53MEQ.js.map +0 -1
  269. package/dist/render-YGNGTX3W.js.map +0 -1
  270. package/dist/replay-PCSQOGCZ.js.map +0 -1
  271. package/dist/report-LQS4ECTM.js.map +0 -1
  272. package/dist/report-mirror-HDTOJE64.js.map +0 -1
  273. package/dist/schema-A4WCPIF4.js.map +0 -1
  274. package/dist/site-mirror-WON4FTJ2.js.map +0 -1
  275. package/dist/submissions-3I3GAQAX.js.map +0 -1
  276. package/dist/submissions-OVZPVBYG.js.map +0 -1
  277. /package/dist/{airtable-JGCPS7JH.js.map → attachments-JFGFQRXQ.js.map} +0 -0
  278. /package/dist/{blux-4KNXUP2K.js.map → blux-NXQLMS4W.js.map} +0 -0
  279. /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-775VLDYB.js.map} +0 -0
  280. /package/dist/{chunk-FUWFLAWQ.js.map → chunk-3RLDX3Q2.js.map} +0 -0
  281. /package/dist/{chunk-S7RYFUIK.js.map → chunk-6PGXKN6F.js.map} +0 -0
  282. /package/dist/{chunk-HTABUCF4.js.map → chunk-6THEZQLA.js.map} +0 -0
  283. /package/dist/{chunk-2LYOAB2E.js.map → chunk-HSMN35LU.js.map} +0 -0
  284. /package/dist/{chunk-BCKEP5S6.js.map → chunk-JHJNNJLG.js.map} +0 -0
  285. /package/dist/{chunk-ZQV7UQNE.js.map → chunk-LUF52EIY.js.map} +0 -0
  286. /package/dist/{chunk-VSGMCCGQ.js.map → chunk-MMSMZC7S.js.map} +0 -0
  287. /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
  288. /package/dist/{chunk-WMBLR5VA.js.map → chunk-PZZ6HM5T.js.map} +0 -0
  289. /package/dist/{chunk-CJ2RMTT7.js.map → chunk-TV76A6GN.js.map} +0 -0
  290. /package/dist/{chunk-KZRR4NFG.js.map → chunk-TVZ6QCOE.js.map} +0 -0
  291. /package/dist/{chunk-BI2VI6EM.js.map → chunk-YXWBM26P.js.map} +0 -0
  292. /package/dist/{attachments-NLZIJ5NU.js.map → client-DGTO65WB.js.map} +0 -0
  293. /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-KLWA53N5.js.map} +0 -0
  294. /package/dist/{client-G2WGWPCA.js.map → deadletter-G4M3HSVM.js.map} +0 -0
  295. /package/dist/{digest-collectors-DJFN5ZDK.js.map → digest-collectors-CYTN2EUX.js.map} +0 -0
  296. /package/dist/{fleet-state-UEVZ3X3J.js.map → fleet-state-RJROYXKO.js.map} +0 -0
  297. /package/dist/{freeze-A6MUCZU4.js.map → freeze-GUQ43EAU.js.map} +0 -0
  298. /package/dist/{health-mirror-XJAKA3EY.js.map → health-mirror-KU3G7V2Y.js.map} +0 -0
  299. /package/dist/{import-airtable-TAKSN4NV.js.map → import-airtable-UBF5GYAL.js.map} +0 -0
  300. /package/dist/{lighthouse-VHI77PFB.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
  301. /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-airtable-M45OPVRV.js.map} +0 -0
  302. /package/dist/{migrate-Y7R4CESL.js.map → migrate-XZ7U5DFC.js.map} +0 -0
  303. /package/dist/{notify-IISTHNBR.js.map → notify-K3WVH3DM.js.map} +0 -0
  304. /package/dist/{onboard-TM2KESGF.js.map → onboard-JQQIMQW7.js.map} +0 -0
  305. /package/dist/{orchestrate-HYVQPRCE.js.map → orchestrate-L2A5BZFY.js.map} +0 -0
  306. /package/dist/{parity-CAJ477PS.js.map → parity-2JGVUCZD.js.map} +0 -0
  307. /package/dist/{pipeline-YROAY6DT.js.map → pipeline-UUPXOS2J.js.map} +0 -0
  308. /package/dist/{prospect-audits-R3LWF6U7.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
  309. /package/dist/{render-from-row-IFSBHWDH.js.map → render-from-row-75JTED2Z.js.map} +0 -0
  310. /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-VOLOKKFI.js.map} +0 -0
  311. /package/dist/{sync-K3PKHVLH.js.map → sync-F4USL4K5.js.map} +0 -0
  312. /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-OXSWKVZ2.js.map} +0 -0
  313. /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-EVAL3JTO.js.map} +0 -0
  314. /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
  315. /package/dist/{websites-TB4TCAWM.js.map → websites-Y6FN2XFD.js.map} +0 -0
  316. /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → write-audits-to-airtable-TI3JMWSY.js.map} +0 -0
@@ -1,19 +1,48 @@
1
+ import {
2
+ parseAutoEvidence,
3
+ toReportType
4
+ } from "./chunk-ASLDCWBF.js";
1
5
  import {
2
6
  ALL_CHECKLIST_FIELDS
3
7
  } from "./chunk-VAD43MOR.js";
8
+ import {
9
+ skipsAirtableShadow
10
+ } from "./chunk-RQJFGK3A.js";
4
11
 
5
- // src/reports/airtable/reports.ts
6
- var REPORTS_TABLE = "Reports";
7
- var REPORT_TYPES = ["Maintenance", "Testing", "Launch", "Announcement"];
8
- function toReportType(raw) {
9
- if (raw && REPORT_TYPES.includes(raw)) return raw;
10
- if (raw)
11
- console.warn(`[reports] unknown Report type ${JSON.stringify(raw)} \u2014 treating as Maintenance`);
12
- return "Maintenance";
12
+ // src/reports/draft-fields.ts
13
+ function ymd(d) {
14
+ return d.toISOString().slice(0, 10);
13
15
  }
14
- function isPendingApproval(r) {
15
- return r.draftReady && !r.approvedToSend && r.sentAt === null;
16
+ function draftFields(input) {
17
+ const fields = {
18
+ "Report ID": input.reportId,
19
+ Site: [input.siteId],
20
+ "Report type": input.reportType,
21
+ "Period start": ymd(input.periodStart),
22
+ "Period end": ymd(input.periodEnd),
23
+ "Completed on": ymd(input.completedOn),
24
+ "Lighthouse \u2014 Performance": input.lighthouse.performance,
25
+ "Lighthouse \u2014 Accessibility": input.lighthouse.accessibility,
26
+ "Lighthouse \u2014 Best Practices": input.lighthouse.bestPractices,
27
+ "Lighthouse \u2014 SEO": input.lighthouse.seo,
28
+ "Delivery status": "pending"
29
+ };
30
+ if (input.lastTestedDate) fields["Last tested date"] = ymd(input.lastTestedDate);
31
+ if (input.gaUsersCurrent !== void 0) fields["GA users (period)"] = input.gaUsersCurrent;
32
+ if (input.gaUsersPrevious !== void 0) fields["GA users (prev period)"] = input.gaUsersPrevious;
33
+ if (input.searchFoundPage1 !== void 0) fields["Search found page 1"] = input.searchFoundPage1;
34
+ if (input.searchPosition !== void 0) fields["Search position"] = input.searchPosition;
35
+ if (input.period !== void 0) fields["Period"] = input.period;
36
+ if (input.subjectOverride !== void 0) fields["Subject override"] = input.subjectOverride;
37
+ for (const field of input.checklistTicks ?? []) fields[field] = true;
38
+ if (input.autoEvidence && Object.keys(input.autoEvidence).length > 0) {
39
+ fields["Checklist auto-evidence"] = JSON.stringify(input.autoEvidence);
40
+ }
41
+ return fields;
16
42
  }
43
+
44
+ // src/reports/airtable/reports.ts
45
+ var REPORTS_TABLE = "Reports";
17
46
  function mapRow(rec) {
18
47
  const f = rec.fields;
19
48
  const linkSites = f["Site"] ?? [];
@@ -51,29 +80,6 @@ function mapRow(rec) {
51
80
  overrideAt: f["Override at"] ?? null
52
81
  };
53
82
  }
54
- function parseAutoEvidence(raw) {
55
- if (typeof raw !== "string" || !raw.trim()) return null;
56
- let parsed;
57
- try {
58
- parsed = JSON.parse(raw);
59
- } catch {
60
- return null;
61
- }
62
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
63
- const out = {};
64
- for (const [field, v] of Object.entries(parsed)) {
65
- if (!v || typeof v !== "object") continue;
66
- const o = v;
67
- if (o.result !== "pass" && o.result !== "fail" && o.result !== "unknown" && o.result !== "n/a")
68
- continue;
69
- out[field] = {
70
- result: o.result,
71
- checkedAt: typeof o.checkedAt === "string" ? o.checkedAt : null,
72
- note: typeof o.note === "string" ? o.note : ""
73
- };
74
- }
75
- return Object.keys(out).length > 0 ? out : null;
76
- }
77
83
  function lighthouseFromFields(f) {
78
84
  const p = f["Lighthouse \u2014 Performance"];
79
85
  const a = f["Lighthouse \u2014 Accessibility"];
@@ -83,48 +89,12 @@ function lighthouseFromFields(f) {
83
89
  return null;
84
90
  return { performance: p, accessibility: a, bestPractices: b, seo: s };
85
91
  }
86
- function ymd(d) {
87
- return d.toISOString().slice(0, 10);
88
- }
89
- function escapeFormulaString(s) {
90
- return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
91
- }
92
- async function createDraft(base, input, mirror) {
93
- const fields = {
94
- "Report ID": input.reportId,
95
- Site: [input.siteId],
96
- "Report type": input.reportType,
97
- "Period start": ymd(input.periodStart),
98
- "Period end": ymd(input.periodEnd),
99
- "Completed on": ymd(input.completedOn),
100
- "Lighthouse \u2014 Performance": input.lighthouse.performance,
101
- "Lighthouse \u2014 Accessibility": input.lighthouse.accessibility,
102
- "Lighthouse \u2014 Best Practices": input.lighthouse.bestPractices,
103
- "Lighthouse \u2014 SEO": input.lighthouse.seo,
104
- "Delivery status": "pending"
105
- };
106
- if (input.lastTestedDate) fields["Last tested date"] = ymd(input.lastTestedDate);
107
- if (input.gaUsersCurrent !== void 0) fields["GA users (period)"] = input.gaUsersCurrent;
108
- if (input.gaUsersPrevious !== void 0) fields["GA users (prev period)"] = input.gaUsersPrevious;
109
- if (input.searchFoundPage1 !== void 0) fields["Search found page 1"] = input.searchFoundPage1;
110
- if (input.searchPosition !== void 0) fields["Search position"] = input.searchPosition;
111
- if (input.period !== void 0) fields["Period"] = input.period;
112
- if (input.subjectOverride !== void 0) fields["Subject override"] = input.subjectOverride;
113
- for (const field of input.checklistTicks ?? []) fields[field] = true;
114
- if (input.autoEvidence && Object.keys(input.autoEvidence).length > 0) {
115
- fields["Checklist auto-evidence"] = JSON.stringify(input.autoEvidence);
116
- }
117
- const created = await base(REPORTS_TABLE).create([{ fields }]);
118
- const rec = created[0];
119
- if (!rec) throw new Error("Airtable create returned no records");
120
- const raw = { id: rec.id, fields: rec.fields };
121
- if (mirror) await mirror(raw);
122
- return mapRow(raw);
123
- }
124
92
  async function setDraftReady(base, recordId, ready) {
93
+ if (skipsAirtableShadow("setDraftReady", recordId)) return;
125
94
  await base(REPORTS_TABLE).update([{ id: recordId, fields: { "Draft ready": ready } }]);
126
95
  }
127
96
  async function updateReportScores(base, recordId, scores, completedOn, enrichment) {
97
+ if (skipsAirtableShadow("updateReportScores", recordId)) return;
128
98
  const fields = {
129
99
  "Lighthouse \u2014 Performance": scores.performance,
130
100
  "Lighthouse \u2014 Accessibility": scores.accessibility,
@@ -142,17 +112,6 @@ async function updateReportScores(base, recordId, scores, completedOn, enrichmen
142
112
  fields["Search position"] = enrichment.searchPosition;
143
113
  await base(REPORTS_TABLE).update([{ id: recordId, fields }]);
144
114
  }
145
- async function listSendableReports(base) {
146
- const out = [];
147
- await base(REPORTS_TABLE).select({
148
- filterByFormula: "AND({Draft ready} = TRUE(), {Approved to send} = TRUE(), {Sent at} = BLANK())",
149
- pageSize: 100
150
- }).eachPage((records, fetchNextPage) => {
151
- for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));
152
- fetchNextPage();
153
- });
154
- return out;
155
- }
156
115
  async function listAllReports(base) {
157
116
  const out = [];
158
117
  await base(REPORTS_TABLE).select({ pageSize: 100 }).eachPage((records, fetchNextPage) => {
@@ -161,10 +120,8 @@ async function listAllReports(base) {
161
120
  });
162
121
  return out;
163
122
  }
164
- async function listReportsForSite(base, siteId) {
165
- return (await listAllReports(base)).filter((r) => r.siteId === siteId);
166
- }
167
123
  async function stampSent(base, recordId, sentAt, messageId) {
124
+ if (skipsAirtableShadow("stampSent", recordId)) return;
168
125
  const fields = { "Sent at": sentAt.toISOString() };
169
126
  if (messageId !== null) fields["Resend message ID"] = messageId;
170
127
  await base(REPORTS_TABLE).update([
@@ -174,29 +131,12 @@ async function stampSent(base, recordId, sentAt, messageId) {
174
131
  }
175
132
  ]);
176
133
  }
177
- async function findReportByPeriod(base, siteId, reportType, period) {
178
- const safeType = escapeFormulaString(reportType);
179
- const safePeriod = escapeFormulaString(period);
180
- const formula = `AND({Report type} = "${safeType}", {Period} = "${safePeriod}")`;
181
- const rows = [];
182
- await base(REPORTS_TABLE).select({ filterByFormula: formula, pageSize: 100 }).eachPage((records, fetchNextPage) => {
183
- for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));
184
- fetchNextPage();
185
- });
186
- return rows.find((r) => r.siteId === siteId) ?? null;
187
- }
188
134
 
189
135
  export {
190
- toReportType,
191
- isPendingApproval,
192
- parseAutoEvidence,
193
- createDraft,
136
+ draftFields,
194
137
  setDraftReady,
195
138
  updateReportScores,
196
- listSendableReports,
197
139
  listAllReports,
198
- listReportsForSite,
199
- stampSent,
200
- findReportByPeriod
140
+ stampSent
201
141
  };
202
- //# sourceMappingURL=chunk-F3JWOPTG.js.map
142
+ //# sourceMappingURL=chunk-TEMQPSCM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/draft-fields.ts","../src/reports/airtable/reports.ts"],"sourcesContent":["/** The fields a new report row carries, as ONE pure function (#646 step 4).\n *\n * This used to live inside `createDraft` in the Airtable layer, because Airtable\n * was the primary write. Turso mints the report id and owns the row now, so the\n * field set has two consumers: the Turso creator (`createReportDraft`, via the\n * importer's `mapReportRecord`) and the legacy Airtable `createDraft`. It lives\n * OUT of `src/reports/airtable/` so the creator survives step 6 deleting that\n * directory.\n *\n * The keys are Airtable COLUMN NAMES and stay that way on purpose: they are the\n * vocabulary `mapReportRecord` reads, so a draft maps to a `reports` row through\n * the very function the parity harness diffs against — parity-clean by\n * construction rather than by a second column list someone has to remember to\n * extend. The column names outlive the Airtable client either way (design D1 kept\n * them as the import vocabulary; #646's own checklist notes that step 6 must\n * replace those maps, and this does not add a new place that has to change).\n */\nimport type { ReportType, LighthouseScores } from \"./types.js\";\nimport type { EvidenceRecord } from \"./auto-tick.js\";\n\nexport type DraftInput = {\n reportId: string;\n siteId: string;\n reportType: ReportType;\n /** UTC `YYYY-MM` recurrence key. Omitted on legacy callers; written only when supplied. */\n period?: string;\n periodStart: Date;\n periodEnd: Date;\n completedOn: Date;\n lighthouse: LighthouseScores;\n lastTestedDate: Date | null;\n /** GA \"Users\" for the period / previous period. Omitted when GA is not configured\n * for the site or the fetch failed — the operator fills the fields manually. */\n gaUsersCurrent?: number;\n gaUsersPrevious?: number;\n /** Search-presence result. `searchFoundPage1` is written whenever the check ran (true or\n * false — false is the operator-only negative signal). `searchPosition` only when found. */\n searchFoundPage1?: boolean;\n searchPosition?: number;\n subjectOverride?: string;\n /** Checklist fields to tick at create time (the auto-tick \"pass\" set). */\n checklistTicks?: string[];\n /** Auto-tick evidence snapshot to persist (keyed by checklist field). */\n autoEvidence?: Record<string, EvidenceRecord>;\n};\n\n/** `YYYY-MM-DD`, the date shape every report date column stores. */\nexport function ymd(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * The full field set for a new report row.\n *\n * `Delivery status` is set to \"pending\" at CREATION time, not at send time. This\n * matters for H4: if the send's stamp wrote \"pending\" after the webhook had\n * already written \"delivered\" (a race), the operator would see a regressed status.\n *\n * Optional values are OMITTED rather than written as null — the pre-GA behaviour,\n * where a blank cell means \"fill this in by hand\" and a written null would claim\n * the value was measured as absent.\n */\nexport function draftFields(input: DraftInput): Record<string, unknown> {\n const fields: Record<string, unknown> = {\n \"Report ID\": input.reportId,\n Site: [input.siteId],\n \"Report type\": input.reportType,\n \"Period start\": ymd(input.periodStart),\n \"Period end\": ymd(input.periodEnd),\n \"Completed on\": ymd(input.completedOn),\n \"Lighthouse — Performance\": input.lighthouse.performance,\n \"Lighthouse — Accessibility\": input.lighthouse.accessibility,\n \"Lighthouse — Best Practices\": input.lighthouse.bestPractices,\n \"Lighthouse — SEO\": input.lighthouse.seo,\n \"Delivery status\": \"pending\",\n };\n if (input.lastTestedDate) fields[\"Last tested date\"] = ymd(input.lastTestedDate);\n if (input.gaUsersCurrent !== undefined) fields[\"GA users (period)\"] = input.gaUsersCurrent;\n if (input.gaUsersPrevious !== undefined) fields[\"GA users (prev period)\"] = input.gaUsersPrevious;\n if (input.searchFoundPage1 !== undefined) fields[\"Search found page 1\"] = input.searchFoundPage1;\n if (input.searchPosition !== undefined) fields[\"Search position\"] = input.searchPosition;\n if (input.period !== undefined) fields[\"Period\"] = input.period;\n if (input.subjectOverride !== undefined) fields[\"Subject override\"] = input.subjectOverride;\n // Auto-ticked checklist boxes + the evidence snapshot. The booleans are the same columns the\n // operator/dashboard toggle; the JSON is what the health gate reads (isHealthGateClear/gatingHealth)\n // and also drives the dashboard's green/amber badges.\n for (const field of input.checklistTicks ?? []) fields[field] = true;\n if (input.autoEvidence && Object.keys(input.autoEvidence).length > 0) {\n fields[\"Checklist auto-evidence\"] = JSON.stringify(input.autoEvidence);\n }\n return fields;\n}\n","import type { FieldSet, Records } from \"airtable\";\nimport type { AirtableBase } from \"./client.js\";\nimport { isAirtableRecordId, skipsAirtableShadow } from \"../../fleet/site-id.js\";\nimport { draftFields, ymd, type DraftInput } from \"../draft-fields.js\";\nimport type { ReportType, LighthouseScores } from \"../types.js\";\nimport { ALL_CHECKLIST_FIELDS } from \"../checklist.js\";\nimport {\n toReportType,\n parseAutoEvidence,\n type DeliveryStatus,\n type ReportRow,\n} from \"../report-row.js\";\n\n// The report-row model and its pure coercers live in src/reports/report-row.ts\n// since #539 Phase 6 step 1 (#646). Re-exported so existing imports resolve unchanged.\nexport {\n toReportType,\n isPendingApproval,\n parseAutoEvidence,\n type DeliveryStatus,\n type ReportRow,\n} from \"../report-row.js\";\n\nexport const REPORTS_TABLE = \"Reports\";\n\nexport function mapRow(rec: { id: string; fields: Record<string, unknown> }): ReportRow {\n const f = rec.fields;\n const linkSites = (f[\"Site\"] as string[] | undefined) ?? [];\n const html =\n ((f[\"Rendered HTML\"] as Array<{ url: string; filename: string }> | undefined) ?? [])[0] ?? null;\n return {\n id: rec.id,\n reportId: String(f[\"Report ID\"] ?? \"\"),\n siteId: linkSites[0] ?? \"\",\n reportType: toReportType(f[\"Report type\"] as string | undefined),\n period: (f[\"Period\"] as string | undefined) ?? null,\n periodStart: (f[\"Period start\"] as string | undefined) ?? null,\n periodEnd: (f[\"Period end\"] as string | undefined) ?? null,\n completedOn: (f[\"Completed on\"] as string | undefined) ?? null,\n lighthouse: lighthouseFromFields(f),\n gaUsersCurrent: (f[\"GA users (period)\"] as number | undefined) ?? null,\n gaUsersPrevious: (f[\"GA users (prev period)\"] as number | undefined) ?? null,\n searchFoundPage1:\n typeof f[\"Search found page 1\"] === \"boolean\" ? (f[\"Search found page 1\"] as boolean) : null,\n searchPosition: (f[\"Search position\"] as number | undefined) ?? null,\n lastTestedDate: (f[\"Last tested date\"] as string | undefined) ?? null,\n commentary: (f[\"Commentary\"] as string | undefined) ?? null,\n subjectOverride: (f[\"Subject override\"] as string | undefined) ?? null,\n draftReady: Boolean(f[\"Draft ready\"]),\n approvedToSend: Boolean(f[\"Approved to send\"]),\n sentAt: (f[\"Sent at\"] as string | undefined) ?? null,\n approvedAt: (f[\"Approved At\"] as string | undefined) ?? null,\n approvedBy: (f[\"Approved By\"] as string | undefined) ?? null,\n deliveryStatus: ((f[\"Delivery status\"] as string | undefined) ?? \"pending\") as DeliveryStatus,\n renderedHtmlAttachment: html,\n resendMessageId: (f[\"Resend message ID\"] as string | undefined) ?? null,\n checklist: Object.fromEntries(ALL_CHECKLIST_FIELDS.map((name) => [name, Boolean(f[name])])),\n autoEvidence: parseAutoEvidence(f[\"Checklist auto-evidence\"]),\n sendOverride: Boolean(f[\"Send override\"]),\n overrideReason: (f[\"Override reason\"] as string | undefined) ?? null,\n overrideBy: (f[\"Override by\"] as string | undefined) ?? null,\n overrideAt: (f[\"Override at\"] as string | undefined) ?? null,\n };\n}\n\nfunction lighthouseFromFields(f: Record<string, unknown>): LighthouseScores | null {\n const p = f[\"Lighthouse — Performance\"];\n const a = f[\"Lighthouse — Accessibility\"];\n const b = f[\"Lighthouse — Best Practices\"];\n const s = f[\"Lighthouse — SEO\"];\n if (\n typeof p !== \"number\" ||\n typeof a !== \"number\" ||\n typeof b !== \"number\" ||\n typeof s !== \"number\"\n )\n return null;\n return { performance: p, accessibility: a, bestPractices: b, seo: s };\n}\n\n/** The GA + Search-presence fields a draft can carry, written to the Reports row.\n * `searchFoundPage1` is written whenever the check ran (true or false — false is the\n * operator-only negative signal); `searchPosition` only when found on page 1. Shared by\n * the create path (DraftInput) and the reuse path (updateReportScores). */\nexport type ReportEnrichment = {\n gaUsersCurrent?: number;\n gaUsersPrevious?: number;\n searchFoundPage1?: boolean;\n searchPosition?: number;\n};\n\n// The draft's field set and its `DraftInput` moved to src/reports/draft-fields.ts\n// in #646 step 4: Turso creates reports now, and the creator must outlive this\n// directory. Re-exported so existing imports resolve unchanged.\nexport { draftFields, ymd, type DraftInput } from \"../draft-fields.js\";\n\n/**\n * Escape a string for safe interpolation into an Airtable filterByFormula.\n * Airtable formulas use SQL-like string literals; we escape backslash and\n * double quote. Used wherever an externally-supplied string flows into a\n * formula (e.g. Resend message ids on the webhook path).\n */\nexport function escapeFormulaString(s: string): string {\n return s.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\n/** Called with the record Airtable echoed back from the create, before it is\n * mapped (#539 Phase 5). Structurally typed rather than importing the db\n * layer's `RawRecord`, so this module stays the leaf it has always been. */\nexport type CreatedDraftMirror = (rec: {\n id: string;\n fields: Record<string, unknown>;\n}) => Promise<void>;\n\n/**\n * Create a Reports row in Airtable, letting Airtable mint the report id.\n *\n * LEGACY since #646 step 4 (operator decision 2026-09-17): no production path\n * calls this any more. Reports are created in Turso with a minted\n * `report_<ULID>` id (`src/reports/create-report.ts`), because Airtable cannot be\n * told what record id to use and every later write addresses the report by its\n * id. Kept — not deleted — because deleting anything from this directory is step\n * 6, which needs its own operator go.\n */\nexport async function createDraft(\n base: AirtableBase,\n input: DraftInput,\n mirror?: CreatedDraftMirror,\n): Promise<ReportRow> {\n // #646 step 3: NOT a shadow write, so it does not skip. This path MINTS the\n // report id — Airtable's record id is the report id — and a `site_<ULID>` site\n // has no Websites record for the `Site` link to point at. Skipping would silently\n // produce no report; letting Airtable reject the link would surface as an opaque\n // INVALID_RECORD_ID. Refuse by name; the Turso creator is what such a site uses.\n if (!isAirtableRecordId(input.siteId)) {\n throw new Error(\n `createDraft: site ${input.siteId} was created in Turso and has no Airtable record — ` +\n `create the report in Turso instead (createReportDraft, #646 step 4)`,\n );\n }\n const fields = draftFields(input) as FieldSet;\n const created = (await base(REPORTS_TABLE).create([{ fields }])) as Records<FieldSet>;\n const rec = created[0];\n if (!rec) throw new Error(\"Airtable create returned no records\");\n const raw = { id: rec.id, fields: rec.fields as Record<string, unknown> };\n // The mirror gets what Airtable STORED, never the caller's DraftInput: parity\n // diffs Turso against the stored record, so mapping the input would diverge\n // the moment Airtable normalises a value. Injected rather than called\n // directly so this module keeps no db dependency; the injected\n // implementation owns the never-throw policy (see reports/report-mirror.ts).\n if (mirror) await mirror(raw);\n return mapRow(raw);\n}\n\n/**\n * Every writer below is a SHADOW of a Turso row since the freeze, and since #646\n * step 4 a report's id may be a minted `report_<ULID>` that Airtable has never\n * heard of. Sending such an id to Airtable is a guaranteed 404 on the drafting,\n * approve, send and webhook paths alike, so each writer asks the same question\n * the site writers already ask — could Airtable hold this row at all? — and skips\n * with one greppable `AIRTABLE_SHADOW skipped=non-rec-id writer=<name>` line\n * when it could not. A `rec…` report is untouched: its shadow row is still\n * written until step 6 deletes this layer.\n *\n * The test that covers this (tests/reports/airtable/shadow-skip-report-ids.test.ts)\n * fails if an exported writer here is missing from it.\n */\nexport async function setDraftReady(\n base: AirtableBase,\n recordId: string,\n ready: boolean,\n): Promise<void> {\n if (skipsAirtableShadow(\"setDraftReady\", recordId)) return;\n await base(REPORTS_TABLE).update([{ id: recordId, fields: { \"Draft ready\": ready } }]);\n}\n\n/**\n * Overwrite the four `Lighthouse — *` score cells (and, when supplied, `Completed\n * on`) on an EXISTING Reports row. The launch re-run path uses this: it reuses the\n * already-created Launch row but must refresh its scores to match the freshly-run\n * audit — otherwise the re-rendered preview shows new scores while the row (and the\n * eventually-sent email, which reads the row) keeps the stale ones. The create path\n * already writes fresh scores via `createDraft`; this is its update-side mirror, using\n * the same exact field names so the two stay in lockstep.\n */\nexport async function updateReportScores(\n base: AirtableBase,\n recordId: string,\n scores: LighthouseScores,\n completedOn?: Date,\n enrichment?: ReportEnrichment,\n): Promise<void> {\n if (skipsAirtableShadow(\"updateReportScores\", recordId)) return;\n const fields: FieldSet = {\n \"Lighthouse — Performance\": scores.performance,\n \"Lighthouse — Accessibility\": scores.accessibility,\n \"Lighthouse — Best Practices\": scores.bestPractices,\n \"Lighthouse — SEO\": scores.seo,\n };\n if (completedOn) fields[\"Completed on\"] = ymd(completedOn);\n if (enrichment?.gaUsersCurrent !== undefined)\n fields[\"GA users (period)\"] = enrichment.gaUsersCurrent;\n if (enrichment?.gaUsersPrevious !== undefined)\n fields[\"GA users (prev period)\"] = enrichment.gaUsersPrevious;\n if (enrichment?.searchFoundPage1 !== undefined)\n fields[\"Search found page 1\"] = enrichment.searchFoundPage1;\n if (enrichment?.searchPosition !== undefined)\n fields[\"Search position\"] = enrichment.searchPosition;\n await base(REPORTS_TABLE).update([{ id: recordId, fields }]);\n}\n\n/** Write one report's operator commentary (#539 Phase 4 report review). The\n * caller (`setReportCommentary`) has already validated the length and refused\n * the write for an already-sent report, so this stays a plain single-field\n * writer — the same division of labour as `updateSiteField`/`setSiteDetail`. */\nexport async function updateReportCommentary(\n base: AirtableBase,\n recordId: string,\n text: string,\n): Promise<void> {\n if (skipsAirtableShadow(\"updateReportCommentary\", recordId)) return;\n await base(REPORTS_TABLE).update([{ id: recordId, fields: { Commentary: text } }]);\n}\n\nexport async function listSendableReports(base: AirtableBase): Promise<ReportRow[]> {\n const out: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({\n filterByFormula:\n \"AND({Draft ready} = TRUE(), {Approved to send} = TRUE(), {Sent at} = BLANK())\",\n pageSize: 100,\n })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return out;\n}\n\n/**\n * Fetch every Reports row, unfiltered. Site-scoped callers filter the result in\n * memory: the `Site` linked-record field CANNOT be formula-filtered by record id\n * (see findReportByPeriod's doc for why), and the fleet's Reports table is small\n * enough that one paged fetch-all beats N broken-or-per-site queries.\n */\nexport async function listAllReports(base: AirtableBase): Promise<ReportRow[]> {\n const out: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({ pageSize: 100 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return out;\n}\n\nexport async function listReportsForSite(base: AirtableBase, siteId: string): Promise<ReportRow[]> {\n // Client-side match on the mapped siteId (mapRow reads the record id from the\n // REST response, where it IS present) — record ids can't appear in formulas.\n return (await listAllReports(base)).filter((r) => r.siteId === siteId);\n}\n\n/**\n * Mark a row as sent: write `Sent at` and (when known) `Resend message ID`.\n * Crucially does NOT touch `Delivery status` — that's set to \"pending\" in\n * createDraft and updated by the webhook from there. If we wrote \"pending\" here\n * we could clobber a \"delivered\" that the webhook raced ahead and wrote first (H4).\n *\n * `messageId` may be `null`: the 409 idempotency-conflict path has no recoverable\n * id (the original send's id was lost when the prior run's stamp failed), so it\n * passes null and we write ONLY `Sent at`. Writing a sentinel string like\n * \"idempotent-conflict\" into the id column would masquerade as a real Resend id\n * and silently orphan `findReportByMessageId` lookups; leaving it null is honest —\n * the row still stops replaying (Sent at is set), delivery tracking for that one\n * report is simply degraded (the id is genuinely unknowable on that path).\n */\nexport async function stampSent(\n base: AirtableBase,\n recordId: string,\n sentAt: Date,\n messageId: string | null,\n): Promise<void> {\n if (skipsAirtableShadow(\"stampSent\", recordId)) return;\n const fields: Record<string, string> = { \"Sent at\": sentAt.toISOString() };\n if (messageId !== null) fields[\"Resend message ID\"] = messageId;\n await base(REPORTS_TABLE).update([\n {\n id: recordId,\n fields,\n },\n ]);\n}\n\nexport async function setDeliveryStatus(\n base: AirtableBase,\n recordId: string,\n status: DeliveryStatus,\n): Promise<void> {\n if (skipsAirtableShadow(\"setDeliveryStatus\", recordId)) return;\n await base(REPORTS_TABLE).update([{ id: recordId, fields: { \"Delivery status\": status } }]);\n}\n\n/**\n * Stamp the approval on a Reports row: flips `Approved to send` TRUE and records\n * who/when for the audit trail. The caller (approveReport handler) is responsible\n * for idempotency — this is the raw write. Never touches `Sent at`.\n */\nexport async function approveReportRow(\n base: AirtableBase,\n recordId: string,\n approvedAt: Date,\n approvedBy: string,\n): Promise<void> {\n if (skipsAirtableShadow(\"approveReportRow\", recordId)) return;\n await base(REPORTS_TABLE).update([\n {\n id: recordId,\n fields: {\n \"Approved to send\": true,\n \"Approved At\": approvedAt.toISOString(),\n \"Approved By\": approvedBy,\n },\n },\n ]);\n}\n\n/**\n * Stamp a logged send-anyway override on a Reports row: sets `Send override` TRUE, the reason, and\n * who/when — AND flips `Approved to send` TRUE (with the same Approved At/By stamp) so the daily\n * cron delivers the overridden report. Mirrors {@link approveReportRow}; never touches `Sent at`.\n */\nexport async function overrideReportRow(\n base: AirtableBase,\n recordId: string,\n overrideAt: Date,\n overrideBy: string,\n reason: string,\n): Promise<void> {\n if (skipsAirtableShadow(\"overrideReportRow\", recordId)) return;\n const at = overrideAt.toISOString();\n await base(REPORTS_TABLE).update([\n {\n id: recordId,\n fields: {\n \"Send override\": true,\n \"Override reason\": reason,\n \"Override by\": overrideBy,\n \"Override at\": at,\n \"Approved to send\": true,\n \"Approved At\": at,\n \"Approved By\": overrideBy,\n },\n },\n ]);\n}\n\n/**\n * True when an `.find` rejection is a GENUINE not-found, not a transient failure.\n * The Airtable SDK stamps `.statusCode` (404) and/or `.error` (\"NOT_FOUND\") on\n * its errors. Anything else (429 rate-limit, 500 outage, bad-PAT 401, network\n * error) must NOT be masked as a 404 — see getReportById.\n */\nfunction isNotFoundError(err: unknown): boolean {\n if (typeof err !== \"object\" || err === null) return false;\n const e = err as { statusCode?: unknown; error?: unknown; name?: unknown; message?: unknown };\n if (e.statusCode === 404) return true;\n const tag = String(e.error ?? e.name ?? e.message ?? \"\");\n return tag === \"NOT_FOUND\" || /not found/i.test(tag);\n}\n\n/**\n * Fetch one Reports row by its Airtable record id, or null if it doesn't exist.\n * Only a GENUINE not-found (404 / NOT_FOUND) collapses to null; every other\n * failure (outage, 429, bad PAT, network error) is rethrown so the adapter\n * surfaces a 500 instead of a misleading 404. Swallowing all throws previously\n * turned an Airtable outage into a \"no such report\".\n */\nexport async function getReportById(\n base: AirtableBase,\n recordId: string,\n): Promise<ReportRow | null> {\n try {\n const rec = await base(REPORTS_TABLE).find(recordId);\n return mapRow({ id: rec.id, fields: rec.fields as Record<string, unknown> });\n } catch (err) {\n if (isNotFoundError(err)) return null;\n throw err;\n }\n}\n\nexport async function findReportByMessageId(\n base: AirtableBase,\n messageId: string,\n): Promise<ReportRow | null> {\n const rows: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({\n filterByFormula: `{Resend message ID} = \"${escapeFormulaString(messageId)}\"`,\n maxRecords: 1,\n })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return rows[0] ?? null;\n}\n\n/**\n * Find the Reports row for a `(site, reportType, period)` triple, or null. The\n * idempotency lookup behind search-before-create drafting.\n *\n * The site is matched CLIENT-side, never in the formula: Airtable's formula layer\n * renders linked-record fields ({Site}) as the linked rows' PRIMARY-FIELD NAMES,\n * not record ids, so any formula comparing {Site} or ARRAYJOIN({Site}) against a\n * `recXXX` id matches NOTHING (live-proven against the real base — do not\n * reintroduce that idiom). Record ids exist only in the REST response, where\n * mapRow reads them. So the formula filters on the real scalar fields (Report\n * type + Period — escaped, keeping it injection-safe if their source ever\n * changes), and the first mapped row whose siteId matches wins. The candidate\n * set is at most one row per site for the (type, period), so this stays small.\n */\nexport async function findReportByPeriod(\n base: AirtableBase,\n siteId: string,\n reportType: ReportType,\n period: string,\n): Promise<ReportRow | null> {\n const safeType = escapeFormulaString(reportType);\n const safePeriod = escapeFormulaString(period);\n const formula = `AND({Report type} = \"${safeType}\", {Period} = \"${safePeriod}\")`;\n const rows: ReportRow[] = [];\n await base(REPORTS_TABLE)\n .select({ filterByFormula: formula, pageSize: 100 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return rows.find((r) => r.siteId === siteId) ?? null;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,52 @@
1
+ import {
2
+ ignoreRulesFor,
3
+ pathsMissingFromHead
4
+ } from "./chunk-WMF5S2MB.js";
5
+
6
+ // src/recipes/_head-guard.ts
7
+ import { rm, rmdir } from "fs/promises";
8
+ import { writeFile } from "fs/promises";
9
+ import { dirname, join } from "path";
10
+ var RESTORED_NOTE = "Everything this run wrote to those paths has been put back as it was.";
11
+ async function refusedByGit(cwd, installed, what) {
12
+ const missing = await pathsMissingFromHead(cwd, installed);
13
+ if (missing.length === 0) return null;
14
+ const rules = await ignoreRulesFor(cwd, missing);
15
+ const sources = [...new Set(rules.map((r) => r.split(" ")[0]))];
16
+ const explained = new Set(rules.map((r) => r.split(" ")[1] ?? ""));
17
+ const unexplained = missing.filter((p) => !explained.has(p));
18
+ return {
19
+ missing,
20
+ notes: `${what} was NOT installed: ${missing.length} of ${installed.length} paths git refused to track, so a fresh clone and CI get nothing. Absent from the commit: ${missing.join(", ")}. ` + (sources.length > 0 ? `Excluded by ${sources.join(", ")} \u2014 narrow or remove those rules and re-run. ` : "") + (unexplained.length > 0 ? `No ignore rule matched ${unexplained.join(", ")} \u2014 check .git/info/exclude, core.excludesFile, and whether the directory is a nested repository. ` : "")
21
+ };
22
+ }
23
+ async function undoRefusedWrites(cwd, before, missing) {
24
+ const removed = [];
25
+ for (const [rel, prev] of before) {
26
+ if (!missing.includes(rel)) continue;
27
+ if (prev === null) {
28
+ await rm(join(cwd, rel), { force: true });
29
+ removed.push(rel);
30
+ } else {
31
+ await writeFile(join(cwd, rel), prev, "utf-8");
32
+ }
33
+ }
34
+ for (const rel of removed) {
35
+ let d = dirname(rel);
36
+ while (d !== "." && d !== "/" && d !== "") {
37
+ try {
38
+ await rmdir(join(cwd, d));
39
+ } catch {
40
+ break;
41
+ }
42
+ d = dirname(d);
43
+ }
44
+ }
45
+ }
46
+
47
+ export {
48
+ RESTORED_NOTE,
49
+ refusedByGit,
50
+ undoRefusedWrites
51
+ };
52
+ //# sourceMappingURL=chunk-TJLKWQOX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/recipes/_head-guard.ts"],"sourcesContent":["import { rm, rmdir } from \"node:fs/promises\";\nimport { writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { ignoreRulesFor, pathsMissingFromHead } from \"../util/git.js\";\n\n/** A refusal: what git did not take, and what to tell the operator about it. */\nexport type HeadRefusal = {\n /** The subset of the manifest that is NOT in HEAD's tree. */\n missing: string[];\n /** Operator-facing: what is absent from the commit, and which rule excluded it. */\n notes: string;\n};\n\n/** Appended by a caller that has already put the refused paths back. */\nexport const RESTORED_NOTE =\n \"Everything this run wrote to those paths has been put back as it was.\";\n\n/**\n * POSITIVE EVIDENCE that the commit really carries `installed`: every path must\n * be FOUND in HEAD's tree. `commit()` stages with `git add -A`, which honours\n * the target site's `.gitignore` and exits 0 either way, and git cannot\n * re-include a file whose PARENT DIRECTORY is excluded. So a recipe can write\n * its files to disk, commit none of them, and report success — #734 for\n * `match-harness`, generalised to every recipe that creates NEW paths by #741.\n *\n * `what` names the install in the operator's terms (\"the /health endpoint\").\n * Returns null when every path landed — i.e. the guard GRANTS.\n *\n * PRESENCE, NOT CONTENT: a path in HEAD with the wrong bytes passes here. This\n * answers \"the commit contains what the recipe says it installed\", and must\n * never be read as \"the recipe worked\".\n */\nexport async function refusedByGit(\n cwd: string,\n installed: readonly string[],\n what: string,\n): Promise<HeadRefusal | null> {\n const missing = await pathsMissingFromHead(cwd, installed);\n if (missing.length === 0) return null;\n\n // `<source>:<line>:<pattern>\\t<path>` rows. A missing path with no row is\n // absent for some other reason (a nested repository, say) and is reported as\n // such rather than guessed at.\n const rules = await ignoreRulesFor(cwd, missing);\n const sources = [...new Set(rules.map((r) => r.split(\"\\t\")[0]!))];\n const explained = new Set(rules.map((r) => r.split(\"\\t\")[1] ?? \"\"));\n const unexplained = missing.filter((p) => !explained.has(p));\n\n return {\n missing,\n notes:\n `${what} was NOT installed: ${missing.length} of ${installed.length} paths git refused to ` +\n `track, so a fresh clone and CI get nothing. Absent from the commit: ${missing.join(\", \")}. ` +\n (sources.length > 0\n ? `Excluded by ${sources.join(\", \")} — narrow or remove those rules and re-run. `\n : \"\") +\n (unexplained.length > 0\n ? `No ignore rule matched ${unexplained.join(\", \")} — check .git/info/exclude, ` +\n `core.excludesFile, and whether the directory is a nested repository. `\n : \"\"),\n };\n}\n\n/**\n * Put the checkout back exactly as this run found it, for the paths git refused.\n * Only paths THIS RUN wrote are touched — `before` maps each to its prior\n * content, or null when it did not exist; a path that reached the commit is left\n * to the caller's branch restore, and one we never wrote is never ours.\n * `git checkout -f` cannot do this job: these paths are ignored, so git does not\n * know they exist.\n *\n * Without it the residue is worse than the defect. Every recipe here noops on\n * \"already exists\", so the file git refused would make the NEXT run report the\n * site done over a path no clone will ever have.\n */\nexport async function undoRefusedWrites(\n cwd: string,\n before: ReadonlyMap<string, string | null>,\n missing: readonly string[],\n): Promise<void> {\n const removed: string[] = [];\n for (const [rel, prev] of before) {\n if (!missing.includes(rel)) continue;\n if (prev === null) {\n await rm(join(cwd, rel), { force: true });\n removed.push(rel);\n } else {\n await writeFile(join(cwd, rel), prev, \"utf-8\");\n }\n }\n // Prune the directories this run created, deepest first and only while they\n // are empty: rmdir refuses a non-empty directory, which is exactly the guard\n // wanted, and the walk upward stops at the first one that is someone else's.\n for (const rel of removed) {\n let d = dirname(rel);\n while (d !== \".\" && d !== \"/\" && d !== \"\") {\n try {\n await rmdir(join(cwd, d));\n } catch {\n break;\n }\n d = dirname(d);\n }\n }\n}\n"],"mappings":";;;;;;AAAA,SAAS,IAAI,aAAa;AAC1B,SAAS,iBAAiB;AAC1B,SAAS,SAAS,YAAY;AAYvB,IAAM,gBACX;AAiBF,eAAsB,aACpB,KACA,WACA,MAC6B;AAC7B,QAAM,UAAU,MAAM,qBAAqB,KAAK,SAAS;AACzD,MAAI,QAAQ,WAAW,EAAG,QAAO;AAKjC,QAAM,QAAQ,MAAM,eAAe,KAAK,OAAO;AAC/C,QAAM,UAAU,CAAC,GAAG,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,GAAI,EAAE,CAAC,CAAE,CAAC,CAAC;AAChE,QAAM,YAAY,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,GAAI,EAAE,CAAC,KAAK,EAAE,CAAC;AAClE,QAAM,cAAc,QAAQ,OAAO,CAAC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC;AAE3D,SAAO;AAAA,IACL;AAAA,IACA,OACE,GAAG,IAAI,uBAAuB,QAAQ,MAAM,OAAO,UAAU,MAAM,6FACI,QAAQ,KAAK,IAAI,CAAC,QACxF,QAAQ,SAAS,IACd,eAAe,QAAQ,KAAK,IAAI,CAAC,sDACjC,OACH,YAAY,SAAS,IAClB,0BAA0B,YAAY,KAAK,IAAI,CAAC,2GAEhD;AAAA,EACR;AACF;AAcA,eAAsB,kBACpB,KACA,QACA,SACe;AACf,QAAM,UAAoB,CAAC;AAC3B,aAAW,CAAC,KAAK,IAAI,KAAK,QAAQ;AAChC,QAAI,CAAC,QAAQ,SAAS,GAAG,EAAG;AAC5B,QAAI,SAAS,MAAM;AACjB,YAAM,GAAG,KAAK,KAAK,GAAG,GAAG,EAAE,OAAO,KAAK,CAAC;AACxC,cAAQ,KAAK,GAAG;AAAA,IAClB,OAAO;AACL,YAAM,UAAU,KAAK,KAAK,GAAG,GAAG,MAAM,OAAO;AAAA,IAC/C;AAAA,EACF;AAIA,aAAW,OAAO,SAAS;AACzB,QAAI,IAAI,QAAQ,GAAG;AACnB,WAAO,MAAM,OAAO,MAAM,OAAO,MAAM,IAAI;AACzC,UAAI;AACF,cAAM,MAAM,KAAK,KAAK,CAAC,CAAC;AAAA,MAC1B,QAAQ;AACN;AAAA,MACF;AACA,UAAI,QAAQ,CAAC;AAAA,IACf;AAAA,EACF;AACF;","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  withRecipe
3
- } from "./chunk-ZQV7UQNE.js";
3
+ } from "./chunk-LUF52EIY.js";
4
4
  import {
5
5
  defaultSpawn
6
6
  } from "./chunk-LBYOLBW7.js";
@@ -76,4 +76,4 @@ async function bumpDeps(site, opts = {}) {
76
76
  export {
77
77
  bumpDeps
78
78
  };
79
- //# sourceMappingURL=chunk-CJ2RMTT7.js.map
79
+ //# sourceMappingURL=chunk-TV76A6GN.js.map
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  REPORT_LIST_COLUMNS
3
- } from "./chunk-WVNDEYXL.js";
3
+ } from "./chunk-BINFZQ2T.js";
4
4
  import {
5
5
  mapReportRecord,
6
6
  mapWebsiteRecord
7
- } from "./chunk-6R56OKJZ.js";
7
+ } from "./chunk-LF2TJN4X.js";
8
8
 
9
9
  // src/db/parity.ts
10
10
  var SKIP_COLUMNS = /* @__PURE__ */ new Set([
@@ -129,4 +129,4 @@ export {
129
129
  checkFleetParity,
130
130
  formatParityResult
131
131
  };
132
- //# sourceMappingURL=chunk-KZRR4NFG.js.map
132
+ //# sourceMappingURL=chunk-TVZ6QCOE.js.map
@@ -1,27 +1,32 @@
1
+ import {
2
+ smokeSuite
3
+ } from "./chunk-EN6CTBUN.js";
1
4
  import {
2
5
  onboard
3
- } from "./chunk-S7RYFUIK.js";
6
+ } from "./chunk-6PGXKN6F.js";
4
7
  import {
5
8
  convertToPnpm
6
- } from "./chunk-CWXTNPNZ.js";
9
+ } from "./chunk-AYDQQBNG.js";
7
10
  import {
8
11
  svelteCodemods
9
- } from "./chunk-2LYOAB2E.js";
12
+ } from "./chunk-HSMN35LU.js";
10
13
  import {
11
14
  healthEndpoint
12
- } from "./chunk-WSJ7RO54.js";
15
+ } from "./chunk-LAG6IP7Y.js";
13
16
  import {
14
- smokeSuite
15
- } from "./chunk-DFKBTBO3.js";
17
+ RESTORED_NOTE,
18
+ refusedByGit,
19
+ undoRefusedWrites
20
+ } from "./chunk-TJLKWQOX.js";
16
21
  import {
17
22
  syncConfigs
18
- } from "./chunk-FUWFLAWQ.js";
23
+ } from "./chunk-3RLDX3Q2.js";
19
24
  import {
20
25
  withRecipe
21
- } from "./chunk-ZQV7UQNE.js";
26
+ } from "./chunk-LUF52EIY.js";
22
27
  import {
23
28
  runAudits
24
- } from "./chunk-TO7DJYLZ.js";
29
+ } from "./chunk-L3S3QZXL.js";
25
30
  import {
26
31
  siteLabel
27
32
  } from "./chunk-XXTZBPUY.js";
@@ -89,10 +94,23 @@ async function a11yFixturesPage(site) {
89
94
  }
90
95
  return { kind: "apply", plan: { target } };
91
96
  },
92
- apply: async (planned, { commit }) => {
97
+ apply: async (planned, { commit, cwd }) => {
93
98
  await mkdir(dirname(planned.target), { recursive: true });
94
99
  await writeFile(planned.target, A11Y_FIXTURES_PAGE_TEMPLATE, "utf-8");
95
100
  await commit("feat: add /dev/a11y-fixtures starter route");
101
+ const refusal = await refusedByGit(
102
+ cwd,
103
+ [A11Y_FIXTURES_PAGE_RELATIVE],
104
+ "the /dev/a11y-fixtures route"
105
+ );
106
+ if (refusal) {
107
+ await undoRefusedWrites(
108
+ cwd,
109
+ /* @__PURE__ */ new Map([[A11Y_FIXTURES_PAGE_RELATIVE, null]]),
110
+ refusal.missing
111
+ );
112
+ return { kind: "failed", notes: refusal.notes + RESTORED_NOTE };
113
+ }
96
114
  return { kind: "ok" };
97
115
  }
98
116
  });
@@ -143,4 +161,4 @@ export {
143
161
  DEFAULT_INIT_STEPS,
144
162
  init
145
163
  };
146
- //# sourceMappingURL=chunk-BVEDKMNG.js.map
164
+ //# sourceMappingURL=chunk-U2ZI6G7H.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/recipes/a11y-fixtures-page/index.ts","../src/recipes/a11y-fixtures-page/template.ts","../src/recipes/init.ts"],"sourcesContent":["import { access, mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { withRecipe } from \"../_with-recipe.js\";\nimport { refusedByGit, undoRefusedWrites, RESTORED_NOTE } from \"../_head-guard.js\";\nimport { A11Y_FIXTURES_PAGE_RELATIVE, A11Y_FIXTURES_PAGE_TEMPLATE } from \"./template.js\";\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\n/**\n * Writes a starter `src/routes/dev/a11y-fixtures/+page.svelte` if the route\n * doesn't already exist. The hardcoded URL in `src/configs/lighthouse.ts` +\n * `src/configs/playwright-a11y.ts` targets this path — newly-onboarded sites\n * need the route to exist for either audit to pass. Operator edits to an\n * existing page are never clobbered (noop on existing file).\n */\nexport async function a11yFixturesPage(site: Site): Promise<RecipeResult> {\n const target = join(site.path, A11Y_FIXTURES_PAGE_RELATIVE);\n return withRecipe<{ target: string }>({\n name: \"a11y-fixtures-page\",\n site,\n plan: async () => {\n if (await fileExists(target)) {\n return { kind: \"noop\", notes: `${A11Y_FIXTURES_PAGE_RELATIVE} already exists` };\n }\n return { kind: \"apply\", plan: { target } };\n },\n apply: async (planned, { commit, cwd }) => {\n await mkdir(dirname(planned.target), { recursive: true });\n await writeFile(planned.target, A11Y_FIXTURES_PAGE_TEMPLATE, \"utf-8\");\n await commit(\"feat: add /dev/a11y-fixtures starter route\");\n\n // Did git TAKE it? A site that ignores `src/routes/dev/` (a plausible rule\n // — the route exists only for the audits) gets the file on disk, nothing\n // in the commit, and a result that reads as done. The lighthouse and\n // playwright-a11y configs would then target a route no clone has, and\n // every later run noops on \"already exists\" (#741).\n const refusal = await refusedByGit(\n cwd,\n [A11Y_FIXTURES_PAGE_RELATIVE],\n \"the /dev/a11y-fixtures route\",\n );\n if (refusal) {\n await undoRefusedWrites(\n cwd,\n new Map([[A11Y_FIXTURES_PAGE_RELATIVE, null]]),\n refusal.missing,\n );\n return { kind: \"failed\", notes: refusal.notes + RESTORED_NOTE };\n }\n return { kind: \"ok\" };\n },\n });\n}\n","/** Relative path inside a site where the a11y fixtures route lives. The\n * hardcoded URL in `src/configs/lighthouse.ts` + `src/configs/playwright-a11y.ts`\n * is `/dev/a11y-fixtures`, so a SvelteKit `+page.svelte` here resolves. */\nexport const A11Y_FIXTURES_PAGE_RELATIVE = \"src/routes/dev/a11y-fixtures/+page.svelte\";\n\n/** Stub `+page.svelte` for newly-onboarded sites. Generic on purpose —\n * landmarks, heading hierarchy, and a relative link cover the axe-core +\n * lhci defaults without committing the operator to any specific fixture\n * shape. Replace with site-specific patterns over time. */\nexport const A11Y_FIXTURES_PAGE_TEMPLATE = `<svelte:head>\n <title>a11y fixtures — Reddoor</title>\n <meta\n name=\"description\"\n content=\"Reddoor accessibility fixtures — semantic landmarks, heading hierarchy, and a stable target for @lhci/cli and Playwright + axe-core coverage. Not linked from the public site.\"\n />\n</svelte:head>\n\n<main>\n <header>\n <h1>Accessibility fixtures</h1>\n <p>\n This page exists so <code>@lhci/cli</code> and Playwright + axe-core have a\n stable target with predictable a11y characteristics. It is not linked from\n the public site.\n </p>\n </header>\n\n <section aria-labelledby=\"landmarks-heading\">\n <h2 id=\"landmarks-heading\">Landmarks</h2>\n <p>\n A single <code>main</code> wraps the page; sections each declare\n <code>aria-labelledby</code> matched to their heading id so screen readers\n and axe both see a clean outline.\n </p>\n </section>\n\n <section aria-labelledby=\"links-heading\">\n <h2 id=\"links-heading\">Links</h2>\n <p>\n <a href=\"/\">Back to home</a> — relative link with descriptive visible text,\n so no <code>aria-label</code> override is needed.\n </p>\n </section>\n</main>\n`;\n","import type { AuditResult, RecipeResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { convertToPnpm } from \"./convert-to-pnpm.js\";\nimport { onboard } from \"./onboard.js\";\nimport { syncConfigs } from \"./sync-configs.js\";\nimport { svelteCodemods } from \"./svelte-codemods.js\";\nimport { a11yFixturesPage } from \"./a11y-fixtures-page/index.js\";\nimport { healthEndpoint } from \"./health-endpoint/index.js\";\nimport { smokeSuite } from \"./smoke-suite/index.js\";\nimport { runAudits } from \"../audits/index.js\";\n\nexport type InitStepResult =\n | { kind: \"recipe\"; result: RecipeResult }\n | { kind: \"audit\"; results: AuditResult[] }\n | { kind: \"error\"; message: string };\n\nexport type InitStep = {\n name: string;\n run: (site: Site) => Promise<InitStepResult>;\n};\n\nexport type InitResult = {\n site: string;\n steps: Array<{ name: string; result: InitStepResult }>;\n /** True if every step ran; false if an `error` or `failed` recipe result\n * short-circuited the chain. `noop` recipes do not break completeness. */\n complete: boolean;\n};\n\nexport type InitOptions = {\n /** Override the default step list. Tests inject mocked steps; production\n * code relies on the default. */\n steps?: InitStep[];\n};\n\nfunction recipeStep(name: string, fn: (site: Site) => Promise<RecipeResult>): InitStep {\n return {\n name,\n run: async (site) => ({ kind: \"recipe\", result: await fn(site) }),\n };\n}\n\n/** convert-to-pnpm → onboard → sync-configs → svelte-codemods →\n * a11y-fixtures-page → health-endpoint → smoke-suite → audit. Order is\n * deliberate — every step depends on the prior one's output (pnpm before\n * onboard's installs, onboard's deps before sync-configs writes lighthouserc,\n * fixtures page before audit actually has a route to hit; smoke-suite's\n * `pnpm install` lands before the closing audit). */\nexport const DEFAULT_INIT_STEPS: InitStep[] = [\n recipeStep(\"convert-to-pnpm\", convertToPnpm),\n recipeStep(\"onboard\", onboard),\n recipeStep(\"sync-configs\", syncConfigs),\n recipeStep(\"svelte-codemods\", svelteCodemods),\n recipeStep(\"a11y-fixtures-page\", a11yFixturesPage),\n recipeStep(\"health-endpoint\", healthEndpoint),\n recipeStep(\"smoke-suite\", smokeSuite),\n {\n name: \"audit\",\n run: async (site) => ({ kind: \"audit\", results: await runAudits(site) }),\n },\n];\n\n/**\n * One-shot guided onboarding. Runs the default step sequence against a\n * site, collecting per-step results into an InitResult. Each underlying\n * recipe still creates its own branch — init is a thin orchestrator, not\n * a branch-collapser; the operator ends up with one stack of branches per\n * mutated step (recipes that noop don't branch).\n *\n * Stops the chain on the first uncaught error or `failed` recipe result.\n * `noop` results are expected (e.g. running init twice) and continue the\n * chain. The final audit pass runs if no prior step errored.\n */\nexport async function init(site: Site, opts: InitOptions = {}): Promise<InitResult> {\n const steps = opts.steps ?? DEFAULT_INIT_STEPS;\n const out: Array<{ name: string; result: InitStepResult }> = [];\n\n for (const step of steps) {\n let result: InitStepResult;\n try {\n result = await step.run(site);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n out.push({ name: step.name, result: { kind: \"error\", message } });\n return { site: siteLabel(site), steps: out, complete: false };\n }\n out.push({ name: step.name, result });\n if (result.kind === \"recipe\" && result.result.status === \"failed\") {\n return { site: siteLabel(site), steps: out, complete: false };\n }\n }\n\n return { site: siteLabel(site), steps: out, complete: true };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,69 @@
1
+ import {
2
+ draftFields,
3
+ setDraftReady
4
+ } from "./chunk-TEMQPSCM.js";
5
+ import {
6
+ isPendingApproval
7
+ } from "./chunk-ASLDCWBF.js";
8
+ import {
9
+ encodeUlid,
10
+ skipsAirtableShadow
11
+ } from "./chunk-RQJFGK3A.js";
12
+
13
+ // src/reports/queue.ts
14
+ var REPORT_TIER = {
15
+ Maintenance: 1,
16
+ Testing: 2,
17
+ Announcement: 3,
18
+ Launch: 3
19
+ };
20
+ function reportTier(type) {
21
+ return REPORT_TIER[type];
22
+ }
23
+ async function queueDraft(base, report, mirror) {
24
+ const setReady = async (id, ready) => {
25
+ await setDraftReady(base, id, ready);
26
+ await mirror.patch(id, { draft_ready: ready ? 1 : 0 });
27
+ };
28
+ const newTier = reportTier(report.reportType);
29
+ const others = (await mirror.forSite(report.siteId)).filter(isPendingApproval).filter((r) => r.id !== report.id);
30
+ const blocker = others.find((r) => reportTier(r.reportType) >= newTier);
31
+ if (blocker) {
32
+ await setReady(report.id, false);
33
+ return { queued: false, blockedBy: blocker.reportType, supersededIds: [] };
34
+ }
35
+ const supersededIds = [];
36
+ for (const r of others) {
37
+ await setReady(r.id, false);
38
+ supersededIds.push(r.id);
39
+ }
40
+ await setReady(report.id, true);
41
+ return { queued: true, supersededIds };
42
+ }
43
+
44
+ // src/fleet/report-id.ts
45
+ function mintReportId(now = Date.now()) {
46
+ const random = new Uint8Array(10);
47
+ globalThis.crypto.getRandomValues(random);
48
+ return `report_${encodeUlid(now, random)}`;
49
+ }
50
+
51
+ // src/reports/create-report.ts
52
+ async function createReportDraft(input, deps) {
53
+ const id = (deps.mintId ?? mintReportId)();
54
+ const row = await deps.create({ id, fields: draftFields(input) });
55
+ skipsAirtableShadow("createReportDraft", id);
56
+ return row;
57
+ }
58
+ async function findReportForPeriod(store, siteId, reportType, period) {
59
+ const rows = await store.forSite(siteId);
60
+ return rows.find((r) => r.reportType === reportType && r.period === period) ?? null;
61
+ }
62
+
63
+ export {
64
+ createReportDraft,
65
+ findReportForPeriod,
66
+ reportTier,
67
+ queueDraft
68
+ };
69
+ //# sourceMappingURL=chunk-UPE6D4AT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/queue.ts","../src/fleet/report-id.ts","../src/reports/create-report.ts"],"sourcesContent":["import type { ReportType } from \"./types.js\";\nimport type { AirtableBase } from \"./airtable/client.js\";\nimport { isPendingApproval, setDraftReady } from \"./airtable/reports.js\";\nimport type { ReportMirror } from \"./report-mirror.js\";\n\n/**\n * Report tiers — a higher tier is a SUPERSET of the lower ones, so only the highest-tier\n * un-sent draft is worth keeping in a site's approve queue. Maintenance ⊂ Testing ⊂\n * {Announcement, Launch}. Announcement and Launch share tier 3 (neither supersets the other).\n */\nexport const REPORT_TIER: Record<ReportType, number> = {\n Maintenance: 1,\n Testing: 2,\n Announcement: 3,\n Launch: 3,\n};\n\nexport function reportTier(type: ReportType): number {\n return REPORT_TIER[type];\n}\n\nexport type QueueOutcome = {\n /** Whether `report` was placed in (kept in) the approve queue. */\n queued: boolean;\n /** When `queued` is false, the queued report type that subsumes this one. */\n blockedBy?: ReportType;\n /** Ids of lower-tier queued reports that this one superseded (un-queued). */\n supersededIds: string[];\n};\n\n/**\n * Enforce \"one report queued per site, highest tier wins\" at draft time. Given a freshly\n * created/reused report, look at the OTHER reports already pending approval for the same site\n * (Draft ready ∧ ¬Approved ∧ ¬Sent) and:\n *\n * - If any is of an EQUAL-OR-HIGHER tier → the new report is subsumed: leave it un-queued\n * (Draft ready = false) and report `blockedBy`. (Equal-tier, e.g. a queued Launch when an\n * Announcement is drafted, keeps the existing one rather than silently replacing it.)\n * - Otherwise the new report is strictly the highest → un-queue every (strictly lower) pending\n * report for the site (superseded, not deleted — the row is kept) and queue the new one.\n *\n * Returns what happened so the caller can surface it. PURE side effects are all `setDraftReady`.\n *\n * #539 Phase 5: each flag is mirrored into Turso. The SUPERSEDED rows matter as\n * much as the new one — un-queueing them is the whole point of this function, so\n * a mirror covering only `report.id` would leave the console showing a site with\n * two queued reports until the next hourly sync.\n *\n * #646 step 4: the site's other reports are READ from Turso (`mirror.forSite`),\n * which is why `mirror` is no longer optional. The Airtable read this replaces\n * could not see a report drafted for a `site_<ULID>` site — and an empty answer\n * does not fail, it silently queues a second report for a site that already had\n * one. The Airtable `setDraftReady` stays as the shadow; it skips a report id\n * Airtable cannot hold.\n */\nexport async function queueDraft(\n base: AirtableBase,\n report: { id: string; siteId: string; reportType: ReportType },\n mirror: ReportMirror,\n): Promise<QueueOutcome> {\n const setReady = async (id: string, ready: boolean): Promise<void> => {\n await setDraftReady(base, id, ready);\n await mirror.patch(id, { draft_ready: ready ? 1 : 0 });\n };\n const newTier = reportTier(report.reportType);\n const others = (await mirror.forSite(report.siteId))\n .filter(isPendingApproval)\n .filter((r) => r.id !== report.id);\n\n const blocker = others.find((r) => reportTier(r.reportType) >= newTier);\n if (blocker) {\n // A queued report already covers this one. Make sure the new draft is NOT queued (the reuse\n // path may hand us a row that was Draft-ready from a prior run) and stand down.\n await setReady(report.id, false);\n return { queued: false, blockedBy: blocker.reportType, supersededIds: [] };\n }\n\n // The new report is strictly the highest-tier pending — supersede the rest (un-queue), keep it.\n const supersededIds: string[] = [];\n for (const r of others) {\n await setReady(r.id, false);\n supersededIds.push(r.id);\n }\n await setReady(report.id, true);\n return { queued: true, supersededIds };\n}\n","/**\n * Report ids after Airtable (#646 step 4, operator decision 2026-09-17).\n *\n * The site decision (`site-id.ts`), applied to the other table Airtable used to\n * mint for us. Two shapes coexist PERMANENTLY:\n *\n * - `rec…` every report drafted before this. Airtable's record id\n * became the Turso primary key at import (design D1) and is\n * never rewritten — `reports.id` is what the approve route,\n * the preview url, the re-render and the send all address.\n * - `report_<ULID>` every report drafted since, minted here.\n *\n * The ULID encoding itself is NOT duplicated: `encodeUlid` is the one in\n * `site-id.ts`, which carries the reasoning for implementing it locally. This\n * module is the report half of the same decision, kept separate only so the two\n * prefixes cannot be confused at a call site.\n *\n * Unlike a site, a report has no slug: nothing outside the database addresses a\n * report by anything but this id, so the id IS the external handle (the preview\n * link, the approve POST, the `--rerender` argument). That is why the three\n * Netlify routes validate its shape, and why {@link isReportId} exists — the\n * routes must accept both shapes, and a shape check that only knew `rec` would\n * 404 every report drafted from now on.\n */\nimport { encodeUlid } from \"./site-id.js\";\n\n/** Mint a new report id: `report_<ULID>`. Time-sortable, like the site ids. */\nexport function mintReportId(now: number = Date.now()): string {\n const random = new Uint8Array(10);\n globalThis.crypto.getRandomValues(random);\n return `report_${encodeUlid(now, random)}`;\n}\n\n/** A report id minted by {@link mintReportId}. */\nexport function isMintedReportId(id: string): boolean {\n return /^report_[0-9A-HJKMNP-TV-Z]{26}$/.test(id);\n}\n\n/**\n * Either shape a report id can legitimately take — the predicate the request\n * routes validate with.\n *\n * Deliberately NOT a \"looks roughly like an id\" test: these values come off a\n * query string, so the route answers 404 for anything that is not one of the two\n * minters' output shapes. The `rec` half is the routes' own `/^rec[A-Za-z0-9]+$/`\n * kept EXACTLY as it was — widening it here would quietly relax three request\n * paths that this change is only supposed to teach a second shape. That is also\n * why it is stricter than `isAirtableRecordId`, which is the SHADOW-WRITE\n * question (\"could Airtable hold this row at all\") and answers it for readable\n * test fixtures like `rec_report_1` too.\n */\nexport function isReportId(id: string): boolean {\n return /^rec[A-Za-z0-9]+$/.test(id) || isMintedReportId(id);\n}\n","/**\n * Creating a report, Turso-native (#539 Phase 6 step 4, #646; operator decision\n * 2026-09-17).\n *\n * Until now Airtable MINTED the report id: `createDraft` posted to the Reports\n * table and whatever record id came back became `reports.id` in Turso. That is why\n * a `site_<ULID>` site could not receive a report at all — Airtable has no\n * Websites record for the `Site` link to point at, so `createDraft` refused the\n * site by name (#646 step 3). Turso mints `report_<ULID>` here instead and owns\n * the row, exactly as `ensure-site` does for sites.\n *\n * ## The Airtable shadow for a NEW report: none\n *\n * Airtable cannot be told what record id to use. A shadow record would therefore\n * carry an id that is NOT the report id, which every later write addresses — the\n * approve stamp, the queue flag, `Sent at`, the delivery status — so the shadow\n * would be unreachable from the moment it was written and would drift on its\n * first update. Every Airtable report writer now skips a non-`rec` id with the\n * same `AIRTABLE_SHADOW skipped=non-rec-id` line the site writers log, and a\n * minted report id is never a `rec` id, so the create skips too. This function\n * logs that skip rather than staying silent about it, the same way\n * `ensureSite` does for a new site.\n *\n * Pre-existing `rec…` reports are untouched: both shapes coexist permanently, and\n * a `rec` report's Airtable row still receives its shadow writes until step 6.\n */\nimport { draftFields, type DraftInput } from \"./draft-fields.js\";\nimport { mintReportId } from \"../fleet/report-id.js\";\nimport { skipsAirtableShadow } from \"../fleet/site-id.js\";\nimport type { ReportRow } from \"./report-row.js\";\nimport type { ReportType } from \"./types.js\";\n\n/** The one Turso operation creating a report needs: insert the row and hand back\n * what was STORED. Implemented by the report writer (`makeReportMirror().create`),\n * which owns the db handle and the freeze's error semantics. Reading the row back\n * rather than mapping the input is the same rule the create mirror already\n * followed: the caller gets what the store holds, so a coercion can never\n * diverge the returned row from the persisted one. */\nexport type ReportInserter = (rec: {\n id: string;\n fields: Record<string, unknown>;\n}) => Promise<ReportRow>;\n\nexport type CreateReportDeps = {\n create: ReportInserter;\n /** Injectable for tests; defaults to {@link mintReportId}. */\n mintId?: () => string;\n};\n\n/** Mint a report id, write the row to Turso, and return the stored row. */\nexport async function createReportDraft(\n input: DraftInput,\n deps: CreateReportDeps,\n): Promise<ReportRow> {\n const id = (deps.mintId ?? mintReportId)();\n const row = await deps.create({ id, fields: draftFields(input) });\n // Logged, not attempted — see \"The Airtable shadow for a NEW report\" above.\n skipsAirtableShadow(\"createReportDraft\", id);\n return row;\n}\n\n/**\n * The `(site, type, period)` idempotency lookup behind search-before-create\n * drafting — the re-run dedupe the `launch` and `announce` recipes make before\n * they draft.\n *\n * It replaces the Airtable `findReportByPeriod`, whose whole design was a\n * workaround: Airtable's formula layer renders a linked-record field as the\n * linked rows' primary-field NAMES, so no formula could filter by site id and\n * the site had to be matched client-side after fetching every row of the\n * (type, period). Here the site scope IS the query (`forSite`, served by\n * idx_reports_site) and the triple is matched in memory over one site's handful\n * of reports.\n */\nexport async function findReportForPeriod(\n store: { forSite: (siteId: string) => Promise<ReportRow[]> },\n siteId: string,\n reportType: ReportType,\n period: string,\n): Promise<ReportRow | null> {\n const rows = await store.forSite(siteId);\n return rows.find((r) => r.reportType === reportType && r.period === period) ?? null;\n}\n"],"mappings":";;;;;;;;;;;;;AAUO,IAAM,cAA0C;AAAA,EACrD,aAAa;AAAA,EACb,SAAS;AAAA,EACT,cAAc;AAAA,EACd,QAAQ;AACV;AAEO,SAAS,WAAW,MAA0B;AACnD,SAAO,YAAY,IAAI;AACzB;AAoCA,eAAsB,WACpB,MACA,QACA,QACuB;AACvB,QAAM,WAAW,OAAO,IAAY,UAAkC;AACpE,UAAM,cAAc,MAAM,IAAI,KAAK;AACnC,UAAM,OAAO,MAAM,IAAI,EAAE,aAAa,QAAQ,IAAI,EAAE,CAAC;AAAA,EACvD;AACA,QAAM,UAAU,WAAW,OAAO,UAAU;AAC5C,QAAM,UAAU,MAAM,OAAO,QAAQ,OAAO,MAAM,GAC/C,OAAO,iBAAiB,EACxB,OAAO,CAAC,MAAM,EAAE,OAAO,OAAO,EAAE;AAEnC,QAAM,UAAU,OAAO,KAAK,CAAC,MAAM,WAAW,EAAE,UAAU,KAAK,OAAO;AACtE,MAAI,SAAS;AAGX,UAAM,SAAS,OAAO,IAAI,KAAK;AAC/B,WAAO,EAAE,QAAQ,OAAO,WAAW,QAAQ,YAAY,eAAe,CAAC,EAAE;AAAA,EAC3E;AAGA,QAAM,gBAA0B,CAAC;AACjC,aAAW,KAAK,QAAQ;AACtB,UAAM,SAAS,EAAE,IAAI,KAAK;AAC1B,kBAAc,KAAK,EAAE,EAAE;AAAA,EACzB;AACA,QAAM,SAAS,OAAO,IAAI,IAAI;AAC9B,SAAO,EAAE,QAAQ,MAAM,cAAc;AACvC;;;AC1DO,SAAS,aAAa,MAAc,KAAK,IAAI,GAAW;AAC7D,QAAM,SAAS,IAAI,WAAW,EAAE;AAChC,aAAW,OAAO,gBAAgB,MAAM;AACxC,SAAO,UAAU,WAAW,KAAK,MAAM,CAAC;AAC1C;;;ACmBA,eAAsB,kBACpB,OACA,MACoB;AACpB,QAAM,MAAM,KAAK,UAAU,cAAc;AACzC,QAAM,MAAM,MAAM,KAAK,OAAO,EAAE,IAAI,QAAQ,YAAY,KAAK,EAAE,CAAC;AAEhE,sBAAoB,qBAAqB,EAAE;AAC3C,SAAO;AACT;AAeA,eAAsB,oBACpB,OACA,QACA,YACA,QAC2B;AAC3B,QAAM,OAAO,MAAM,MAAM,QAAQ,MAAM;AACvC,SAAO,KAAK,KAAK,CAAC,MAAM,EAAE,eAAe,cAAc,EAAE,WAAW,MAAM,KAAK;AACjF;","names":[]}
@@ -5,22 +5,14 @@ import {
5
5
  import {
6
6
  isProbablyEmail,
7
7
  parseAddresses
8
- } from "./chunk-2ZAZ7ETT.js";
9
- import {
10
- listAllReports
11
- } from "./chunk-F3JWOPTG.js";
8
+ } from "./chunk-LDHRIQUW.js";
12
9
  import {
13
10
  gatingHealth,
14
11
  isSendOverridden
15
12
  } from "./chunk-VAD43MOR.js";
16
13
  import {
17
- openBase,
18
- readAirtableConfig
19
- } from "./chunk-HN6I2XWI.js";
20
- import {
21
- listWebsites,
22
14
  siteSlug
23
- } from "./chunk-3Z6X3BHU.js";
15
+ } from "./chunk-ICSRWW5Q.js";
24
16
 
25
17
  // src/reports/preflight.ts
26
18
  var OPERATOR_DOMAINS = ["reddoorla.com", "tuckerlemos.com"];
@@ -150,13 +142,13 @@ function preflightSite(site, reports, type, now) {
150
142
  findings.push({
151
143
  level: "warn",
152
144
  check: "scores-missing",
153
- message: "Websites row is missing Lighthouse scores \u2014 `announce` will skip this site; run `audit lighthouse --write-airtable` first"
145
+ message: "Websites row is missing Lighthouse scores \u2014 `announce` will skip this site; run `audit lighthouse --write-back` first"
154
146
  });
155
147
  } else {
156
148
  findings.push({
157
149
  level: "fail",
158
150
  check: "scores-missing",
159
- message: "Websites row is missing Lighthouse scores \u2014 drafting will throw (scoresFromWebsite); run `audit lighthouse --write-airtable` first"
151
+ message: "Websites row is missing Lighthouse scores \u2014 drafting will throw (scoresFromWebsite); run `audit lighthouse --write-back` first"
160
152
  });
161
153
  }
162
154
  }
@@ -240,12 +232,11 @@ function preflightFleet(sites) {
240
232
  return findings;
241
233
  }
242
234
  async function preflight(deps) {
243
- const base = deps?.base ?? openBase(readAirtableConfig());
244
235
  const type = deps?.type ?? "Announcement";
245
236
  const now = deps?.now ?? /* @__PURE__ */ new Date();
246
- const websites = await listWebsites(base);
237
+ const websites = await deps.roster();
247
238
  const selected = deps?.site ? websites.filter((w) => siteSlug(w.name) === siteSlug(deps.site)) : type === "Announcement" ? websites.filter((w) => w.status === "maintained") : websites.filter((w) => w.status === null || ELIGIBLE_STATUSES.has(w.status));
248
- const allReports = selected.length > 0 ? await listAllReports(base) : [];
239
+ const allReports = selected.length > 0 ? await deps.allReports() : [];
249
240
  const results = selected.map(
250
241
  (site) => preflightSite(
251
242
  site,
@@ -341,4 +332,4 @@ export {
341
332
  preflight,
342
333
  approveBlockers
343
334
  };
344
- //# sourceMappingURL=chunk-R5IMFAFR.js.map
335
+ //# sourceMappingURL=chunk-VMGHQUTV.js.map