@reddoorla/maintenance 0.85.1 → 0.86.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 (252) hide show
  1. package/README.md +8 -0
  2. package/dist/airtable-G7WBQQMI.js +9 -0
  3. package/dist/{announce-GIAYN67S.js → announce-MXTUQ6C7.js} +26 -19
  4. package/dist/announce-MXTUQ6C7.js.map +1 -0
  5. package/dist/{bump-deps-SWJB6H5V.js → bump-deps-EC74CPLS.js} +9 -9
  6. package/dist/chunk-2EVEFOZY.js +129 -0
  7. package/dist/chunk-2EVEFOZY.js.map +1 -0
  8. package/dist/{chunk-COZPL62X.js → chunk-2NDRHXMR.js} +25 -17
  9. package/dist/chunk-2NDRHXMR.js.map +1 -0
  10. package/dist/{chunk-SDUJIANJ.js → chunk-2NJQLSEU.js} +3 -3
  11. package/dist/chunk-2NQUCS7O.js +243 -0
  12. package/dist/chunk-2NQUCS7O.js.map +1 -0
  13. package/dist/{chunk-QRDB4NLB.js → chunk-2ZPT7GLV.js} +43 -404
  14. package/dist/chunk-2ZPT7GLV.js.map +1 -0
  15. package/dist/{chunk-ZTPPCHDB.js → chunk-3G25KIWW.js} +4 -4
  16. package/dist/chunk-3SF7D4DY.js +359 -0
  17. package/dist/chunk-3SF7D4DY.js.map +1 -0
  18. package/dist/{chunk-Q276MZ7E.js → chunk-3VQWSE3F.js} +4 -4
  19. package/dist/{chunk-37S6L6G4.js → chunk-5UINJNAJ.js} +72 -2
  20. package/dist/chunk-5UINJNAJ.js.map +1 -0
  21. package/dist/{chunk-THEFTZ7F.js → chunk-62CMMGEO.js} +5 -5
  22. package/dist/{chunk-FDIFTIXC.js → chunk-6TWWY7ZU.js} +2 -2
  23. package/dist/{chunk-UYCAF77E.js → chunk-7RJ2HMMJ.js} +3 -3
  24. package/dist/chunk-ACBRLLFT.js +198 -0
  25. package/dist/chunk-ACBRLLFT.js.map +1 -0
  26. package/dist/chunk-BNM5POSJ.js +284 -0
  27. package/dist/chunk-BNM5POSJ.js.map +1 -0
  28. package/dist/{chunk-R6FPFMOS.js → chunk-BQRFQSGY.js} +3 -3
  29. package/dist/{chunk-MGR2H7F4.js → chunk-BYK66EWO.js} +19 -12
  30. package/dist/chunk-BYK66EWO.js.map +1 -0
  31. package/dist/{chunk-N4MOEF32.js → chunk-C6B4RHXR.js} +36 -8
  32. package/dist/chunk-C6B4RHXR.js.map +1 -0
  33. package/dist/{chunk-6QRLRT3P.js → chunk-CQR4LIM7.js} +12 -265
  34. package/dist/chunk-CQR4LIM7.js.map +1 -0
  35. package/dist/{chunk-SEEA6RXF.js → chunk-DJZWYIPP.js} +2 -2
  36. package/dist/chunk-DJZWYIPP.js.map +1 -0
  37. package/dist/chunk-EVBCK2KC.js +68 -0
  38. package/dist/chunk-EVBCK2KC.js.map +1 -0
  39. package/dist/{chunk-TP5M2NQB.js → chunk-F5KM4LEQ.js} +3 -3
  40. package/dist/{chunk-62VF32IC.js → chunk-ILUNZHYV.js} +2 -2
  41. package/dist/chunk-ITIOFN7A.js +396 -0
  42. package/dist/chunk-ITIOFN7A.js.map +1 -0
  43. package/dist/{chunk-LS6QPANO.js → chunk-IYSEC543.js} +2 -2
  44. package/dist/{chunk-DC5H7KEQ.js → chunk-JHGHLKFS.js} +2 -2
  45. package/dist/{chunk-PBWDDDQH.js → chunk-KTH5ZODA.js} +2 -2
  46. package/dist/{chunk-4DHQCN5H.js → chunk-LBYOLBW7.js} +17 -2
  47. package/dist/chunk-LBYOLBW7.js.map +1 -0
  48. package/dist/{chunk-577HCGZO.js → chunk-LDWZSLSU.js} +38 -23
  49. package/dist/chunk-LDWZSLSU.js.map +1 -0
  50. package/dist/{chunk-LTB6KVJW.js → chunk-N45M3FDK.js} +13 -11
  51. package/dist/{chunk-LTB6KVJW.js.map → chunk-N45M3FDK.js.map} +1 -1
  52. package/dist/chunk-NYUYNYQL.js +48 -0
  53. package/dist/chunk-NYUYNYQL.js.map +1 -0
  54. package/dist/{chunk-7J53SUT6.js → chunk-P2FE5AI6.js} +4 -4
  55. package/dist/{chunk-3YXDSCM6.js → chunk-PPE5KZJK.js} +29 -11
  56. package/dist/chunk-PPE5KZJK.js.map +1 -0
  57. package/dist/chunk-PTDMVGKL.js +31 -0
  58. package/dist/chunk-PTDMVGKL.js.map +1 -0
  59. package/dist/{chunk-D4FVAIEJ.js → chunk-PZR4EBPY.js} +5 -2
  60. package/dist/chunk-PZR4EBPY.js.map +1 -0
  61. package/dist/{chunk-NWB4O5EY.js → chunk-SLUQ7R6I.js} +3 -3
  62. package/dist/chunk-SLUQ7R6I.js.map +1 -0
  63. package/dist/{chunk-GZNBCHAZ.js → chunk-TERE5SDF.js} +4 -4
  64. package/dist/{chunk-ZMIPG6TR.js → chunk-TVL5MVUF.js} +4 -4
  65. package/dist/chunk-UDRHAQT7.js +172 -0
  66. package/dist/chunk-UDRHAQT7.js.map +1 -0
  67. package/dist/chunk-VAD43MOR.js +72 -0
  68. package/dist/chunk-VAD43MOR.js.map +1 -0
  69. package/dist/chunk-VSBFM7DS.js +229 -0
  70. package/dist/chunk-VSBFM7DS.js.map +1 -0
  71. package/dist/chunk-W7Z3BGCI.js +274 -0
  72. package/dist/chunk-W7Z3BGCI.js.map +1 -0
  73. package/dist/chunk-X3B723J4.js +1209 -0
  74. package/dist/chunk-X3B723J4.js.map +1 -0
  75. package/dist/{chunk-VRWUL4TR.js → chunk-XHHMM5HX.js} +2 -2
  76. package/dist/{chunk-RJB3QUSP.js → chunk-XIDUYLSR.js} +3 -3
  77. package/dist/{chunk-QG6OSL4M.js → chunk-XUGSZP7Z.js} +2 -2
  78. package/dist/{chunk-R4AB6GCT.js → chunk-Y53WOQIJ.js} +7 -4
  79. package/dist/{chunk-R4AB6GCT.js.map → chunk-Y53WOQIJ.js.map} +1 -1
  80. package/dist/{chunk-ICXV25N7.js → chunk-Z3KYFUN6.js} +3 -3
  81. package/dist/{chunk-MPBXOJ3N.js → chunk-Z7AXYKQB.js} +33 -2
  82. package/dist/chunk-Z7AXYKQB.js.map +1 -0
  83. package/dist/{chunk-6V7565O2.js → chunk-ZKXSX4LO.js} +11 -11
  84. package/dist/cli/bin.js +46 -28
  85. package/dist/cli/bin.js.map +1 -1
  86. package/dist/cli/commands/audit.d.ts +30 -1
  87. package/dist/cli/commands/audit.js +63 -31
  88. package/dist/cli/commands/audit.js.map +1 -1
  89. package/dist/client-AUE3XUPO.js +11 -0
  90. package/dist/configs/playwright-a11y.js +1 -1
  91. package/dist/configs/svelte.d.ts +30 -1
  92. package/dist/configs/svelte.js +9 -1
  93. package/dist/configs/svelte.js.map +1 -1
  94. package/dist/{convert-to-pnpm-AT52NWZB.js → convert-to-pnpm-6L6P74WR.js} +9 -9
  95. package/dist/db-B35OGV3G.js +217 -0
  96. package/dist/db-B35OGV3G.js.map +1 -0
  97. package/dist/{digest-UVHYZUYE.js → digest-BL6UJYKH.js} +23 -20
  98. package/dist/{digest-UVHYZUYE.js.map → digest-BL6UJYKH.js.map} +1 -1
  99. package/dist/dump-P6EBSIOH.js +68 -0
  100. package/dist/dump-P6EBSIOH.js.map +1 -0
  101. package/dist/email-VTTTKKL5.js +195 -0
  102. package/dist/email-VTTTKKL5.js.map +1 -0
  103. package/dist/{ensure-site-S25MDDHW.js → ensure-site-DFTB7MN3.js} +8 -7
  104. package/dist/ensure-site-DFTB7MN3.js.map +1 -0
  105. package/dist/fleet-events-cF2Om88w.d.ts +24 -0
  106. package/dist/{forms-notify-target-TWN4EH6K.js → forms-notify-target-75U6OU7L.js} +38 -132
  107. package/dist/forms-notify-target-75U6OU7L.js.map +1 -0
  108. package/dist/{github-signals-G4ILZAJ6.js → github-signals-IJUPGCUX.js} +40 -19
  109. package/dist/github-signals-IJUPGCUX.js.map +1 -0
  110. package/dist/{header-image-T7XD37JL.js → header-image-AWLLQA4L.js} +34 -11
  111. package/dist/header-image-AWLLQA4L.js.map +1 -0
  112. package/dist/header-images-335L2AKB.js +68 -0
  113. package/dist/header-images-335L2AKB.js.map +1 -0
  114. package/dist/{health-endpoint-242WG2OJ.js → health-endpoint-WJC2UMYS.js} +9 -9
  115. package/dist/health-mirror-4B2L7T2Y.js +75 -0
  116. package/dist/health-mirror-4B2L7T2Y.js.map +1 -0
  117. package/dist/import-airtable-HE43FB4H.js +35 -0
  118. package/dist/index.d.ts +175 -21
  119. package/dist/index.js +116 -74
  120. package/dist/index.js.map +1 -1
  121. package/dist/{init-TFVAN5OY.js → init-NJEOEO7I.js} +21 -18
  122. package/dist/{init-TFVAN5OY.js.map → init-NJEOEO7I.js.map} +1 -1
  123. package/dist/{launch-DWY2MF54.js → launch-XNAX427U.js} +32 -26
  124. package/dist/{launch-DWY2MF54.js.map → launch-XNAX427U.js.map} +1 -1
  125. package/dist/lighthouse-VHI77PFB.js +11 -0
  126. package/dist/{lighthouse-airtable-BIPM26SZ.js → lighthouse-airtable-LBVIYYP5.js} +3 -3
  127. package/dist/mailchimp-XYCTEEJY.js +72 -0
  128. package/dist/mailchimp-XYCTEEJY.js.map +1 -0
  129. package/dist/migrate-GQH237A6.js +7 -0
  130. package/dist/notify-C645PGD5.js +22 -0
  131. package/dist/{onboard-53CITIM7.js → onboard-GK4PHPNI.js} +9 -9
  132. package/dist/orchestrate-77UO4BYJ.js +30 -0
  133. package/dist/orchestrate-77UO4BYJ.js.map +1 -0
  134. package/dist/parity-MWS24A3H.js +12 -0
  135. package/dist/parity-MWS24A3H.js.map +1 -0
  136. package/dist/pipeline-S6CVHDGB.js +12 -0
  137. package/dist/pipeline-S6CVHDGB.js.map +1 -0
  138. package/dist/{preflight-3XPDUHFJ.js → preflight-6WBNU2GZ.js} +16 -13
  139. package/dist/{preflight-3XPDUHFJ.js.map → preflight-6WBNU2GZ.js.map} +1 -1
  140. package/dist/{prismic-ci-UTQ6LRR3.js → prismic-ci-3RHOZUUT.js} +15 -15
  141. package/dist/{prismic-models-PVES6R5V.js → prismic-models-QBGUT2Q3.js} +10 -10
  142. package/dist/prospect-audit-IUNCJ2LV.js +174 -0
  143. package/dist/prospect-audit-IUNCJ2LV.js.map +1 -0
  144. package/dist/prospect-audits-3PMIO73D.js +19 -0
  145. package/dist/prospect-audits-3PMIO73D.js.map +1 -0
  146. package/dist/{protection-audit-64PLLBLI.js → protection-audit-SJO53MEQ.js} +3 -3
  147. package/dist/recipes/sync-configs.js +3 -3
  148. package/dist/render-AQAVQDPZ.js +10 -0
  149. package/dist/render-AQAVQDPZ.js.map +1 -0
  150. package/dist/{renovate-dispatch-ICJGSFCE.js → renovate-dispatch-QJY75ODR.js} +8 -8
  151. package/dist/replay-C2LXMJZP.js +66 -0
  152. package/dist/replay-C2LXMJZP.js.map +1 -0
  153. package/dist/{report-6CKSWJNG.js → report-4EVNDHOZ.js} +55 -29
  154. package/dist/report-4EVNDHOZ.js.map +1 -0
  155. package/dist/reports/header-image/assets/headline-announcement.png +0 -0
  156. package/dist/reports/header-image/assets/headline-launch.png +0 -0
  157. package/dist/reports/header-image/assets/headline-maintenance.png +0 -0
  158. package/dist/reports/header-image/assets/headline-testing.png +0 -0
  159. package/dist/reports/header-image/assets/plate-clean.png +0 -0
  160. package/dist/{self-updating-PPQVV6UA.js → self-updating-6DWI2Z56.js} +9 -9
  161. package/dist/{selftest-CQ6NOX52.js → selftest-RC36MTKP.js} +25 -20
  162. package/dist/selftest-RC36MTKP.js.map +1 -0
  163. package/dist/smoke-B2GQOSIZ.js +20 -0
  164. package/dist/smoke-B2GQOSIZ.js.map +1 -0
  165. package/dist/{smoke-suite-OBWNPALQ.js → smoke-suite-VYY2LXM2.js} +9 -9
  166. package/dist/{submissions-GJ6ITBRH.js → submissions-Q7DRFOCD.js} +3 -3
  167. package/dist/{svelte-codemods-ZOBDHIES.js → svelte-codemods-25J2FO22.js} +9 -9
  168. package/dist/sync-34HNZZ56.js +44 -0
  169. package/dist/sync-34HNZZ56.js.map +1 -0
  170. package/dist/{sync-configs-CJXUX6MD.js → sync-configs-3SE6PSCO.js} +9 -9
  171. package/dist/{upgrade-WLX27UPC.js → upgrade-WBRQ62FX.js} +9 -9
  172. package/dist/{webflow-624FEBFD.js → webflow-IRKOIT7S.js} +4 -4
  173. package/dist/webhook-VOGOW3W6.js +38 -0
  174. package/dist/webhook-VOGOW3W6.js.map +1 -0
  175. package/dist/{websites-SADZR4XQ.js → websites-SXWTP72S.js} +6 -2
  176. package/dist/websites-SXWTP72S.js.map +1 -0
  177. package/dist/{write-audits-to-airtable-7PH75YWY.js → write-audits-to-airtable-MBEILLPU.js} +4 -4
  178. package/dist/write-audits-to-airtable-MBEILLPU.js.map +1 -0
  179. package/package.json +5 -2
  180. package/dist/airtable-HRKSA7V4.js +0 -9
  181. package/dist/announce-GIAYN67S.js.map +0 -1
  182. package/dist/chunk-37S6L6G4.js.map +0 -1
  183. package/dist/chunk-3YXDSCM6.js.map +0 -1
  184. package/dist/chunk-4DHQCN5H.js.map +0 -1
  185. package/dist/chunk-4SOCPK7A.js +0 -108
  186. package/dist/chunk-4SOCPK7A.js.map +0 -1
  187. package/dist/chunk-577HCGZO.js.map +0 -1
  188. package/dist/chunk-6QRLRT3P.js.map +0 -1
  189. package/dist/chunk-COZPL62X.js.map +0 -1
  190. package/dist/chunk-D4FVAIEJ.js.map +0 -1
  191. package/dist/chunk-MGR2H7F4.js.map +0 -1
  192. package/dist/chunk-MPBXOJ3N.js.map +0 -1
  193. package/dist/chunk-N4MOEF32.js.map +0 -1
  194. package/dist/chunk-NWB4O5EY.js.map +0 -1
  195. package/dist/chunk-QRDB4NLB.js.map +0 -1
  196. package/dist/chunk-SEEA6RXF.js.map +0 -1
  197. package/dist/chunk-U5SZ4FOP.js +0 -26
  198. package/dist/chunk-U5SZ4FOP.js.map +0 -1
  199. package/dist/client-X5WUJWDE.js +0 -11
  200. package/dist/db-JSIZYT23.js +0 -23
  201. package/dist/db-JSIZYT23.js.map +0 -1
  202. package/dist/ensure-site-S25MDDHW.js.map +0 -1
  203. package/dist/forms-notify-target-TWN4EH6K.js.map +0 -1
  204. package/dist/github-signals-G4ILZAJ6.js.map +0 -1
  205. package/dist/header-image-T7XD37JL.js.map +0 -1
  206. package/dist/migrate-LJUFGXQZ.js +0 -7
  207. package/dist/orchestrate-KDNXZQX7.js +0 -27
  208. package/dist/report-6CKSWJNG.js.map +0 -1
  209. package/dist/reports/header-image/assets/plate.png +0 -0
  210. package/dist/selftest-CQ6NOX52.js.map +0 -1
  211. /package/dist/{airtable-HRKSA7V4.js.map → airtable-G7WBQQMI.js.map} +0 -0
  212. /package/dist/{bump-deps-SWJB6H5V.js.map → bump-deps-EC74CPLS.js.map} +0 -0
  213. /package/dist/{chunk-SDUJIANJ.js.map → chunk-2NJQLSEU.js.map} +0 -0
  214. /package/dist/{chunk-ZTPPCHDB.js.map → chunk-3G25KIWW.js.map} +0 -0
  215. /package/dist/{chunk-Q276MZ7E.js.map → chunk-3VQWSE3F.js.map} +0 -0
  216. /package/dist/{chunk-THEFTZ7F.js.map → chunk-62CMMGEO.js.map} +0 -0
  217. /package/dist/{chunk-FDIFTIXC.js.map → chunk-6TWWY7ZU.js.map} +0 -0
  218. /package/dist/{chunk-UYCAF77E.js.map → chunk-7RJ2HMMJ.js.map} +0 -0
  219. /package/dist/{chunk-R6FPFMOS.js.map → chunk-BQRFQSGY.js.map} +0 -0
  220. /package/dist/{chunk-TP5M2NQB.js.map → chunk-F5KM4LEQ.js.map} +0 -0
  221. /package/dist/{chunk-62VF32IC.js.map → chunk-ILUNZHYV.js.map} +0 -0
  222. /package/dist/{chunk-LS6QPANO.js.map → chunk-IYSEC543.js.map} +0 -0
  223. /package/dist/{chunk-DC5H7KEQ.js.map → chunk-JHGHLKFS.js.map} +0 -0
  224. /package/dist/{chunk-PBWDDDQH.js.map → chunk-KTH5ZODA.js.map} +0 -0
  225. /package/dist/{chunk-7J53SUT6.js.map → chunk-P2FE5AI6.js.map} +0 -0
  226. /package/dist/{chunk-GZNBCHAZ.js.map → chunk-TERE5SDF.js.map} +0 -0
  227. /package/dist/{chunk-ZMIPG6TR.js.map → chunk-TVL5MVUF.js.map} +0 -0
  228. /package/dist/{chunk-VRWUL4TR.js.map → chunk-XHHMM5HX.js.map} +0 -0
  229. /package/dist/{chunk-RJB3QUSP.js.map → chunk-XIDUYLSR.js.map} +0 -0
  230. /package/dist/{chunk-QG6OSL4M.js.map → chunk-XUGSZP7Z.js.map} +0 -0
  231. /package/dist/{chunk-ICXV25N7.js.map → chunk-Z3KYFUN6.js.map} +0 -0
  232. /package/dist/{chunk-6V7565O2.js.map → chunk-ZKXSX4LO.js.map} +0 -0
  233. /package/dist/{client-X5WUJWDE.js.map → client-AUE3XUPO.js.map} +0 -0
  234. /package/dist/{convert-to-pnpm-AT52NWZB.js.map → convert-to-pnpm-6L6P74WR.js.map} +0 -0
  235. /package/dist/{health-endpoint-242WG2OJ.js.map → health-endpoint-WJC2UMYS.js.map} +0 -0
  236. /package/dist/{lighthouse-airtable-BIPM26SZ.js.map → import-airtable-HE43FB4H.js.map} +0 -0
  237. /package/dist/{migrate-LJUFGXQZ.js.map → lighthouse-VHI77PFB.js.map} +0 -0
  238. /package/dist/{orchestrate-KDNXZQX7.js.map → lighthouse-airtable-LBVIYYP5.js.map} +0 -0
  239. /package/dist/{websites-SADZR4XQ.js.map → migrate-GQH237A6.js.map} +0 -0
  240. /package/dist/{write-audits-to-airtable-7PH75YWY.js.map → notify-C645PGD5.js.map} +0 -0
  241. /package/dist/{onboard-53CITIM7.js.map → onboard-GK4PHPNI.js.map} +0 -0
  242. /package/dist/{prismic-ci-UTQ6LRR3.js.map → prismic-ci-3RHOZUUT.js.map} +0 -0
  243. /package/dist/{prismic-models-PVES6R5V.js.map → prismic-models-QBGUT2Q3.js.map} +0 -0
  244. /package/dist/{protection-audit-64PLLBLI.js.map → protection-audit-SJO53MEQ.js.map} +0 -0
  245. /package/dist/{renovate-dispatch-ICJGSFCE.js.map → renovate-dispatch-QJY75ODR.js.map} +0 -0
  246. /package/dist/{self-updating-PPQVV6UA.js.map → self-updating-6DWI2Z56.js.map} +0 -0
  247. /package/dist/{smoke-suite-OBWNPALQ.js.map → smoke-suite-VYY2LXM2.js.map} +0 -0
  248. /package/dist/{submissions-GJ6ITBRH.js.map → submissions-Q7DRFOCD.js.map} +0 -0
  249. /package/dist/{svelte-codemods-ZOBDHIES.js.map → svelte-codemods-25J2FO22.js.map} +0 -0
  250. /package/dist/{sync-configs-CJXUX6MD.js.map → sync-configs-3SE6PSCO.js.map} +0 -0
  251. /package/dist/{upgrade-WLX27UPC.js.map → upgrade-WBRQ62FX.js.map} +0 -0
  252. /package/dist/{webflow-624FEBFD.js.map → webflow-IRKOIT7S.js.map} +0 -0
@@ -0,0 +1,195 @@
1
+ import {
2
+ renderProspectReport
3
+ } from "./chunk-W7Z3BGCI.js";
4
+ import "./chunk-X3B723J4.js";
5
+ import {
6
+ escapeHtml,
7
+ safeUrl
8
+ } from "./chunk-DJZWYIPP.js";
9
+ import {
10
+ defaultResendClient
11
+ } from "./chunk-WEKL2HYJ.js";
12
+ import {
13
+ hostnameOf
14
+ } from "./chunk-EVBCK2KC.js";
15
+
16
+ // src/prospect/email.ts
17
+ var FROM_ADDRESS = "Reddoor Reports <reports@reddoorla.com>";
18
+ var IMPACT_ORDER = { high: 0, medium: 1, low: 2 };
19
+ var TOP_FIX_COUNT = 3;
20
+ function resolveName(result) {
21
+ return result.businessName && result.businessName.trim() ? result.businessName : hostnameOf(result.url);
22
+ }
23
+ function subjectSafe(s) {
24
+ return s.replace(/\s+/g, " ").trim();
25
+ }
26
+ function formatIsoDate(iso) {
27
+ const d = new Date(iso);
28
+ if (Number.isNaN(d.getTime())) return iso;
29
+ return d.toLocaleDateString("en-US", {
30
+ year: "numeric",
31
+ month: "long",
32
+ day: "numeric",
33
+ timeZone: "UTC"
34
+ });
35
+ }
36
+ function notMeasuredLines(result) {
37
+ const lines = [];
38
+ if (!result.checks.ok) {
39
+ lines.push({
40
+ label: "Findability",
41
+ reason: `the checks stage failed \u2014 ${result.checks.error}`
42
+ });
43
+ lines.push({
44
+ label: "Readability",
45
+ reason: `the checks stage failed \u2014 ${result.checks.error}`
46
+ });
47
+ } else {
48
+ const c = result.checks.data;
49
+ if (result.scores.findability === null) {
50
+ lines.push({
51
+ label: "Findability",
52
+ reason: !c.crawlerAccessMeasured ? "robots.txt could not be fetched, so crawler access wasn't measured" : "every crawled page failed to produce readable content"
53
+ });
54
+ }
55
+ if (result.scores.readability === null) {
56
+ lines.push({
57
+ label: "Readability",
58
+ reason: "no page produced a comparable raw/rendered pair to measure JavaScript dependence"
59
+ });
60
+ }
61
+ }
62
+ if (!result.lighthouse.ok) {
63
+ lines.push({ label: "Lighthouse", reason: result.lighthouse.error });
64
+ }
65
+ if (!result.analyze.ok) {
66
+ lines.push({ label: "Answers", reason: result.analyze.error });
67
+ } else if (result.scores.answers === null) {
68
+ lines.push({ label: "Answers", reason: "the model found no buyer questions to check" });
69
+ }
70
+ if (!result.probes.ok) {
71
+ lines.push({ label: "AI Visibility", reason: result.probes.error });
72
+ } else if (result.scores.aiVisibility === null) {
73
+ lines.push({ label: "AI Visibility", reason: "no category (buyer-question) query ran" });
74
+ }
75
+ return lines;
76
+ }
77
+ var SCORE_FIELDS = [
78
+ { key: "findability", label: "Findability" },
79
+ { key: "readability", label: "Readability" },
80
+ { key: "answers", label: "Answers" },
81
+ { key: "aiVisibility", label: "AI Visibility" }
82
+ ];
83
+ function scoresTableHtml(scores) {
84
+ const cells = SCORE_FIELDS.map(({ key, label }) => {
85
+ const value = scores[key];
86
+ const display = value === null ? "Not measured" : String(value);
87
+ const valueStyle = value === null ? "color:#8a857e;font-size:14px;" : "color:#d71920;font-size:28px;";
88
+ return `<td style="padding:12px 16px;border:1px solid #e6e1da;text-align:center;">
89
+ <div style="font-weight:600;${valueStyle}">${escapeHtml(display)}</div>
90
+ <div style="font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:#57544f;margin-top:4px;">${escapeHtml(label)}</div>
91
+ </td>`;
92
+ }).join("");
93
+ return `<table role="presentation" style="border-collapse:collapse;margin:16px 0;"><tr>${cells}</tr></table>`;
94
+ }
95
+ function notMeasuredHtml(lines) {
96
+ if (lines.length === 0) return "";
97
+ const items = lines.map((l) => `<li><strong>${escapeHtml(l.label)}</strong> \u2014 ${escapeHtml(l.reason)}</li>`).join("");
98
+ return `<h2 style="font-size:16px;margin:24px 0 8px;">Not measured</h2><ul style="margin:0;padding-left:20px;">${items}</ul>`;
99
+ }
100
+ function fixesHtml(analyze) {
101
+ if (!analyze.ok) {
102
+ return `<p style="color:#8a857e;margin:8px 0 0;">Fixes not available \u2014 ${escapeHtml(analyze.error)}</p>`;
103
+ }
104
+ const top = [...analyze.data.fixes].sort((a, b) => IMPACT_ORDER[a.impact] - IMPACT_ORDER[b.impact]).slice(0, TOP_FIX_COUNT);
105
+ if (top.length === 0) {
106
+ return `<p style="margin:8px 0 0;">Nothing urgent \u2014 the essentials are already in place.</p>`;
107
+ }
108
+ const items = top.map(
109
+ (f) => `<li><strong>${escapeHtml(f.title)}</strong> \u2014 ${escapeHtml(f.why)} <span style="color:#8a857e;">(${escapeHtml(f.impact)} impact, ${escapeHtml(f.effort)} effort)</span></li>`
110
+ ).join("");
111
+ return `<ol style="margin:8px 0 0;padding-left:20px;">${items}</ol>`;
112
+ }
113
+ function linkHtml(link) {
114
+ if (!link) {
115
+ return `<p style="margin:24px 0 0;color:#8a857e;">No shareable link \u2014 the audit could not be saved to the database. The full report is attached to this email instead.</p>`;
116
+ }
117
+ const safe = safeUrl(link);
118
+ return `<p style="margin:24px 0 0;"><strong>Full report:</strong> <a href="${escapeHtml(safe)}">${escapeHtml(link)}</a></p>`;
119
+ }
120
+ var INTERNAL_NOTE = `<p style="margin:24px 0 0;padding:12px 14px;background:#f4f2ef;border-left:3px solid #d71920;color:#57544f;font-size:14px;">
121
+ Internal sheet \u2014 it names the reasons a section could not be measured. The
122
+ version to send a prospect is the linked report, also attached to this email.
123
+ </p>`;
124
+ function buildAuditEmail(result, opts) {
125
+ const name = resolveName(result);
126
+ const subject = `Prospect audit \u2014 ${subjectSafe(name)}`;
127
+ const html = `<!doctype html>
128
+ <html lang="en">
129
+ <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
130
+ <body style="margin:0;padding:24px;background:#faf8f5;color:#1a1a1a;font:15px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;">
131
+ <h1 style="font-size:22px;margin:0 0 4px;">Prospect audit \u2014 ${escapeHtml(name)}</h1>
132
+ <p style="margin:0 0 16px;color:#57544f;">
133
+ <a href="${escapeHtml(safeUrl(result.url))}">${escapeHtml(result.url)}</a> \xB7 audited ${escapeHtml(formatIsoDate(result.generatedAt))}
134
+ </p>
135
+ ${scoresTableHtml(result.scores)}
136
+ ${notMeasuredHtml(notMeasuredLines(result))}
137
+ <h2 style="font-size:16px;margin:24px 0 8px;">Top fixes</h2>
138
+ ${fixesHtml(result.analyze)}
139
+ ${linkHtml(opts.link)}
140
+ ${INTERNAL_NOTE}
141
+ </body>
142
+ </html>`;
143
+ return { subject, html, attachmentHtml: renderProspectReport(result) };
144
+ }
145
+ function parseProspectAuditRecipients(raw) {
146
+ if (!raw) return [];
147
+ const seen = /* @__PURE__ */ new Set();
148
+ const list = [];
149
+ for (const part of raw.split(",")) {
150
+ const trimmed = part.trim().toLowerCase();
151
+ if (!trimmed) continue;
152
+ if (seen.has(trimmed)) continue;
153
+ seen.add(trimmed);
154
+ list.push(trimmed);
155
+ }
156
+ return list;
157
+ }
158
+ function slugify(s) {
159
+ const slug = s.toLowerCase().replace(/[^a-z0-9.-]+/g, "-").replace(/^-+|-+$/g, "");
160
+ return slug || "prospect";
161
+ }
162
+ async function sendAuditEmail(result, opts) {
163
+ if (opts.recipients.length === 0) {
164
+ return {
165
+ sent: false,
166
+ reason: "no recipients configured (PROSPECT_AUDIT_RECIPIENTS is unset or empty)"
167
+ };
168
+ }
169
+ const built = buildAuditEmail(result, { link: opts.link });
170
+ const attachmentHtml = opts.renderReport ? opts.renderReport(result) : built.attachmentHtml;
171
+ const client = opts.client ?? defaultResendClient();
172
+ const idempotencyKey = `prospect-audit:${opts.auditId ?? `${result.url}#${result.generatedAt}`}`;
173
+ const filename = `prospect-audit-${slugify(hostnameOf(result.url))}.html`;
174
+ const { messageId } = await client.send({
175
+ from: FROM_ADDRESS,
176
+ to: opts.recipients,
177
+ subject: built.subject,
178
+ html: built.html,
179
+ attachments: [
180
+ {
181
+ filename,
182
+ content: Buffer.from(attachmentHtml, "utf-8").toString("base64"),
183
+ contentType: "text/html"
184
+ }
185
+ ],
186
+ idempotencyKey
187
+ });
188
+ return { sent: true, messageId, recipients: opts.recipients };
189
+ }
190
+ export {
191
+ buildAuditEmail,
192
+ parseProspectAuditRecipients,
193
+ sendAuditEmail
194
+ };
195
+ //# sourceMappingURL=email-VTTTKKL5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/prospect/email.ts"],"sourcesContent":["import { escapeHtml, safeUrl } from \"../util/html.js\";\nimport { hostnameOf } from \"../util/url.js\";\nimport { renderProspectReport } from \"./render.js\";\nimport { defaultResendClient, type ResendClient } from \"../reports/send/resend.js\";\nimport type { AnalyzeResult, Fix, ProspectAuditResult, StageResult } from \"./types.js\";\n\n/**\n * Mirrors `reports/send/orchestrate.ts`'s `FROM_ADDRESS` (its line 18) — copied,\n * not imported, exactly like `digest.ts` and `recipes/selftest-email.ts` already\n * do. orchestrate.ts's own `FROM_ADDRESS` isn't exported, and importing it would\n * drag its whole Airtable/MJML dependency chain into this module's graph, which\n * stays lazy-loaded on purpose (see bin.ts's central-dep-blocker comment — this\n * module reaches `resend`, a devDependency, and must only ever be reached via a\n * dynamic `import()` from the CLI).\n */\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\n\nconst IMPACT_ORDER: Record<Fix[\"impact\"], number> = { high: 0, medium: 1, low: 2 };\n\n/** \"top few fixes\" per the design — enough to be useful on an internal sheet\n * without turning it into the full client report. */\nconst TOP_FIX_COUNT = 3;\n\nexport type AuditEmailContent = {\n subject: string;\n html: string;\n /** The FULL client-facing report (render.ts's `renderProspectReport`, pure\n * and deterministic like this function), so `sendAuditEmail` can attach it\n * without a second render call on the default path. */\n attachmentHtml: string;\n};\n\n/** The searchable business name when the pipeline resolved one, else the\n * hostname — mirrors render.ts's identical fallback (types.ts's businessName\n * doc: \"Null when no name was ever resolved... itself a finding, not an\n * error\"). Used for both the subject line and the sheet's own heading. */\nfunction resolveName(result: ProspectAuditResult): string {\n return result.businessName && result.businessName.trim()\n ? result.businessName\n : hostnameOf(result.url);\n}\n\n/**\n * Mail subject headers are plain text, never HTML-rendered by a client — no\n * `escapeHtml` needed there (that would wrongly show literal \"&lt;\" etc. in\n * the subject line). A raw newline/control character IS worth collapsing:\n * a business name that embeds one (scraped page title, model output) would\n * otherwise visibly break the subject, and a stray CR/LF in a header value\n * is the classic email-header-injection vector — defense-in-depth even\n * though the Resend API's JSON transport already neutralizes it here.\n */\nfunction subjectSafe(s: string): string {\n return s.replace(/\\s+/g, \" \").trim();\n}\n\n/** Local copy of render.ts's date formatter — small enough that importing it\n * (render.ts doesn't export it) isn't worth the coupling. */\nfunction formatIsoDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return iso;\n return d.toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n timeZone: \"UTC\",\n });\n}\n\ntype NotMeasuredLine = { label: string; reason: string };\n\n/**\n * Everything on the sheet that reads \"not measured\", and why — in plain\n * words. Unlike render.ts's client-facing report (which collapses every\n * stage failure to STAGE_FAILED_MESSAGE so a stranger never sees operator\n * vocabulary), this email goes to Tucker/Tim/Erik: the real diagnostic\n * (a fetch error, an LLM timeout) is more useful to them than a sanitized\n * phrase, so stage `error` strings are surfaced verbatim (still escaped —\n * they can embed arbitrary text from a fetch response).\n *\n * The four-Scores branches mirror checks.ts's `computeScores` exactly: which\n * stage feeds which score, and the cases where that stage's OWN data still\n * nulls the score even though the stage itself succeeded (crawlerAccessMeasured\n * false, avgMissing null, empty buyerQuestions, a null visibilityScore). If\n * computeScores' conditions change, this needs the matching update.\n */\nfunction notMeasuredLines(result: ProspectAuditResult): NotMeasuredLine[] {\n const lines: NotMeasuredLine[] = [];\n\n if (!result.checks.ok) {\n lines.push({\n label: \"Findability\",\n reason: `the checks stage failed — ${result.checks.error}`,\n });\n lines.push({\n label: \"Readability\",\n reason: `the checks stage failed — ${result.checks.error}`,\n });\n } else {\n const c = result.checks.data;\n if (result.scores.findability === null) {\n lines.push({\n label: \"Findability\",\n reason: !c.crawlerAccessMeasured\n ? \"robots.txt could not be fetched, so crawler access wasn't measured\"\n : \"every crawled page failed to produce readable content\",\n });\n }\n if (result.scores.readability === null) {\n lines.push({\n label: \"Readability\",\n reason: \"no page produced a comparable raw/rendered pair to measure JavaScript dependence\",\n });\n }\n }\n\n if (!result.lighthouse.ok) {\n lines.push({ label: \"Lighthouse\", reason: result.lighthouse.error });\n }\n\n if (!result.analyze.ok) {\n lines.push({ label: \"Answers\", reason: result.analyze.error });\n } else if (result.scores.answers === null) {\n lines.push({ label: \"Answers\", reason: \"the model found no buyer questions to check\" });\n }\n\n if (!result.probes.ok) {\n lines.push({ label: \"AI Visibility\", reason: result.probes.error });\n } else if (result.scores.aiVisibility === null) {\n lines.push({ label: \"AI Visibility\", reason: \"no category (buyer-question) query ran\" });\n }\n\n return lines;\n}\n\nconst SCORE_FIELDS: { key: keyof ProspectAuditResult[\"scores\"]; label: string }[] = [\n { key: \"findability\", label: \"Findability\" },\n { key: \"readability\", label: \"Readability\" },\n { key: \"answers\", label: \"Answers\" },\n { key: \"aiVisibility\", label: \"AI Visibility\" },\n];\n\nfunction scoresTableHtml(scores: ProspectAuditResult[\"scores\"]): string {\n const cells = SCORE_FIELDS.map(({ key, label }) => {\n const value = scores[key];\n const display = value === null ? \"Not measured\" : String(value);\n const valueStyle =\n value === null ? \"color:#8a857e;font-size:14px;\" : \"color:#d71920;font-size:28px;\";\n return `<td style=\"padding:12px 16px;border:1px solid #e6e1da;text-align:center;\">\n <div style=\"font-weight:600;${valueStyle}\">${escapeHtml(display)}</div>\n <div style=\"font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:#57544f;margin-top:4px;\">${escapeHtml(label)}</div>\n </td>`;\n }).join(\"\");\n return `<table role=\"presentation\" style=\"border-collapse:collapse;margin:16px 0;\"><tr>${cells}</tr></table>`;\n}\n\nfunction notMeasuredHtml(lines: NotMeasuredLine[]): string {\n if (lines.length === 0) return \"\";\n const items = lines\n .map((l) => `<li><strong>${escapeHtml(l.label)}</strong> — ${escapeHtml(l.reason)}</li>`)\n .join(\"\");\n return `<h2 style=\"font-size:16px;margin:24px 0 8px;\">Not measured</h2><ul style=\"margin:0;padding-left:20px;\">${items}</ul>`;\n}\n\n/** analyze.error and fix titles/why are untrusted (LLM output, or text lifted\n * from the prospect's own site) — every interpolation below is escaped. */\nfunction fixesHtml(analyze: StageResult<AnalyzeResult>): string {\n if (!analyze.ok) {\n return `<p style=\"color:#8a857e;margin:8px 0 0;\">Fixes not available — ${escapeHtml(analyze.error)}</p>`;\n }\n const top = [...analyze.data.fixes]\n .sort((a, b) => IMPACT_ORDER[a.impact] - IMPACT_ORDER[b.impact])\n .slice(0, TOP_FIX_COUNT);\n if (top.length === 0) {\n return `<p style=\"margin:8px 0 0;\">Nothing urgent — the essentials are already in place.</p>`;\n }\n const items = top\n .map(\n (f) =>\n `<li><strong>${escapeHtml(f.title)}</strong> — ${escapeHtml(f.why)} <span style=\"color:#8a857e;\">(${escapeHtml(f.impact)} impact, ${escapeHtml(f.effort)} effort)</span></li>`,\n )\n .join(\"\");\n return `<ol style=\"margin:8px 0 0;padding-left:20px;\">${items}</ol>`;\n}\n\nfunction linkHtml(link: string | null): string {\n if (!link) {\n return `<p style=\"margin:24px 0 0;color:#8a857e;\">No shareable link — the audit could not be saved to the database. The full report is attached to this email instead.</p>`;\n }\n const safe = safeUrl(link);\n return `<p style=\"margin:24px 0 0;\"><strong>Full report:</strong> <a href=\"${escapeHtml(safe)}\">${escapeHtml(link)}</a></p>`;\n}\n\n/** This sheet is for us, and it says so. It carries the real reasons a stage did\n * not run — useful internally, wrong in front of a prospect — so it has to be\n * obvious that the shareable artefact is the attachment and the link, not this\n * body. Without the label, the natural thing to do with a useful email is\n * forward it. */\nconst INTERNAL_NOTE = `<p style=\"margin:24px 0 0;padding:12px 14px;background:#f4f2ef;border-left:3px solid #d71920;color:#57544f;font-size:14px;\">\n Internal sheet — it names the reasons a section could not be measured. The\n version to send a prospect is the linked report, also attached to this email.\n</p>`;\n\n/**\n * Build the internal sheet — a SHORT summary for Tucker/Tim/Erik, distinct\n * from the client-facing report render.ts builds. Pure: no IO, so it's safe\n * to call from a test with no network. `attachmentHtml` is the full report\n * (render.ts's `renderProspectReport`, itself pure), included here so\n * `sendAuditEmail`'s default path doesn't need a second render call.\n *\n * Every value that can trace back to the prospect's site or the model —\n * the business name, fixes, stage error text, the narrative-adjacent bits —\n * is escaped with `escapeHtml`/`safeUrl`, the same honesty-and-safety rule\n * render.ts follows for the client-facing report.\n */\nexport function buildAuditEmail(\n result: ProspectAuditResult,\n opts: { link: string | null },\n): AuditEmailContent {\n const name = resolveName(result);\n const subject = `Prospect audit — ${subjectSafe(name)}`;\n\n const html = `<!doctype html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /></head>\n<body style=\"margin:0;padding:24px;background:#faf8f5;color:#1a1a1a;font:15px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;\">\n <h1 style=\"font-size:22px;margin:0 0 4px;\">Prospect audit — ${escapeHtml(name)}</h1>\n <p style=\"margin:0 0 16px;color:#57544f;\">\n <a href=\"${escapeHtml(safeUrl(result.url))}\">${escapeHtml(result.url)}</a> · audited ${escapeHtml(formatIsoDate(result.generatedAt))}\n </p>\n ${scoresTableHtml(result.scores)}\n ${notMeasuredHtml(notMeasuredLines(result))}\n <h2 style=\"font-size:16px;margin:24px 0 8px;\">Top fixes</h2>\n ${fixesHtml(result.analyze)}\n ${linkHtml(opts.link)}\n ${INTERNAL_NOTE}\n</body>\n</html>`;\n\n return { subject, html, attachmentHtml: renderProspectReport(result) };\n}\n\n/**\n * Parse `PROSPECT_AUDIT_RECIPIENTS` (comma-separated) into a clean address\n * list: trimmed, empties dropped, case-insensitively deduped — mirrors\n * `reports/send/orchestrate.ts`'s `parseAddresses` for the same reasons (an\n * operator-typed env var, same typo surface). No address is ever hardcoded\n * anywhere in this module; an unset or empty var yields `[]`, which\n * `sendAuditEmail` treats as \"skip, and say why\" rather than an error.\n */\nexport function parseProspectAuditRecipients(raw: string | undefined | null): string[] {\n if (!raw) return [];\n const seen = new Set<string>();\n const list: string[] = [];\n for (const part of raw.split(\",\")) {\n const trimmed = part.trim().toLowerCase();\n if (!trimmed) continue;\n if (seen.has(trimmed)) continue;\n seen.add(trimmed);\n list.push(trimmed);\n }\n return list;\n}\n\n/** Filesystem-safe attachment-filename fragment. Tiny local copy of the CLI's\n * private `slugify` (prospect-audit.ts) — not worth exporting/sharing one\n * three-line predicate across a module boundary. */\nfunction slugify(s: string): string {\n const slug = s\n .toLowerCase()\n .replace(/[^a-z0-9.-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return slug || \"prospect\";\n}\n\nexport type SendAuditEmailOptions = {\n link: string | null;\n recipients: string[];\n /**\n * The persisted `prospect_audits` row id (from `createProspectAudit`), when\n * persistence succeeded. Mirrors orchestrate.ts's\n * `idempotencyKey: \\`report:${report.id}\\`` pattern, so re-sending for the\n * SAME already-persisted audit dedupes at Resend instead of double-mailing.\n * Null when persistence failed — the sheet can still be sent (it's built to\n * survive independently of Turso), but then there is no stable row id, so\n * the key falls back to `url`+`generatedAt`: that still protects a\n * same-process double-call, though — unlike a DB id — it can't survive an\n * entire pipeline rerun, which mints genuinely new content anyway.\n */\n auditId?: string | null;\n /** Defaults to `defaultResendClient()`. */\n client?: ResendClient;\n /** Test seam for the ATTACHMENT content only (production omits it and gets\n * the real `renderProspectReport` output `buildAuditEmail` already\n * computed) — lets a test of this function's sending mechanics (recipients,\n * idempotency key, attachment shape) skip re-deriving the full report. */\n renderReport?: (result: ProspectAuditResult) => string;\n};\n\nexport type SendAuditEmailResult =\n { sent: true; messageId: string; recipients: string[] } | { sent: false; reason: string };\n\n/**\n * Send the audit sheet with the full rendered report attached as an `.html`\n * file (base64), so the sheet survives independently of the link and of\n * Turso. Empty `recipients` is a legitimate, non-exceptional outcome (no\n * `PROSPECT_AUDIT_RECIPIENTS` configured) — returns `{sent:false}` rather\n * than throwing, so a caller can report it as a warning; a genuine send\n * failure (a throwing `client.send`) still propagates as a real exception.\n */\nexport async function sendAuditEmail(\n result: ProspectAuditResult,\n opts: SendAuditEmailOptions,\n): Promise<SendAuditEmailResult> {\n if (opts.recipients.length === 0) {\n return {\n sent: false,\n reason: \"no recipients configured (PROSPECT_AUDIT_RECIPIENTS is unset or empty)\",\n };\n }\n\n const built = buildAuditEmail(result, { link: opts.link });\n const attachmentHtml = opts.renderReport ? opts.renderReport(result) : built.attachmentHtml;\n const client = opts.client ?? defaultResendClient();\n const idempotencyKey = `prospect-audit:${opts.auditId ?? `${result.url}#${result.generatedAt}`}`;\n const filename = `prospect-audit-${slugify(hostnameOf(result.url))}.html`;\n\n const { messageId } = await client.send({\n from: FROM_ADDRESS,\n to: opts.recipients,\n subject: built.subject,\n html: built.html,\n attachments: [\n {\n filename,\n content: Buffer.from(attachmentHtml, \"utf-8\").toString(\"base64\"),\n contentType: \"text/html\",\n },\n ],\n idempotencyKey,\n });\n\n return { sent: true, messageId, recipients: opts.recipients };\n}\n"],"mappings":"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","names":[]}
@@ -1,12 +1,13 @@
1
- import {
2
- WEBSITES_TABLE,
3
- listWebsites,
4
- siteSlug
5
- } from "./chunk-577HCGZO.js";
6
1
  import {
7
2
  openBase,
8
3
  readAirtableConfig
9
4
  } from "./chunk-HN6I2XWI.js";
5
+ import {
6
+ WEBSITES_TABLE,
7
+ listWebsites,
8
+ siteSlug,
9
+ toAirtableStatus
10
+ } from "./chunk-LDWZSLSU.js";
10
11
  import "./chunk-6VAL7XJT.js";
11
12
 
12
13
  // src/reports/airtable/ensure-site.ts
@@ -26,7 +27,7 @@ async function ensureSite(base, input) {
26
27
  if (!existing) {
27
28
  const fields = {
28
29
  Name: input.displayName ?? slug,
29
- Status: "in development",
30
+ Status: toAirtableStatus("building"),
30
31
  [COLS.gitRepo]: input.gitRepo ?? `reddoorla/${slug}`
31
32
  };
32
33
  if (input.url) fields[COLS.url] = input.url;
@@ -84,4 +85,4 @@ async function runEnsureSiteCommand(slug, opts) {
84
85
  export {
85
86
  runEnsureSiteCommand
86
87
  };
87
- //# sourceMappingURL=ensure-site-S25MDDHW.js.map
88
+ //# sourceMappingURL=ensure-site-DFTB7MN3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/airtable/ensure-site.ts","../src/cli/commands/ensure-site.ts"],"sourcesContent":["import type { FieldSet, Records } from \"airtable\";\nimport type { AirtableBase } from \"./client.js\";\nimport { WEBSITES_TABLE, listWebsites, siteSlug } from \"./websites.js\";\nimport { toAirtableStatus } from \"./site-status.js\";\n\nexport type EnsureSiteInput = {\n /** Canonical slug — matches by siteSlug(Name); the Name on create when no displayName given. */\n slug: string;\n /** Human display name written to Name on create. Name is consumed VERBATIM in\n * client-facing copy (forms auto-reply intro, report subjects), so a bare\n * machine slug there reads as \"Thanks for reaching out to acme-co.\" Must\n * slugify to the same slug or the row wouldn't be found on re-run. */\n displayName?: string;\n url?: string;\n pointOfContact?: string;\n /** owner/repo. Default on create: `reddoorla/<slug>`. */\n gitRepo?: string;\n};\n\nexport type EnsureSiteResult = {\n status: \"created\" | \"exists\";\n siteId: string;\n /** Airtable column names written on the exists path (create path writes all). */\n updatedFields: string[];\n /** Inputs that DIFFERED from an existing non-blank cell and were left\n * untouched (fill-blanks-only) — surfaced so a resumed bootstrap with a\n * corrected value doesn't silently discard it. */\n skippedMismatches: string[];\n};\n\n/** Column-name map (load-bearing magic strings — see websites.ts's mapRow header). */\nconst COLS = {\n url: \"url\",\n pointOfContact: \"point of contact\",\n gitRepo: \"Git repo\",\n} as const;\n\n/**\n * Find-or-create the Websites row for a slug so a new site exists in the fleet\n * inventory (audits, form-ingest slug resolution, reports) from day one.\n *\n * Fill-blanks-only on the exists path: this command runs from a bootstrap skill\n * that may be re-run to resume — it must never clobber operator-edited cells.\n * Frequencies are deliberately NOT set (launch flips the lifecycle); Status is\n * only written on create (\"building\" — Airtable's \"in development\" until the\n * stage-2 vocabulary switch flips).\n */\nexport async function ensureSite(\n base: AirtableBase,\n input: EnsureSiteInput,\n): Promise<EnsureSiteResult> {\n const slug = siteSlug(input.slug);\n if (!slug) throw new Error(`ensure-site: '${input.slug}' does not slugify to a usable slug`);\n if (input.displayName && siteSlug(input.displayName) !== slug)\n throw new Error(\n `ensure-site: display name '${input.displayName}' slugifies to '${siteSlug(input.displayName)}', not '${slug}' — the row would not be found on re-run`,\n );\n\n const existing = (await listWebsites(base)).find((w) => siteSlug(w.name) === slug);\n\n if (!existing) {\n const fields: FieldSet = {\n Name: input.displayName ?? slug,\n Status: toAirtableStatus(\"building\"),\n [COLS.gitRepo]: input.gitRepo ?? `reddoorla/${slug}`,\n };\n if (input.url) fields[COLS.url] = input.url;\n if (input.pointOfContact) fields[COLS.pointOfContact] = input.pointOfContact;\n const created = (await base(WEBSITES_TABLE).create([{ fields }])) as Records<FieldSet>;\n return {\n status: \"created\",\n siteId: created[0]!.id,\n updatedFields: [],\n skippedMismatches: [],\n };\n }\n\n const updates: FieldSet = {};\n const skippedMismatches: string[] = [];\n const blank = (v: unknown) => v === null || v === undefined || v === \"\";\n const consider = (column: string, provided: string | undefined, current: unknown) => {\n if (!provided) return;\n if (blank(current)) updates[column] = provided;\n else if (current !== provided) skippedMismatches.push(column);\n };\n consider(COLS.url, input.url, existing.url || null);\n consider(COLS.pointOfContact, input.pointOfContact, existing.pointOfContact);\n consider(COLS.gitRepo, input.gitRepo, existing.gitRepo);\n\n const updatedFields = Object.keys(updates);\n if (updatedFields.length > 0) {\n await base(WEBSITES_TABLE).update([{ id: existing.id, fields: updates }]);\n }\n return { status: \"exists\", siteId: existing.id, updatedFields, skippedMismatches };\n}\n","import { openBase, readAirtableConfig } from \"../../reports/airtable/client.js\";\nimport { ensureSite } from \"../../reports/airtable/ensure-site.js\";\n\nexport type EnsureSiteCommandOptions = {\n name?: string;\n url?: string;\n contact?: string;\n gitRepo?: string;\n cwd?: string;\n};\n\n/**\n * `ensure-site <slug>` — create/verify the fleet-inventory row for a new site.\n * Day-one step of the /new-site bootstrap skill. Fill-blanks-only; safe to re-run.\n */\nexport async function runEnsureSiteCommand(\n slug: string | undefined,\n opts: EnsureSiteCommandOptions,\n): Promise<{ output: string; code: number }> {\n if (!slug) return { output: \"Provide a <slug> (e.g. `ensure-site roalson`).\", code: 2 };\n try {\n const base = openBase(readAirtableConfig());\n const result = await ensureSite(base, {\n slug,\n ...(opts.name ? { displayName: opts.name } : {}),\n ...(opts.url ? { url: opts.url } : {}),\n ...(opts.contact ? { pointOfContact: opts.contact } : {}),\n ...(opts.gitRepo ? { gitRepo: opts.gitRepo } : {}),\n });\n const filled =\n result.updatedFields.length > 0\n ? ` — filled blank field(s): ${result.updatedFields.join(\", \")}`\n : \"\";\n const skipped =\n result.skippedMismatches.length > 0\n ? ` — differs from existing, left untouched (edit in Airtable): ${result.skippedMismatches.join(\", \")}`\n : \"\";\n const nameNote =\n result.status === \"created\" && !opts.name\n ? ` — Name set to \"${slug}\"; retitle in Airtable before forms/announce go live (or re-create with --name)`\n : \"\";\n return {\n output: `[${slug}] ${result.status} (${result.siteId})${filled}${skipped}${nameNote}`,\n code: 0,\n };\n } catch (err) {\n const e = err as { message?: string; exitCode?: number };\n return { output: e.message ?? String(err), code: e.exitCode ?? 1 };\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,24 @@
1
+ import * as airtable_lib_airtable_base_js from 'airtable/lib/airtable_base.js';
2
+
3
+ type AirtableConfig = {
4
+ apiKey: string;
5
+ baseId: string;
6
+ };
7
+ type AirtableBase = ReturnType<typeof openBase>;
8
+ declare function openBase(cfg: AirtableConfig): airtable_lib_airtable_base_js.AirtableBase;
9
+
10
+ type FleetEventType = "pr_automerged" | "vuln_cleared" | "ci_recovered" | "site_launched" | "fleet_swept" | "cert_renewed" | "report_sent_with_override";
11
+ /** One recorded fleet activity event. `id` is deterministic (see the detectors)
12
+ * so re-runs INSERT OR IGNORE without duplicating. `siteId`/`siteName` are null
13
+ * for fleet-wide rollups; `data` is optional structured detail (e.g. the PR url). */
14
+ type FleetEvent = {
15
+ id: string;
16
+ ts: string;
17
+ type: FleetEventType;
18
+ siteId: string | null;
19
+ siteName: string | null;
20
+ summary: string;
21
+ data: unknown | null;
22
+ };
23
+
24
+ export type { AirtableBase as A, FleetEvent as F, FleetEventType as a };
@@ -1,141 +1,34 @@
1
+ import {
2
+ describeNotifyTarget
3
+ } from "./chunk-VSBFM7DS.js";
4
+ import "./chunk-BNM5POSJ.js";
1
5
  import "./chunk-SNSTJLQW.js";
6
+ import "./chunk-5ML7FMBD.js";
7
+ import "./chunk-DJZWYIPP.js";
2
8
  import {
3
- operatorEmail
4
- } from "./chunk-5ML7FMBD.js";
5
- import "./chunk-SEEA6RXF.js";
9
+ openBase,
10
+ readAirtableConfig
11
+ } from "./chunk-HN6I2XWI.js";
6
12
  import "./chunk-2OP4JKIQ.js";
7
13
  import {
14
+ canonicalizeStatus,
8
15
  listWebsites,
9
16
  siteSlug,
17
+ toAirtableStatus,
10
18
  updateSiteField
11
- } from "./chunk-577HCGZO.js";
12
- import {
13
- openBase,
14
- readAirtableConfig
15
- } from "./chunk-HN6I2XWI.js";
19
+ } from "./chunk-LDWZSLSU.js";
16
20
  import "./chunk-6VAL7XJT.js";
17
21
 
18
- // src/forms/ingest.ts
19
- var DUPLICATE_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
20
-
21
- // src/forms/notify.ts
22
- function pocAddress(site) {
23
- return site.pointOfContact ?? site.reportRecipientsTo ?? null;
24
- }
25
- function normalizeRecipients(v) {
26
- const arr = Array.isArray(v) ? v : v ? [v] : [];
27
- const seen = /* @__PURE__ */ new Set();
28
- const out = [];
29
- for (const raw of arr) {
30
- if (typeof raw !== "string") continue;
31
- const t = raw.trim();
32
- if (t && !seen.has(t)) {
33
- seen.add(t);
34
- out.push(t);
35
- }
36
- }
37
- return out;
38
- }
39
- function resolveRecipients(site, submission) {
40
- if (site.status !== "maintenance") {
41
- return { to: [operatorEmail()], cc: [] };
42
- }
43
- const routing = site.notifyRouting;
44
- if (routing) {
45
- const value = parseExtraFields(submission.extraFields)[routing.field];
46
- const match = typeof value === "string" ? routing.routes[value] : void 0;
47
- const to = normalizeRecipients(match ?? routing.default);
48
- if (to.length > 0) return { to, cc: normalizeRecipients(routing.cc) };
49
- }
50
- const poc = pocAddress(site);
51
- return poc ? { to: [poc], cc: [] } : null;
52
- }
53
- function probeSubmission(extraFields) {
54
- return {
55
- id: "probe",
56
- submissionId: null,
57
- siteId: "probe",
58
- formType: "contact",
59
- name: "probe",
60
- email: "probe@example.com",
61
- phone: null,
62
- message: null,
63
- extraFields,
64
- sourceUrl: null,
65
- utm: null,
66
- submittedAt: null,
67
- status: "new",
68
- notifyStatus: "pending",
69
- resendMessageId: null
70
- };
71
- }
72
- function describeNotifyTarget(site) {
73
- if (site.status !== "maintenance") {
74
- const guarded = resolveRecipients(site, probeSubmission(null));
75
- return {
76
- audience: "operator",
77
- to: guarded?.to ?? [],
78
- cc: [],
79
- reason: `Status is ${site.status === null ? "blank" : `"${site.status}"`}, not "maintenance" \u2014 the pre-launch guard routes every notification to the operator.`
80
- };
81
- }
82
- const routing = site.notifyRouting;
83
- if (!routing) {
84
- const direct = resolveRecipients(site, probeSubmission(null));
85
- if (!direct || direct.to.length === 0) {
86
- return {
87
- audience: "nobody",
88
- to: [],
89
- cc: [],
90
- reason: `Status is "maintenance" but the site has no Point of Contact and no Notify Routing \u2014 the lead is stored and no notification is sent.`
91
- };
92
- }
93
- return {
94
- audience: "client",
95
- to: direct.to,
96
- cc: direct.cc,
97
- reason: `Status is "maintenance" \u2014 notifications go to the site's Point of Contact.`
98
- };
99
- }
100
- const to = /* @__PURE__ */ new Set();
101
- const cc = /* @__PURE__ */ new Set();
102
- const values = [...Object.keys(routing.routes), "\0no-such-route"];
103
- for (const value of values) {
104
- const r = resolveRecipients(site, probeSubmission(JSON.stringify({ [routing.field]: value })));
105
- r?.to.forEach((a) => to.add(a));
106
- r?.cc.forEach((a) => cc.add(a));
107
- }
108
- if (to.size === 0) {
109
- return {
110
- audience: "nobody",
111
- to: [],
112
- cc: [],
113
- reason: `Status is "maintenance" with Notify Routing on "${routing.field}", but no route, default or Point of Contact resolves to an address.`
114
- };
115
- }
116
- return {
117
- audience: "client",
118
- to: [...to],
119
- cc: [...cc],
120
- reason: `Status is "maintenance" with Notify Routing on "${routing.field}" \u2014 the recipient depends on that field's value; every address it could reach is listed.`
121
- };
122
- }
123
- function parseExtraFields(raw) {
124
- if (!raw) return {};
125
- try {
126
- const parsed = JSON.parse(raw);
127
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
128
- return parsed;
129
- }
130
- } catch {
131
- }
132
- return {};
133
- }
134
-
135
22
  // src/recipes/forms-notify-target.ts
136
23
  var STATUS_COLUMN = "Status";
137
- var LIVE_STATUS = "maintenance";
138
- var VERIFY_STATUS = "launch period";
24
+ var LIVE_STATUS = "maintained";
25
+ var VERIFY_STATUS = "launching";
26
+ function restoreCell(raw) {
27
+ const canonical = canonicalizeStatus(raw);
28
+ if (canonical === null) return raw;
29
+ const roundTripped = toAirtableStatus(canonical);
30
+ return roundTripped === raw ? roundTripped : raw;
31
+ }
139
32
  function findSite(rows, site) {
140
33
  const wanted = site.trim().toLowerCase();
141
34
  return rows.find(
@@ -159,11 +52,12 @@ async function formsNotifyTarget(deps) {
159
52
  if (!deps.set) {
160
53
  return { site: row.name, status: row.status, target: describeNotifyTarget(row) };
161
54
  }
162
- const to = deps.set === "on" ? VERIFY_STATUS : deps.restore?.trim();
55
+ const restoreRaw = deps.restore?.trim();
56
+ const to = deps.set === "on" ? VERIFY_STATUS : canonicalizeStatus(restoreRaw) ?? void 0;
163
57
  if (deps.set === "off" && !to) {
164
58
  throw Object.assign(
165
59
  new Error(
166
- `--set off needs --restore <status>: the status to return to is never inferred. Guessing "${LIVE_STATUS}" for a site that was "hosting" or "legacy" would start sending real client notifications \u2014 the inverse of the failure this command exists to prevent.`
60
+ `--set off needs --restore <status>: the status to return to is never inferred. Guessing "${LIVE_STATUS}" for a site that was "hosted-only" or "archived" would start sending real client notifications \u2014 the inverse of the failure this command exists to prevent.`
167
61
  ),
168
62
  { exitCode: 2 }
169
63
  );
@@ -176,7 +70,8 @@ async function formsNotifyTarget(deps) {
176
70
  { exitCode: 2 }
177
71
  );
178
72
  }
179
- await updateSiteField(base, row.id, STATUS_COLUMN, to);
73
+ const cell = deps.set === "on" ? toAirtableStatus(VERIFY_STATUS) : restoreCell(restoreRaw);
74
+ await updateSiteField(base, row.id, STATUS_COLUMN, cell);
180
75
  const after = findSite(await listWebsites(base), deps.site);
181
76
  if (!after) {
182
77
  throw Object.assign(new Error(`${row.name} vanished from Websites during the flip.`), {
@@ -187,7 +82,18 @@ async function formsNotifyTarget(deps) {
187
82
  site: after.name,
188
83
  status: after.status,
189
84
  target: describeNotifyTarget(after),
190
- flip: { from: row.status, to, confirmed: after.status === to }
85
+ // `confirmed` compares the RAW cell against the exact string written, not
86
+ // canonical-to-canonical. Canonical equality is too weak to be a read-back
87
+ // guard here: `legacy` and `deprecated` are the same canonical status, so a
88
+ // cell holding either would confirm a flip to the other. Comparing raw is the
89
+ // only comparison that can actually catch "the cell is not what I sent". The
90
+ // canonical check rides along so a reader that stopped canonicalizing is
91
+ // caught too.
92
+ flip: {
93
+ from: row.status,
94
+ to,
95
+ confirmed: after.statusRaw === cell && after.status === to
96
+ }
191
97
  };
192
98
  }
193
99
 
@@ -254,4 +160,4 @@ export {
254
160
  formatNotifyTarget,
255
161
  runFormsNotifyTargetCommand
256
162
  };
257
- //# sourceMappingURL=forms-notify-target-TWN4EH6K.js.map
163
+ //# sourceMappingURL=forms-notify-target-75U6OU7L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/recipes/forms-notify-target.ts","../src/cli/commands/forms-notify-target.ts"],"sourcesContent":["import { openBase, readAirtableConfig } from \"../reports/airtable/client.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport {\n listWebsites,\n siteSlug,\n updateSiteField,\n type Status,\n type WebsiteRow,\n} from \"../reports/airtable/websites.js\";\nimport { canonicalizeStatus, toAirtableStatus } from \"../reports/airtable/site-status.js\";\nimport { describeNotifyTarget, type NotifyTarget } from \"../forms/notify.js\";\n\n/** The Airtable column the pre-launch guard actually lives in. */\nexport const STATUS_COLUMN = \"Status\";\n\n/** The two ends of the verify flip. Deliberately the ONLY transition this\n * performs: a site in any other status is already guarded (or is deliberately\n * something else, like \"hosted-only\"), and silently rewriting that would be the\n * same class of unseen change as the incident. */\nexport const LIVE_STATUS: Status = \"maintained\";\nexport const VERIFY_STATUS: Status = \"launching\";\n\nexport type FormsNotifyTargetDeps = {\n base?: AirtableBase;\n /** Site slug or the Airtable Websites NAME (both accepted). */\n site: string;\n /** `on` routes notifications to the operator; `off` restores. Omit to read. */\n set?: \"on\" | \"off\";\n /** Status to restore with `--set off`. Required, never inferred. */\n restore?: string;\n};\n\nexport type FormsNotifyTargetResult = {\n site: string;\n status: Status | null;\n target: NotifyTarget;\n /** Present only when a flip was attempted. `confirmed` is the whole point:\n * it comes from RE-READING the row, not from the write call returning. */\n flip?: { from: Status | null; to: Status; confirmed: boolean };\n};\n\n/**\n * The exact Airtable cell to write for an operator-supplied `--restore` value:\n * the operator's own string, verbatim.\n *\n * This is the ONE non-revertible surface in the #539 Phase 4 stage-1 rename —\n * every other change is code, and `git revert` undoes code. It cannot undo a\n * rewritten Airtable cell. So the rule here is stricter than everywhere else:\n * substitute only when the substitution is provably lossless, i.e. when\n * canonicalizing and mapping back ROUND-TRIPS to the operator's own string.\n *\n * That condition is, today, never false in a way that changes the answer — which\n * is the point. `toAirtableStatus(canonicalizeStatus(raw))` either equals `raw`\n * (so writing it is writing `raw`) or it does not (so we must write `raw`). The\n * function therefore reduces to \"write raw\", and it is written this way so the\n * reduction is visible rather than assumed. The case that made it matter:\n * `--restore legacy` canonicalizes to `archived`, which maps back to\n * \"deprecated\" — a different, real, operator-visible Airtable option that nobody\n * asked for. `hosting` → `hosted-only` → \"hosting\" round-trips and is safe.\n *\n * Airtable, not this module, is the authority on which option strings the\n * \"Status\" single-select accepts. Writing verbatim delegates to it: a typo is\n * rejected loudly at the API, exactly as it was before the rename.\n */\nexport function restoreCell(raw: string): string {\n const canonical = canonicalizeStatus(raw);\n if (canonical === null) return raw;\n const roundTripped = toAirtableStatus(canonical);\n return roundTripped === raw ? roundTripped : raw;\n}\n\nfunction findSite(rows: WebsiteRow[], site: string): WebsiteRow | undefined {\n const wanted = site.trim().toLowerCase();\n return rows.find(\n (r) => siteSlug(r.name) === siteSlug(site) || r.name.trim().toLowerCase() === wanted,\n );\n}\n\n/**\n * Answer \"who would a form submission on this site email?\" — and optionally\n * flip the pre-launch guard, confirming the flip by reading it back.\n *\n * The guard is a single Airtable `Status` cell. Nothing between \"I intended to\n * flip it\" and \"the client received a test lead\" reported the current state, so\n * on 2026-08-03 a flip that never landed sent a real client a test submission.\n * The fix is not a better intention, it is feedback: this reads the row back\n * after every write and refuses to call the flip confirmed on anything less.\n */\nexport async function formsNotifyTarget(\n deps: FormsNotifyTargetDeps,\n): Promise<FormsNotifyTargetResult> {\n const base = deps.base ?? openBase(readAirtableConfig());\n const rows = await listWebsites(base);\n const row = findSite(rows, deps.site);\n if (!row) {\n // The Websites NAME is not the repo slug (\"Sonder\", not \"gallerysonder\"),\n // and that mismatch has cost time before — so name the near misses rather\n // than making the operator go read Airtable to find the spelling.\n const needle = siteSlug(deps.site);\n const near = rows\n .map((r) => r.name)\n .filter((n) => siteSlug(n).includes(needle) || needle.includes(siteSlug(n)));\n throw Object.assign(\n new Error(\n `No Websites row matches '${deps.site}'. The Websites NAME is not always the repo ` +\n `slug (Sonder, not gallerysonder).` +\n (near.length > 0 ? ` Did you mean: ${near.join(\", \")}?` : \"\"),\n ),\n { exitCode: 2 },\n );\n }\n\n if (!deps.set) {\n return { site: row.name, status: row.status, target: describeNotifyTarget(row) };\n }\n\n // `--restore` is operator free text. It is canonicalized for what this command\n // REPORTS (`flip.to`, and the predicates downstream), but NEVER for what it\n // WRITES — see `restoreCell` below.\n const restoreRaw = deps.restore?.trim();\n const to = deps.set === \"on\" ? VERIFY_STATUS : (canonicalizeStatus(restoreRaw) ?? undefined);\n if (deps.set === \"off\" && !to) {\n throw Object.assign(\n new Error(\n `--set off needs --restore <status>: the status to return to is never inferred. ` +\n `Guessing \"${LIVE_STATUS}\" for a site that was \"hosted-only\" or \"archived\" would start ` +\n `sending real client notifications — the inverse of the failure this command exists ` +\n `to prevent.`,\n ),\n { exitCode: 2 },\n );\n }\n // Only ever flip a LIVE site into verify mode. A site already outside\n // \"maintained\" is guarded already, and rewriting its status would destroy a\n // real value nobody asked us to touch.\n if (deps.set === \"on\" && row.status !== LIVE_STATUS) {\n throw Object.assign(\n new Error(\n `${row.name} is \"${row.status ?? \"blank\"}\", not \"${LIVE_STATUS}\" — notifications ` +\n `already go to the operator only, so there is nothing to flip. Leaving the status ` +\n `untouched.`,\n ),\n { exitCode: 2 },\n );\n }\n\n // `--set on` writes a status this MODULE owns (VERIFY_STATUS), so mapping it to\n // the current Airtable vocabulary is correct. `--set off` writes the operator's\n // own string — see restoreCell.\n const cell = deps.set === \"on\" ? toAirtableStatus(VERIFY_STATUS) : restoreCell(restoreRaw!);\n await updateSiteField(base, row.id, STATUS_COLUMN, cell);\n\n // Read it back. The write returning is NOT evidence the field changed.\n const after = findSite(await listWebsites(base), deps.site);\n if (!after) {\n throw Object.assign(new Error(`${row.name} vanished from Websites during the flip.`), {\n exitCode: 1,\n });\n }\n return {\n site: after.name,\n status: after.status,\n target: describeNotifyTarget(after),\n // `confirmed` compares the RAW cell against the exact string written, not\n // canonical-to-canonical. Canonical equality is too weak to be a read-back\n // guard here: `legacy` and `deprecated` are the same canonical status, so a\n // cell holding either would confirm a flip to the other. Comparing raw is the\n // only comparison that can actually catch \"the cell is not what I sent\". The\n // canonical check rides along so a reader that stopped canonicalizing is\n // caught too.\n flip: {\n from: row.status,\n to: to!,\n confirmed: after.statusRaw === cell && after.status === to,\n },\n };\n}\n","import {\n formsNotifyTarget,\n LIVE_STATUS,\n VERIFY_STATUS,\n type FormsNotifyTargetResult,\n} from \"../../recipes/forms-notify-target.js\";\n\nexport type FormsNotifyTargetCommandOptions = {\n set?: string;\n restore?: string;\n cwd?: string;\n};\n\nconst AUDIENCE_LABEL = {\n client: \"THE CLIENT\",\n operator: \"OPERATOR ONLY\",\n nobody: \"NOBODY\",\n} as const;\n\n/** The line that decides whether it is safe to test-submit. Deliberately the\n * loudest thing on screen: the incident happened because the answer was\n * invisible, not because it was subtle. */\nexport function formatNotifyTarget(r: FormsNotifyTargetResult): string {\n const lines: string[] = [];\n if (r.flip) {\n const arrow = `${r.flip.from ?? \"blank\"} → ${r.flip.to}`;\n lines.push(\n r.flip.confirmed\n ? `${r.site} Status: ${arrow} ✓ confirmed by read-back`\n : `${r.site} Status: ${arrow} ✗ NOT CONFIRMED — the row still reads \"${r.status ?? \"blank\"}\". Nothing was verified; do not test-submit.`,\n );\n } else {\n lines.push(`${r.site} Status: ${r.status ?? \"blank\"}`);\n }\n\n lines.push(`A submission right now would notify: ${AUDIENCE_LABEL[r.target.audience]}`);\n if (r.target.to.length > 0) lines.push(` to: ${r.target.to.join(\", \")}`);\n if (r.target.cc.length > 0) lines.push(` cc: ${r.target.cc.join(\", \")}`);\n lines.push(` ${r.target.reason}`);\n\n if (r.target.audience === \"client\") {\n lines.push(\n \"\",\n `⚠️ A test submission WILL email the client, and email cannot be recalled.`,\n ` Route it to yourself first: reddoor forms-notify-target ${r.site} --set on`,\n );\n }\n if (r.flip?.confirmed && r.flip.to === VERIFY_STATUS) {\n lines.push(\n \"\",\n `Safe to test. When you are done, restore it:`,\n ` reddoor forms-notify-target ${r.site} --set off --restore ${r.flip.from ?? LIVE_STATUS}`,\n );\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * `forms-notify-target <site>` — show who a form submission would email, and\n * optionally flip the pre-launch guard with a read-back confirmation.\n *\n * Read-only by default: answering the question must never be riskier than not\n * asking it.\n */\nexport async function runFormsNotifyTargetCommand(\n site: string | undefined,\n opts: FormsNotifyTargetCommandOptions,\n): Promise<{ output: string; code: number }> {\n if (!site?.trim()) {\n return { output: \"forms-notify-target requires <site> (slug or Airtable name)\", code: 2 };\n }\n const set = opts.set?.trim().toLowerCase();\n if (set !== undefined && set !== \"on\" && set !== \"off\") {\n return { output: `--set must be 'on' or 'off' (got '${opts.set}')`, code: 2 };\n }\n try {\n const result = await formsNotifyTarget({\n site: site.trim(),\n ...(set ? { set: set as \"on\" | \"off\" } : {}),\n ...(opts.restore ? { restore: opts.restore } : {}),\n });\n // An unconfirmed flip must not exit 0: a script (or a person skimming) that\n // reads exit status would otherwise take \"I flipped it\" on faith, which is\n // exactly the assumption that sent a client a test lead.\n return {\n output: formatNotifyTarget(result),\n code: result.flip && !result.flip.confirmed ? 1 : 0,\n };\n } catch (err) {\n const e = err as { message?: string; exitCode?: number };\n return { output: e.message ?? String(err), code: e.exitCode ?? 1 };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAaO,IAAM,gBAAgB;AAMtB,IAAM,cAAsB;AAC5B,IAAM,gBAAwB;AA4C9B,SAAS,YAAY,KAAqB;AAC/C,QAAM,YAAY,mBAAmB,GAAG;AACxC,MAAI,cAAc,KAAM,QAAO;AAC/B,QAAM,eAAe,iBAAiB,SAAS;AAC/C,SAAO,iBAAiB,MAAM,eAAe;AAC/C;AAEA,SAAS,SAAS,MAAoB,MAAsC;AAC1E,QAAM,SAAS,KAAK,KAAK,EAAE,YAAY;AACvC,SAAO,KAAK;AAAA,IACV,CAAC,MAAM,SAAS,EAAE,IAAI,MAAM,SAAS,IAAI,KAAK,EAAE,KAAK,KAAK,EAAE,YAAY,MAAM;AAAA,EAChF;AACF;AAYA,eAAsB,kBACpB,MACkC;AAClC,QAAM,OAAO,KAAK,QAAQ,SAAS,mBAAmB,CAAC;AACvD,QAAM,OAAO,MAAM,aAAa,IAAI;AACpC,QAAM,MAAM,SAAS,MAAM,KAAK,IAAI;AACpC,MAAI,CAAC,KAAK;AAIR,UAAM,SAAS,SAAS,KAAK,IAAI;AACjC,UAAM,OAAO,KACV,IAAI,CAAC,MAAM,EAAE,IAAI,EACjB,OAAO,CAAC,MAAM,SAAS,CAAC,EAAE,SAAS,MAAM,KAAK,OAAO,SAAS,SAAS,CAAC,CAAC,CAAC;AAC7E,UAAM,OAAO;AAAA,MACX,IAAI;AAAA,QACF,4BAA4B,KAAK,IAAI,mFAElC,KAAK,SAAS,IAAI,kBAAkB,KAAK,KAAK,IAAI,CAAC,MAAM;AAAA,MAC9D;AAAA,MACA,EAAE,UAAU,EAAE;AAAA,IAChB;AAAA,EACF;AAEA,MAAI,CAAC,KAAK,KAAK;AACb,WAAO,EAAE,MAAM,IAAI,MAAM,QAAQ,IAAI,QAAQ,QAAQ,qBAAqB,GAAG,EAAE;AAAA,EACjF;AAKA,QAAM,aAAa,KAAK,SAAS,KAAK;AACtC,QAAM,KAAK,KAAK,QAAQ,OAAO,gBAAiB,mBAAmB,UAAU,KAAK;AAClF,MAAI,KAAK,QAAQ,SAAS,CAAC,IAAI;AAC7B,UAAM,OAAO;AAAA,MACX,IAAI;AAAA,QACF,4FACe,WAAW;AAAA,MAG5B;AAAA,MACA,EAAE,UAAU,EAAE;AAAA,IAChB;AAAA,EACF;AAIA,MAAI,KAAK,QAAQ,QAAQ,IAAI,WAAW,aAAa;AACnD,UAAM,OAAO;AAAA,MACX,IAAI;AAAA,QACF,GAAG,IAAI,IAAI,QAAQ,IAAI,UAAU,OAAO,WAAW,WAAW;AAAA,MAGhE;AAAA,MACA,EAAE,UAAU,EAAE;AAAA,IAChB;AAAA,EACF;AAKA,QAAM,OAAO,KAAK,QAAQ,OAAO,iBAAiB,aAAa,IAAI,YAAY,UAAW;AAC1F,QAAM,gBAAgB,MAAM,IAAI,IAAI,eAAe,IAAI;AAGvD,QAAM,QAAQ,SAAS,MAAM,aAAa,IAAI,GAAG,KAAK,IAAI;AAC1D,MAAI,CAAC,OAAO;AACV,UAAM,OAAO,OAAO,IAAI,MAAM,GAAG,IAAI,IAAI,0CAA0C,GAAG;AAAA,MACpF,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AACA,SAAO;AAAA,IACL,MAAM,MAAM;AAAA,IACZ,QAAQ,MAAM;AAAA,IACd,QAAQ,qBAAqB,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQlC,MAAM;AAAA,MACJ,MAAM,IAAI;AAAA,MACV;AAAA,MACA,WAAW,MAAM,cAAc,QAAQ,MAAM,WAAW;AAAA,IAC1D;AAAA,EACF;AACF;;;ACnKA,IAAM,iBAAiB;AAAA,EACrB,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,QAAQ;AACV;AAKO,SAAS,mBAAmB,GAAoC;AACrE,QAAM,QAAkB,CAAC;AACzB,MAAI,EAAE,MAAM;AACV,UAAM,QAAQ,GAAG,EAAE,KAAK,QAAQ,OAAO,WAAM,EAAE,KAAK,EAAE;AACtD,UAAM;AAAA,MACJ,EAAE,KAAK,YACH,GAAG,EAAE,IAAI,aAAa,KAAK,oCAC3B,GAAG,EAAE,IAAI,aAAa,KAAK,sDAA4C,EAAE,UAAU,OAAO;AAAA,IAChG;AAAA,EACF,OAAO;AACL,UAAM,KAAK,GAAG,EAAE,IAAI,aAAa,EAAE,UAAU,OAAO,EAAE;AAAA,EACxD;AAEA,QAAM,KAAK,wCAAwC,eAAe,EAAE,OAAO,QAAQ,CAAC,EAAE;AACtF,MAAI,EAAE,OAAO,GAAG,SAAS,EAAG,OAAM,KAAK,SAAS,EAAE,OAAO,GAAG,KAAK,IAAI,CAAC,EAAE;AACxE,MAAI,EAAE,OAAO,GAAG,SAAS,EAAG,OAAM,KAAK,SAAS,EAAE,OAAO,GAAG,KAAK,IAAI,CAAC,EAAE;AACxE,QAAM,KAAK,KAAK,EAAE,OAAO,MAAM,EAAE;AAEjC,MAAI,EAAE,OAAO,aAAa,UAAU;AAClC,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,+DAA+D,EAAE,IAAI;AAAA,IACvE;AAAA,EACF;AACA,MAAI,EAAE,MAAM,aAAa,EAAE,KAAK,OAAO,eAAe;AACpD,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,iCAAiC,EAAE,IAAI,wBAAwB,EAAE,KAAK,QAAQ,WAAW;AAAA,IAC3F;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AASA,eAAsB,4BACpB,MACA,MAC2C;AAC3C,MAAI,CAAC,MAAM,KAAK,GAAG;AACjB,WAAO,EAAE,QAAQ,+DAA+D,MAAM,EAAE;AAAA,EAC1F;AACA,QAAM,MAAM,KAAK,KAAK,KAAK,EAAE,YAAY;AACzC,MAAI,QAAQ,UAAa,QAAQ,QAAQ,QAAQ,OAAO;AACtD,WAAO,EAAE,QAAQ,qCAAqC,KAAK,GAAG,MAAM,MAAM,EAAE;AAAA,EAC9E;AACA,MAAI;AACF,UAAM,SAAS,MAAM,kBAAkB;AAAA,MACrC,MAAM,KAAK,KAAK;AAAA,MAChB,GAAI,MAAM,EAAE,IAAyB,IAAI,CAAC;AAAA,MAC1C,GAAI,KAAK,UAAU,EAAE,SAAS,KAAK,QAAQ,IAAI,CAAC;AAAA,IAClD,CAAC;AAID,WAAO;AAAA,MACL,QAAQ,mBAAmB,MAAM;AAAA,MACjC,MAAM,OAAO,QAAQ,CAAC,OAAO,KAAK,YAAY,IAAI;AAAA,IACpD;AAAA,EACF,SAAS,KAAK;AACZ,UAAM,IAAI;AACV,WAAO,EAAE,QAAQ,EAAE,WAAW,OAAO,GAAG,GAAG,MAAM,EAAE,YAAY,EAAE;AAAA,EACnE;AACF;","names":[]}