@reddoorla/maintenance 0.86.0 → 0.88.2

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 (233) hide show
  1. package/dist/airtable-QG5ICI3Y.js +9 -0
  2. package/dist/{announce-MXTUQ6C7.js → announce-GKD5YJNB.js} +49 -28
  3. package/dist/announce-GKD5YJNB.js.map +1 -0
  4. package/dist/attachments-NLZIJ5NU.js +9 -0
  5. package/dist/{blux-ZGDSHI3U.js → blux-DN4MI532.js} +5 -5
  6. package/dist/{bump-deps-EC74CPLS.js → bump-deps-KGH5UYSF.js} +4 -4
  7. package/dist/{chunk-N45M3FDK.js → chunk-6I6L3CMX.js} +4 -4
  8. package/dist/{chunk-KTH5ZODA.js → chunk-7BZUOZPU.js} +2 -2
  9. package/dist/{chunk-2EVEFOZY.js → chunk-7FFUBF4I.js} +6 -3
  10. package/dist/chunk-7FFUBF4I.js.map +1 -0
  11. package/dist/{chunk-LDWZSLSU.js → chunk-7NYN2P6I.js} +7 -4
  12. package/dist/{chunk-LDWZSLSU.js.map → chunk-7NYN2P6I.js.map} +1 -1
  13. package/dist/{chunk-X3B723J4.js → chunk-7TMWBXX7.js} +184 -25
  14. package/dist/chunk-7TMWBXX7.js.map +1 -0
  15. package/dist/{chunk-3VQWSE3F.js → chunk-7UAYMAKR.js} +4 -4
  16. package/dist/{chunk-W7Z3BGCI.js → chunk-A2UNJIKH.js} +15 -7
  17. package/dist/chunk-A2UNJIKH.js.map +1 -0
  18. package/dist/{chunk-F5KM4LEQ.js → chunk-ANJNTAMM.js} +4 -70
  19. package/dist/chunk-ANJNTAMM.js.map +1 -0
  20. package/dist/{chunk-2ZPT7GLV.js → chunk-B23FFWU4.js} +4 -4
  21. package/dist/chunk-B23FFWU4.js.map +1 -0
  22. package/dist/{chunk-DJZWYIPP.js → chunk-BGTYPVLT.js} +1 -1
  23. package/dist/chunk-BGTYPVLT.js.map +1 -0
  24. package/dist/chunk-BRYXFH77.js +70 -0
  25. package/dist/chunk-BRYXFH77.js.map +1 -0
  26. package/dist/{chunk-2NDRHXMR.js → chunk-DGZWFI2G.js} +18 -141
  27. package/dist/chunk-DGZWFI2G.js.map +1 -0
  28. package/dist/{chunk-ACBRLLFT.js → chunk-F3JWOPTG.js} +7 -3
  29. package/dist/chunk-F3JWOPTG.js.map +1 -0
  30. package/dist/chunk-GQDDWYU2.js +372 -0
  31. package/dist/chunk-GQDDWYU2.js.map +1 -0
  32. package/dist/{chunk-Z7AXYKQB.js → chunk-IY6256GK.js} +1 -1
  33. package/dist/{chunk-BYK66EWO.js → chunk-J3WEZGIN.js} +52 -46
  34. package/dist/chunk-J3WEZGIN.js.map +1 -0
  35. package/dist/{chunk-BNM5POSJ.js → chunk-JRHIFA5R.js} +2 -2
  36. package/dist/{chunk-ZKXSX4LO.js → chunk-K6SLF6GI.js} +6 -6
  37. package/dist/{chunk-CQR4LIM7.js → chunk-KJGKZLCT.js} +7 -3
  38. package/dist/{chunk-CQR4LIM7.js.map → chunk-KJGKZLCT.js.map} +1 -1
  39. package/dist/{chunk-PPE5KZJK.js → chunk-KMCZA7CH.js} +13 -3
  40. package/dist/chunk-KMCZA7CH.js.map +1 -0
  41. package/dist/{chunk-3SF7D4DY.js → chunk-MFHS7IQ2.js} +36 -1
  42. package/dist/{chunk-3SF7D4DY.js.map → chunk-MFHS7IQ2.js.map} +1 -1
  43. package/dist/{chunk-SNSTJLQW.js → chunk-NO74DXDP.js} +48 -2
  44. package/dist/chunk-NO74DXDP.js.map +1 -0
  45. package/dist/{chunk-C6B4RHXR.js → chunk-O3GT46R2.js} +8 -2
  46. package/dist/chunk-O3GT46R2.js.map +1 -0
  47. package/dist/{chunk-ITIOFN7A.js → chunk-P6AORG32.js} +29 -10
  48. package/dist/chunk-P6AORG32.js.map +1 -0
  49. package/dist/{chunk-SLUQ7R6I.js → chunk-PR343K7F.js} +5 -5
  50. package/dist/{chunk-P2FE5AI6.js → chunk-Q47DU255.js} +4 -4
  51. package/dist/{chunk-EVBCK2KC.js → chunk-RARREDQE.js} +2 -1
  52. package/dist/{chunk-EVBCK2KC.js.map → chunk-RARREDQE.js.map} +1 -1
  53. package/dist/chunk-RUPPWAD6.js +7 -0
  54. package/dist/chunk-RUPPWAD6.js.map +1 -0
  55. package/dist/{chunk-VSBFM7DS.js → chunk-SJOXWJ5Y.js} +3 -3
  56. package/dist/{chunk-TERE5SDF.js → chunk-ULHPALI5.js} +4 -4
  57. package/dist/{chunk-XUGSZP7Z.js → chunk-W45FZEGE.js} +2 -2
  58. package/dist/{chunk-JHGHLKFS.js → chunk-WS6NU475.js} +2 -2
  59. package/dist/chunk-XP5WM2LV.js +146 -0
  60. package/dist/chunk-XP5WM2LV.js.map +1 -0
  61. package/dist/{chunk-ILUNZHYV.js → chunk-YFAXOD7Z.js} +10 -6
  62. package/dist/chunk-YFAXOD7Z.js.map +1 -0
  63. package/dist/{chunk-6TWWY7ZU.js → chunk-Z3JXLULD.js} +2 -2
  64. package/dist/cli/bin.js +34 -29
  65. package/dist/cli/bin.js.map +1 -1
  66. package/dist/cli/commands/audit.d.ts +4 -0
  67. package/dist/cli/commands/audit.js +25 -18
  68. package/dist/cli/commands/audit.js.map +1 -1
  69. package/dist/client-JYKPVCJX.js +11 -0
  70. package/dist/configs/playwright-a11y.js +1 -1
  71. package/dist/{convert-to-pnpm-6L6P74WR.js → convert-to-pnpm-NWB5FCQ2.js} +4 -4
  72. package/dist/{db-B35OGV3G.js → db-V6UIS433.js} +124 -29
  73. package/dist/db-V6UIS433.js.map +1 -0
  74. package/dist/{digest-BL6UJYKH.js → digest-2JEAEJK6.js} +60 -35
  75. package/dist/digest-2JEAEJK6.js.map +1 -0
  76. package/dist/digest-state-CRZRNGDI.js +25 -0
  77. package/dist/digest-state-CRZRNGDI.js.map +1 -0
  78. package/dist/digest-state-F37XUCS5.js +13 -0
  79. package/dist/{dump-P6EBSIOH.js → dump-EPL7AZFR.js} +38 -13
  80. package/dist/dump-EPL7AZFR.js.map +1 -0
  81. package/dist/{email-VTTTKKL5.js → email-SL7464YI.js} +16 -6
  82. package/dist/{email-VTTTKKL5.js.map → email-SL7464YI.js.map} +1 -1
  83. package/dist/{ensure-site-DFTB7MN3.js → ensure-site-FU6527OU.js} +19 -11
  84. package/dist/ensure-site-FU6527OU.js.map +1 -0
  85. package/dist/fleet-state-R5LU7IC3.js +41 -0
  86. package/dist/{forms-notify-target-75U6OU7L.js → forms-notify-target-SGDGXXK5.js} +10 -7
  87. package/dist/forms-notify-target-SGDGXXK5.js.map +1 -0
  88. package/dist/{github-signals-IJUPGCUX.js → github-signals-GWGCKEE2.js} +13 -12
  89. package/dist/{github-signals-IJUPGCUX.js.map → github-signals-GWGCKEE2.js.map} +1 -1
  90. package/dist/{header-image-AWLLQA4L.js → header-image-7A7XLVEU.js} +8 -8
  91. package/dist/{header-images-335L2AKB.js → header-images-SQBCE5OD.js} +21 -1
  92. package/dist/header-images-SQBCE5OD.js.map +1 -0
  93. package/dist/{health-endpoint-WJC2UMYS.js → health-endpoint-UVX6R5N5.js} +4 -4
  94. package/dist/health-mirror-GCDN7BJL.js +46 -0
  95. package/dist/health-mirror-GCDN7BJL.js.map +1 -0
  96. package/dist/{import-airtable-HE43FB4H.js → import-airtable-OHVY344G.js} +7 -5
  97. package/dist/index.d.ts +149 -0
  98. package/dist/index.js +245 -108
  99. package/dist/index.js.map +1 -1
  100. package/dist/{init-NJEOEO7I.js → init-FGTYNGUI.js} +11 -11
  101. package/dist/{launch-XNAX427U.js → launch-5H7LPEGP.js} +59 -29
  102. package/dist/launch-5H7LPEGP.js.map +1 -0
  103. package/dist/{lighthouse-airtable-LBVIYYP5.js → lighthouse-airtable-3GUWHETW.js} +3 -3
  104. package/dist/migrate-YITCXBLS.js +7 -0
  105. package/dist/{notify-C645PGD5.js → notify-4HP4GZ2H.js} +5 -5
  106. package/dist/{onboard-GK4PHPNI.js → onboard-BKCYBAVW.js} +4 -4
  107. package/dist/{orchestrate-77UO4BYJ.js → orchestrate-JNKCWU5K.js} +14 -13
  108. package/dist/parity-NBYZUBFU.js +14 -0
  109. package/dist/pdf-N5QLQ5ZQ.js +38 -0
  110. package/dist/pdf-N5QLQ5ZQ.js.map +1 -0
  111. package/dist/{pipeline-S6CVHDGB.js → pipeline-WYTXSLHX.js} +3 -3
  112. package/dist/{preflight-6WBNU2GZ.js → preflight-IMTYDDD4.js} +15 -14
  113. package/dist/{preflight-6WBNU2GZ.js.map → preflight-IMTYDDD4.js.map} +1 -1
  114. package/dist/{prismic-ci-3RHOZUUT.js → prismic-ci-MY2U6NN7.js} +7 -7
  115. package/dist/{prismic-models-QBGUT2Q3.js → prismic-models-NJN5JORF.js} +15 -10
  116. package/dist/prismic-models-NJN5JORF.js.map +1 -0
  117. package/dist/prospect/types.d.ts +281 -0
  118. package/dist/prospect/types.js +1 -0
  119. package/dist/{prospect-audit-IUNCJ2LV.js → prospect-audit-JMBRVXSG.js} +32 -13
  120. package/dist/prospect-audit-JMBRVXSG.js.map +1 -0
  121. package/dist/recipes/sync-configs.js +2 -2
  122. package/dist/render-VX72AESY.js +10 -0
  123. package/dist/render-VX72AESY.js.map +1 -0
  124. package/dist/render-from-row-DOYJDO2L.js +17 -0
  125. package/dist/render-from-row-DOYJDO2L.js.map +1 -0
  126. package/dist/{renovate-dispatch-QJY75ODR.js → renovate-dispatch-6GQWTRKX.js} +10 -10
  127. package/dist/{renovate-dispatch-QJY75ODR.js.map → renovate-dispatch-6GQWTRKX.js.map} +1 -1
  128. package/dist/{replay-C2LXMJZP.js → replay-4S6ENYCH.js} +3 -3
  129. package/dist/{report-4EVNDHOZ.js → report-TALY5P3L.js} +63 -21
  130. package/dist/report-TALY5P3L.js.map +1 -0
  131. package/dist/report-mirror-AAK3MXHY.js +55 -0
  132. package/dist/report-mirror-AAK3MXHY.js.map +1 -0
  133. package/dist/rerender-3FM6DNRH.js +32 -0
  134. package/dist/rerender-3FM6DNRH.js.map +1 -0
  135. package/dist/schema-75PJECAE.js +18 -0
  136. package/dist/schema-75PJECAE.js.map +1 -0
  137. package/dist/{self-updating-6DWI2Z56.js → self-updating-NTCCWJYZ.js} +4 -4
  138. package/dist/{selftest-RC36MTKP.js → selftest-NZTR4SM2.js} +26 -24
  139. package/dist/{selftest-RC36MTKP.js.map → selftest-NZTR4SM2.js.map} +1 -1
  140. package/dist/site-mirror-Q763PLGG.js +62 -0
  141. package/dist/site-mirror-Q763PLGG.js.map +1 -0
  142. package/dist/{smoke-suite-VYY2LXM2.js → smoke-suite-4JFZZYVW.js} +4 -4
  143. package/dist/{spam-classifier-XLBFCO7G.js → spam-classifier-T5H4VTP6.js} +4 -2
  144. package/dist/spam-classifier-T5H4VTP6.js.map +1 -0
  145. package/dist/{submissions-Q7DRFOCD.js → submissions-TEHPBSZJ.js} +5 -5
  146. package/dist/{svelte-codemods-25J2FO22.js → svelte-codemods-QAPDZC6F.js} +4 -4
  147. package/dist/{sync-34HNZZ56.js → sync-GBFV45WX.js} +6 -4
  148. package/dist/{sync-34HNZZ56.js.map → sync-GBFV45WX.js.map} +1 -1
  149. package/dist/{sync-configs-3SE6PSCO.js → sync-configs-34JIA6KZ.js} +6 -6
  150. package/dist/{upgrade-WBRQ62FX.js → upgrade-IR2OO3KP.js} +4 -4
  151. package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
  152. package/dist/{webhook-VOGOW3W6.js → webhook-XM6EV4XT.js} +2 -2
  153. package/dist/{websites-SXWTP72S.js → websites-D6WYFAES.js} +2 -2
  154. package/dist/websites-D6WYFAES.js.map +1 -0
  155. package/dist/write-audits-to-airtable-OBHGR7DQ.js +17 -0
  156. package/dist/write-audits-to-airtable-OBHGR7DQ.js.map +1 -0
  157. package/package.json +5 -1
  158. package/dist/airtable-G7WBQQMI.js +0 -9
  159. package/dist/announce-MXTUQ6C7.js.map +0 -1
  160. package/dist/chunk-2EVEFOZY.js.map +0 -1
  161. package/dist/chunk-2NDRHXMR.js.map +0 -1
  162. package/dist/chunk-2ZPT7GLV.js.map +0 -1
  163. package/dist/chunk-ACBRLLFT.js.map +0 -1
  164. package/dist/chunk-BYK66EWO.js.map +0 -1
  165. package/dist/chunk-C6B4RHXR.js.map +0 -1
  166. package/dist/chunk-DJZWYIPP.js.map +0 -1
  167. package/dist/chunk-F5KM4LEQ.js.map +0 -1
  168. package/dist/chunk-ILUNZHYV.js.map +0 -1
  169. package/dist/chunk-ITIOFN7A.js.map +0 -1
  170. package/dist/chunk-PPE5KZJK.js.map +0 -1
  171. package/dist/chunk-SNSTJLQW.js.map +0 -1
  172. package/dist/chunk-W7Z3BGCI.js.map +0 -1
  173. package/dist/chunk-X3B723J4.js.map +0 -1
  174. package/dist/client-AUE3XUPO.js +0 -11
  175. package/dist/db-B35OGV3G.js.map +0 -1
  176. package/dist/digest-BL6UJYKH.js.map +0 -1
  177. package/dist/dump-P6EBSIOH.js.map +0 -1
  178. package/dist/ensure-site-DFTB7MN3.js.map +0 -1
  179. package/dist/forms-notify-target-75U6OU7L.js.map +0 -1
  180. package/dist/header-images-335L2AKB.js.map +0 -1
  181. package/dist/health-mirror-4B2L7T2Y.js +0 -75
  182. package/dist/health-mirror-4B2L7T2Y.js.map +0 -1
  183. package/dist/launch-XNAX427U.js.map +0 -1
  184. package/dist/migrate-GQH237A6.js +0 -7
  185. package/dist/parity-MWS24A3H.js +0 -12
  186. package/dist/prismic-models-QBGUT2Q3.js.map +0 -1
  187. package/dist/prospect-audit-IUNCJ2LV.js.map +0 -1
  188. package/dist/render-AQAVQDPZ.js +0 -10
  189. package/dist/report-4EVNDHOZ.js.map +0 -1
  190. package/dist/write-audits-to-airtable-MBEILLPU.js +0 -14
  191. /package/dist/{airtable-G7WBQQMI.js.map → airtable-QG5ICI3Y.js.map} +0 -0
  192. /package/dist/{client-AUE3XUPO.js.map → attachments-NLZIJ5NU.js.map} +0 -0
  193. /package/dist/{blux-ZGDSHI3U.js.map → blux-DN4MI532.js.map} +0 -0
  194. /package/dist/{bump-deps-EC74CPLS.js.map → bump-deps-KGH5UYSF.js.map} +0 -0
  195. /package/dist/{chunk-N45M3FDK.js.map → chunk-6I6L3CMX.js.map} +0 -0
  196. /package/dist/{chunk-KTH5ZODA.js.map → chunk-7BZUOZPU.js.map} +0 -0
  197. /package/dist/{chunk-3VQWSE3F.js.map → chunk-7UAYMAKR.js.map} +0 -0
  198. /package/dist/{chunk-Z7AXYKQB.js.map → chunk-IY6256GK.js.map} +0 -0
  199. /package/dist/{chunk-BNM5POSJ.js.map → chunk-JRHIFA5R.js.map} +0 -0
  200. /package/dist/{chunk-ZKXSX4LO.js.map → chunk-K6SLF6GI.js.map} +0 -0
  201. /package/dist/{chunk-SLUQ7R6I.js.map → chunk-PR343K7F.js.map} +0 -0
  202. /package/dist/{chunk-P2FE5AI6.js.map → chunk-Q47DU255.js.map} +0 -0
  203. /package/dist/{chunk-VSBFM7DS.js.map → chunk-SJOXWJ5Y.js.map} +0 -0
  204. /package/dist/{chunk-TERE5SDF.js.map → chunk-ULHPALI5.js.map} +0 -0
  205. /package/dist/{chunk-XUGSZP7Z.js.map → chunk-W45FZEGE.js.map} +0 -0
  206. /package/dist/{chunk-JHGHLKFS.js.map → chunk-WS6NU475.js.map} +0 -0
  207. /package/dist/{chunk-6TWWY7ZU.js.map → chunk-Z3JXLULD.js.map} +0 -0
  208. /package/dist/{import-airtable-HE43FB4H.js.map → client-JYKPVCJX.js.map} +0 -0
  209. /package/dist/{convert-to-pnpm-6L6P74WR.js.map → convert-to-pnpm-NWB5FCQ2.js.map} +0 -0
  210. /package/dist/{lighthouse-airtable-LBVIYYP5.js.map → digest-state-F37XUCS5.js.map} +0 -0
  211. /package/dist/{migrate-GQH237A6.js.map → fleet-state-R5LU7IC3.js.map} +0 -0
  212. /package/dist/{header-image-AWLLQA4L.js.map → header-image-7A7XLVEU.js.map} +0 -0
  213. /package/dist/{health-endpoint-WJC2UMYS.js.map → health-endpoint-UVX6R5N5.js.map} +0 -0
  214. /package/dist/{notify-C645PGD5.js.map → import-airtable-OHVY344G.js.map} +0 -0
  215. /package/dist/{init-NJEOEO7I.js.map → init-FGTYNGUI.js.map} +0 -0
  216. /package/dist/{orchestrate-77UO4BYJ.js.map → lighthouse-airtable-3GUWHETW.js.map} +0 -0
  217. /package/dist/{parity-MWS24A3H.js.map → migrate-YITCXBLS.js.map} +0 -0
  218. /package/dist/{pipeline-S6CVHDGB.js.map → notify-4HP4GZ2H.js.map} +0 -0
  219. /package/dist/{onboard-GK4PHPNI.js.map → onboard-BKCYBAVW.js.map} +0 -0
  220. /package/dist/{render-AQAVQDPZ.js.map → orchestrate-JNKCWU5K.js.map} +0 -0
  221. /package/dist/{spam-classifier-XLBFCO7G.js.map → parity-NBYZUBFU.js.map} +0 -0
  222. /package/dist/{websites-SXWTP72S.js.map → pipeline-WYTXSLHX.js.map} +0 -0
  223. /package/dist/{prismic-ci-3RHOZUUT.js.map → prismic-ci-MY2U6NN7.js.map} +0 -0
  224. /package/dist/{write-audits-to-airtable-MBEILLPU.js.map → prospect/types.js.map} +0 -0
  225. /package/dist/{replay-C2LXMJZP.js.map → replay-4S6ENYCH.js.map} +0 -0
  226. /package/dist/{self-updating-6DWI2Z56.js.map → self-updating-NTCCWJYZ.js.map} +0 -0
  227. /package/dist/{smoke-suite-VYY2LXM2.js.map → smoke-suite-4JFZZYVW.js.map} +0 -0
  228. /package/dist/{submissions-Q7DRFOCD.js.map → submissions-TEHPBSZJ.js.map} +0 -0
  229. /package/dist/{svelte-codemods-25J2FO22.js.map → svelte-codemods-QAPDZC6F.js.map} +0 -0
  230. /package/dist/{sync-configs-3SE6PSCO.js.map → sync-configs-34JIA6KZ.js.map} +0 -0
  231. /package/dist/{upgrade-WBRQ62FX.js.map → upgrade-IR2OO3KP.js.map} +0 -0
  232. /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
  233. /package/dist/{webhook-VOGOW3W6.js.map → webhook-XM6EV4XT.js.map} +0 -0
@@ -0,0 +1,281 @@
1
+ import { a as AuditResult } from '../types-OIQZ5hWN.js';
2
+
3
+ /** Every pipeline stage resolves to this. A failed stage degrades its report
4
+ * section to "not measured" — it never kills the run (spec: error handling). */
5
+ type StageResult<T> = {
6
+ ok: true;
7
+ data: T;
8
+ } | {
9
+ ok: false;
10
+ error: string;
11
+ };
12
+ type RobotsAgentAccess = {
13
+ agent: string;
14
+ /** May this agent fetch "/" per robots.txt? (No robots.txt → allowed.) */
15
+ allowed: boolean;
16
+ /** The deciding rule, e.g. "User-agent: GPTBot → Disallow: /". Null = no rule matched. */
17
+ matchedRule: string | null;
18
+ };
19
+ type PageExtract = {
20
+ title: string | null;
21
+ metaDescription: string | null;
22
+ canonical: string | null;
23
+ /** property/name → content for og:* and twitter:* metas. */
24
+ social: Record<string, string>;
25
+ /** `level` is 1-6 (H1-H6). */
26
+ headings: {
27
+ level: number;
28
+ text: string;
29
+ }[];
30
+ /** Raw text of each <script type="application/ld+json"> block. */
31
+ jsonLd: string[];
32
+ images: {
33
+ total: number;
34
+ withAlt: number;
35
+ };
36
+ hasViewportMeta: boolean;
37
+ /** Visible text, whitespace-collapsed. */
38
+ text: string;
39
+ };
40
+ type PageCapture = {
41
+ url: string;
42
+ /** HTTP status of the raw fetch. Null = the fetch itself failed. */
43
+ status: number | null;
44
+ /** Extract of the raw HTTP HTML (what non-JS crawlers see). */
45
+ raw: PageExtract | null;
46
+ /** Extract of the Playwright-rendered DOM (what a browser sees). */
47
+ rendered: PageExtract | null;
48
+ error: string | null;
49
+ };
50
+ type CrawlResult = {
51
+ /** Normalized origin, e.g. "https://example.com". */
52
+ origin: string;
53
+ robotsTxt: string | null;
54
+ /** One entry per agent in crawl.ts's ALL_AGENTS (6 AI + 2 classical). */
55
+ agentAccess: RobotsAgentAccess[];
56
+ sitemap: {
57
+ present: boolean;
58
+ urlCount: number;
59
+ };
60
+ llmsTxt: {
61
+ present: boolean;
62
+ firstLine: string | null;
63
+ };
64
+ /** Per sidecar, the error that stopped us fetching it, or null. A fetch that
65
+ * FAILED must never be reported as "the site has no robots.txt" — that would
66
+ * claim the crawlers are unrestricted when we simply did not look. */
67
+ sidecarErrors: {
68
+ robots: string | null;
69
+ llms: string | null;
70
+ sitemap: string | null;
71
+ };
72
+ /** Lower-cased homepage response headers (security-header check input). */
73
+ homeHeaders: Record<string, string>;
74
+ pages: PageCapture[];
75
+ };
76
+ type ChecksResult = {
77
+ /** False when the robots.txt fetch itself failed, so "no AI crawler is
78
+ * blocked" would be our silence rather than the site's answer. The report
79
+ * must say "not measured" for crawler access when this is false. */
80
+ crawlerAccessMeasured: boolean;
81
+ crawlerAccess: {
82
+ blockedAi: string[];
83
+ allowedAi: string[];
84
+ blockedClassical: string[];
85
+ };
86
+ jsDependence: {
87
+ /** 0..1 — fraction of rendered words absent from the raw HTML, weighted by
88
+ * page size (total missing words / total rendered words across every
89
+ * comparable page) — or null when no page produced a comparable pair.
90
+ * Never 0 for "nothing measured": a page whose raw fetch was blocked, or
91
+ * whose rendered text tokenizes to nothing, drops out of the average
92
+ * rather than forcing it toward a false "perfectly crawlable". */
93
+ avgMissing: number | null;
94
+ perPage: {
95
+ url: string;
96
+ /** 0..1 — this page's own missing fraction (unweighted). */
97
+ missing: number;
98
+ /** Token count wordSet(rendered.text) produced for this page — the
99
+ * weight avgMissing gives it, and why a "coming soon" stub can't swing
100
+ * the headline number the way a full page does. */
101
+ renderedWords: number;
102
+ }[];
103
+ };
104
+ schema: {
105
+ typesFound: string[];
106
+ missingExpected: string[];
107
+ invalidBlocks: number;
108
+ };
109
+ meta: {
110
+ pageCount: number;
111
+ missingTitle: number;
112
+ missingDescription: number;
113
+ missingCanonical: number;
114
+ missingSocial: number;
115
+ /** Pages where neither the raw nor the rendered fetch produced an extract
116
+ * — excluded from pageCount and every other meta denominator above, so a
117
+ * report reading "1 of 2 pages missing a description" doesn't silently
118
+ * hide the third page that produced nothing at all. */
119
+ pagesWithoutExtract: number;
120
+ };
121
+ headings: {
122
+ pagesWithoutH1: number;
123
+ pagesWithLevelSkips: number;
124
+ };
125
+ securityHeaders: {
126
+ present: string[];
127
+ missing: string[];
128
+ };
129
+ /** False when the sitemap.xml fetch itself failed (crawl.sidecarErrors.sitemap
130
+ * !== null), so `sitemapPresent: false` would otherwise read as "confirmed
131
+ * absent" rather than "we never got an answer". The report must say "not
132
+ * measured" for sitemap presence when this is false, and computeScores must
133
+ * treat it as neutral rather than penalizing the technical component for a
134
+ * fetch we could not complete. */
135
+ sitemapMeasured: boolean;
136
+ sitemapPresent: boolean;
137
+ /** Same as sitemapMeasured, for the llms.txt fetch (crawl.sidecarErrors.llms). */
138
+ llmsTxtMeasured: boolean;
139
+ llmsTxtPresent: boolean;
140
+ viewportOk: boolean;
141
+ };
142
+ type BuyerQuestion = {
143
+ question: string;
144
+ answered: "yes" | "partial" | "no";
145
+ /** Is there a passage an AI answer could quote verbatim? */
146
+ quotable: boolean;
147
+ page: string | null;
148
+ evidence: string | null;
149
+ };
150
+ type Fix = {
151
+ title: string;
152
+ why: string;
153
+ impact: "high" | "medium" | "low";
154
+ effort: "low" | "medium" | "high";
155
+ tier: "crawl" | "content" | "technical";
156
+ };
157
+ type AnalyzeResult = {
158
+ /** The business's proper name, as a searchable proper noun — "Acme Roofing",
159
+ * not a description of it. Empty string when the site never states one,
160
+ * which is itself a finding. This is what the visibility probes query and
161
+ * brand-match against; `business` below is prose and unsearchable. */
162
+ businessName: string;
163
+ /** The model's read of what this company does, for whom, where. */
164
+ business: string;
165
+ entityClarity: {
166
+ score: number;
167
+ missing: string[];
168
+ };
169
+ buyerQuestions: BuyerQuestion[];
170
+ /** Standalone searches for the visibility probes — what a buyer types before
171
+ * they know this company exists. Distinct from `buyerQuestions`, which are
172
+ * phrased about this site and are unanswerable on their own; see the schema
173
+ * comment in analyze.ts for what that cost us. Never mentions the business. */
174
+ categoryQueries: string[];
175
+ fixes: Fix[];
176
+ narrative: {
177
+ findability: string;
178
+ readability: string;
179
+ answers: string;
180
+ };
181
+ };
182
+ type ProbeAnswer = {
183
+ engine: string;
184
+ query: string;
185
+ /** "branded" ("who is X" / "X reviews") always echoes the name back even when
186
+ * an engine knows nothing real, so it cannot be trusted as a discoverability
187
+ * signal. "category" is a real buyer question — the one the score is built
188
+ * from. "competitor" is a head-to-head comparison query. */
189
+ kind: "branded" | "category" | "competitor";
190
+ domainCited: boolean;
191
+ brandMentioned: boolean;
192
+ /**
193
+ * Did this answer count toward `visibilityScore`?
194
+ *
195
+ * Recorded here rather than re-derived by the renderer, because the gate is
196
+ * not simply `domainCited || brandMentioned` — a mention only counts when the
197
+ * name is distinctive enough for it to mean anything (see `isDistinctiveName`).
198
+ * The report used to re-derive it with the looser rule and printed "You were
199
+ * named in this answer" above a card contributing zero to the number beside it.
200
+ *
201
+ * Optional because reports persisted before this field existed still render;
202
+ * `render.ts` falls back to the old expression for those, which is what those
203
+ * documents already said.
204
+ */
205
+ countedAsVisible?: boolean;
206
+ citedDomains: string[];
207
+ /** First ~300 chars of the engine's answer — the report's receipt. */
208
+ snippet: string;
209
+ /** True when `snippet` is a truncated prefix of a longer answer — set
210
+ * right where the truncation happens (probes.ts's SNIPPET_CHARS), so the
211
+ * renderer never has to re-derive "was this cut short?" from a length
212
+ * check of its own against a constant it can't see. */
213
+ truncated: boolean;
214
+ /** ISO-8601, set when the answer came back — this stage's whole output is
215
+ * quotable claims about a live third party, and answer engines' answers
216
+ * drift, so a disputed line in a report needs a date attached. */
217
+ askedAt: string;
218
+ };
219
+ type ProbesResult = {
220
+ answers: ProbeAnswer[];
221
+ /** 0..100 — fraction of CATEGORY answers (real buyer questions) where the
222
+ * prospect was cited or mentioned. Null — never 0 — when no category query
223
+ * ran at all, e.g. the analyze stage failed and supplied no buyer questions.
224
+ * Deliberately excludes branded answers: "who is X"/"X reviews" echo the name
225
+ * back even when the engine knows nothing real, which would put a floor
226
+ * under the score that has nothing to do with discoverability. */
227
+ visibilityScore: number | null;
228
+ /** Did any branded query produce a real citation of the prospect's own
229
+ * domain — not merely an echo of the name in prose. Reported separately from
230
+ * visibilityScore because it answers a different question ("does the engine
231
+ * know this business exists at all") from the one the score answers ("would
232
+ * it surface this business to someone who didn't already name it"). */
233
+ brandedRecognized: boolean;
234
+ competitorsSeen: {
235
+ domain: string;
236
+ count: number;
237
+ }[];
238
+ };
239
+ type LighthouseScores = {
240
+ performance: number | null;
241
+ accessibility: number | null;
242
+ bestPractices: number | null;
243
+ seo: number | null;
244
+ summary: string;
245
+ status: AuditResult["status"];
246
+ };
247
+ type Scores = {
248
+ findability: number | null;
249
+ readability: number | null;
250
+ answers: number | null;
251
+ aiVisibility: number | null;
252
+ };
253
+ type ProspectAuditResult = {
254
+ url: string;
255
+ /** The resolved searchable proper NOUN — "Acme Roofing", not a description
256
+ * of it (mirrors `AnalyzeResult.businessName`, the value this is usually
257
+ * copied from). Distinct from `AnalyzeResult.business` (prose, nested
258
+ * under `analyze.data`) — the two share a root name only because they're
259
+ * one level apart, not because they hold the same kind of value. Null
260
+ * when no name was ever resolved (operator override empty, model returned
261
+ * none) — itself a finding, not an error. Persisted verbatim into the
262
+ * database's `business` COLUMN (kept as-is; a rename there would need its
263
+ * own migration for no benefit) — map at that one boundary, not here. */
264
+ businessName: string | null;
265
+ generatedAt: string;
266
+ scores: Scores;
267
+ /** The crawl is the one fatal stage (see pipeline.ts): a crawl failure
268
+ * throws before a ProspectAuditResult is ever built, so the `ok:false`
269
+ * branch of StageResult can never appear here. Narrowed to the success
270
+ * shape only, so consumers never handle the impossible branch. */
271
+ crawl: {
272
+ ok: true;
273
+ data: CrawlResult;
274
+ };
275
+ checks: StageResult<ChecksResult>;
276
+ lighthouse: StageResult<LighthouseScores>;
277
+ analyze: StageResult<AnalyzeResult>;
278
+ probes: StageResult<ProbesResult>;
279
+ };
280
+
281
+ export type { AnalyzeResult, BuyerQuestion, ChecksResult, CrawlResult, Fix, LighthouseScores, PageCapture, PageExtract, ProbeAnswer, ProbesResult, ProspectAuditResult, RobotsAgentAccess, Scores, StageResult };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -1,16 +1,23 @@
1
1
  import {
2
2
  hostnameOf,
3
- isHttpUrl
4
- } from "./chunk-EVBCK2KC.js";
3
+ isHttpUrl,
4
+ isPrivateOrLoopbackHost
5
+ } from "./chunk-RARREDQE.js";
5
6
 
6
7
  // src/cli/commands/prospect-audit.ts
7
8
  import { mkdir, writeFile } from "fs/promises";
8
9
  import { resolve } from "path";
9
10
 
10
- // src/dashboard/handler-helpers.ts
11
- var DEFAULT_DASHBOARD_BASE_URL = "https://reddoor-maintenance.netlify.app";
12
- function resolveDashboardBaseUrl(raw) {
13
- return (raw?.trim() || DEFAULT_DASHBOARD_BASE_URL).replace(/\/$/, "");
11
+ // src/prospect/report-url.ts
12
+ var DEFAULT_REPORT_BASE_URL = "https://reddoorla.com";
13
+ function resolveReportBaseUrl(raw) {
14
+ return (raw?.trim() || DEFAULT_REPORT_BASE_URL).replace(/\/$/, "");
15
+ }
16
+ function reportUrl(token, raw = process.env.REPORT_BASE_URL) {
17
+ return `${resolveReportBaseUrl(raw)}/audit/${token}`;
18
+ }
19
+ function reportPrintUrl(token, raw = process.env.REPORT_BASE_URL) {
20
+ return `${reportUrl(token, raw)}/print`;
14
21
  }
15
22
 
16
23
  // src/cli/commands/prospect-audit.ts
@@ -79,13 +86,16 @@ async function runProspectAuditCommand(url, opts) {
79
86
  if (!isHttpUrl(url)) {
80
87
  return fail(`"${url}" is not a URL. Pass the full address, e.g. https://example.com`);
81
88
  }
89
+ if (isPrivateOrLoopbackHost(new URL(url).hostname)) {
90
+ return fail(`"${url}" is a private or loopback address \u2014 refusing to audit it.`);
91
+ }
82
92
  const canPersist = Boolean(process.env.TURSO_DATABASE_URL);
83
93
  if (!canPersist && !opts.out) {
84
94
  return fail(
85
95
  "No TURSO_DATABASE_URL, so the report cannot be saved or shared. Re-run with --out <file>, or set the Turso credentials."
86
96
  );
87
97
  }
88
- const { runProspectAudit } = await import("./pipeline-S6CVHDGB.js");
98
+ const { runProspectAudit } = await import("./pipeline-WYTXSLHX.js");
89
99
  const onStage = (name, status, detail) => {
90
100
  if (status === "start") console.error(`\u2026 ${name}`);
91
101
  else if (status === "ok") console.error(`\u2713 ${name}`);
@@ -103,7 +113,7 @@ async function runProspectAuditCommand(url, opts) {
103
113
  },
104
114
  { ...opts.deps ?? {}, onStage }
105
115
  );
106
- const { renderProspectReport } = await import("./render-AQAVQDPZ.js");
116
+ const { renderProspectReport } = await import("./render-VX72AESY.js");
107
117
  const html = renderProspectReport(result);
108
118
  const warnings = [];
109
119
  let file = null;
@@ -120,7 +130,7 @@ async function runProspectAuditCommand(url, opts) {
120
130
  let auditId = null;
121
131
  if (canPersist) {
122
132
  try {
123
- const { openDb, readDbConfig } = await import("./client-AUE3XUPO.js");
133
+ const { openDb, readDbConfig } = await import("./client-JYKPVCJX.js");
124
134
  const { createProspectAudit } = await import("./prospect-audits-3PMIO73D.js");
125
135
  const db = await openDb(readDbConfig());
126
136
  const created = await createProspectAudit(db, {
@@ -135,7 +145,7 @@ async function runProspectAuditCommand(url, opts) {
135
145
  });
136
146
  auditId = created.id;
137
147
  token = created.token;
138
- link = `${resolveDashboardBaseUrl(process.env.DASHBOARD_BASE_URL)}/r/${token}`;
148
+ link = reportUrl(token);
139
149
  } catch (err) {
140
150
  warnings.push(`Could not save to the database: ${errorMessage(err)}`);
141
151
  }
@@ -143,12 +153,21 @@ async function runProspectAuditCommand(url, opts) {
143
153
  const delivered = file !== null || link !== null;
144
154
  const recovery = delivered ? null : await writeRecoveryFiles(result, html);
145
155
  const code = delivered ? 0 : 1;
156
+ let pdf = null;
157
+ if (opts.email && token) {
158
+ try {
159
+ const { renderReportPdf } = await import("./pdf-N5QLQ5ZQ.js");
160
+ pdf = await renderReportPdf(reportPrintUrl(token));
161
+ } catch (err) {
162
+ warnings.push(`Could not attach the PDF: ${errorMessage(err)}`);
163
+ }
164
+ }
146
165
  let email = null;
147
166
  if (opts.email) {
148
167
  try {
149
- const { sendAuditEmail, parseProspectAuditRecipients } = await import("./email-VTTTKKL5.js");
168
+ const { sendAuditEmail, parseProspectAuditRecipients } = await import("./email-SL7464YI.js");
150
169
  const recipients = parseProspectAuditRecipients(process.env.PROSPECT_AUDIT_RECIPIENTS);
151
- email = await sendAuditEmail(result, { link, recipients, auditId });
170
+ email = await sendAuditEmail(result, { link, recipients, auditId, pdf });
152
171
  if (!email.sent) {
153
172
  warnings.push(`Could not email the audit sheet: ${email.reason}`);
154
173
  }
@@ -171,4 +190,4 @@ async function runProspectAuditCommand(url, opts) {
171
190
  export {
172
191
  runProspectAuditCommand
173
192
  };
174
- //# sourceMappingURL=prospect-audit-IUNCJ2LV.js.map
193
+ //# sourceMappingURL=prospect-audit-JMBRVXSG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/commands/prospect-audit.ts","../src/prospect/report-url.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { hostnameOf, isHttpUrl, isPrivateOrLoopbackHost } from \"../../util/url.js\";\nimport { reportUrl, reportPrintUrl } from \"../../prospect/report-url.js\";\nimport type { ProspectAuditStatus } from \"../../db/prospect-audits.js\";\nimport type { PipelineDeps, StageName } from \"../../prospect/pipeline.js\";\nimport type { ProspectAuditResult } from \"../../prospect/types.js\";\nimport type { SendAuditEmailResult } from \"../../prospect/email.js\";\n\nexport type ProspectAuditCliOptions = {\n business?: string;\n /** Comma-separated competitor domains. */\n competitors?: string;\n /** cac sets this false for `--no-probes`. */\n probes?: boolean;\n out?: string;\n json?: boolean;\n /**\n * Email the internal sheet (scores, what wasn't measured and why, top\n * fixes, and the shareable link) to `PROSPECT_AUDIT_RECIPIENTS`, once the\n * report is rendered and the `--out`/Turso delivery attempts finish. A\n * missing/empty recipients var, or a Resend failure, is a warning — never a\n * crash — because by the time this runs the audit is already delivered (or\n * recovered) by the code above it.\n */\n email?: boolean;\n /** Test seam: injected pipeline deps. Never set from the CLI. */\n deps?: PipelineDeps;\n};\n\ntype RecoveryWrite = { htmlPath: string; jsonPath: string };\n\nfunction fail(message: string): { output: string; code: number } {\n return { output: message, code: 2 };\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nfunction scoreLine(label: string, value: number | null): string {\n return `${label.padEnd(14)} ${value === null ? \"not measured\" : String(value).padStart(3)}`;\n}\n\n/** Item 3: the `status` column exists to answer, without deserializing the\n * whole result_json, whether this was a clean run or a degraded one. crawl\n * is excluded — it's the one fatal stage (see pipeline.ts), so by the time a\n * ProspectAuditResult exists it was always ok. Every other stage's\n * StageResult already collapses \"failed\" and \"deliberately skipped\"\n * (--no-probes, the checks→analyze cascade) into the same `ok: false` — both\n * mean the report has a \"not measured\" section, so both count as partial\n * here too. */\nfunction auditStatus(result: ProspectAuditResult): ProspectAuditStatus {\n const allStagesOk =\n result.checks.ok && result.lighthouse.ok && result.analyze.ok && result.probes.ok;\n return allStagesOk ? \"complete\" : \"partial\";\n}\n\n/** Filesystem-safe, never empty. Collapses anything outside [a-z0-9.-] to a\n * single dash so a hostname becomes a sane filename fragment. */\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\n/**\n * Last-resort write when NEITHER `--out` nor the database persist made it —\n * the one situation a paid, already-run audit must never end in silently. Lands\n * in `reports/`, mirroring header-image.ts's own convention for CLI-written\n * artifacts (mkdir -p, resolved against cwd), rather than a bare file dropped\n * into whatever directory the operator happened to be standing in when they\n * ran the command — often unrelated to this repo at all in fleet-tool usage.\n */\nasync function writeRecoveryFiles(\n result: ProspectAuditResult,\n html: string,\n): Promise<RecoveryWrite> {\n const dir = resolve(\"reports\");\n await mkdir(dir, { recursive: true });\n const stamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const base = `prospect-audit-${slugify(hostnameOf(result.url))}-${stamp}`;\n const htmlPath = resolve(dir, `${base}.html`);\n const jsonPath = resolve(dir, `${base}.json`);\n await writeFile(htmlPath, html, \"utf-8\");\n await writeFile(jsonPath, JSON.stringify(result, null, 2), \"utf-8\");\n return { htmlPath, jsonPath };\n}\n\nfunction summarize(\n result: ProspectAuditResult,\n link: string | null,\n file: string | null,\n warnings: string[],\n recovery: RecoveryWrite | null,\n email: SendAuditEmailResult | null,\n): string {\n const lines = [\n `Prospect audit — ${result.businessName ?? result.url}`,\n \"\",\n scoreLine(\"Findability\", result.scores.findability),\n scoreLine(\"Readability\", result.scores.readability),\n scoreLine(\"Answers\", result.scores.answers),\n scoreLine(\"AI Visibility\", result.scores.aiVisibility),\n ];\n for (const [name, stage] of [\n [\"checks\", result.checks],\n [\"lighthouse\", result.lighthouse],\n [\"analyze\", result.analyze],\n [\"probes\", result.probes],\n ] as const) {\n if (!stage.ok) lines.push(` ! ${name} not measured — ${stage.error}`);\n }\n if (file) lines.push(\"\", `Report written to ${file}`);\n if (link) lines.push(\"\", `Shareable link: ${link}`);\n if (email?.sent) {\n lines.push(\"\", `Emailed to ${email.recipients.join(\", \")} (${email.messageId})`);\n }\n for (const w of warnings) lines.push(\"\", `! ${w}`);\n if (recovery) {\n lines.push(\n \"\",\n \"Neither --out nor the database took this report — wrote a recovery copy instead:\",\n ` ${recovery.htmlPath}`,\n ` ${recovery.jsonPath}`,\n );\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Run one prospect audit end to end. Progress goes to stderr so `--json` stdout\n * stays pipeable. Persistence needs Turso; without it `--out` is mandatory,\n * because an audit nobody can read afterwards is just a bill.\n *\n * By the time `runProspectAudit` resolves, the money is already spent — only\n * the crawl is fatal, every other stage degrades rather than throwing. So the\n * `--out` write and the database persist below are each wrapped in their own\n * try/catch and attempted independently: a bad `--out` path must not cost a\n * working Turso persist (or vice versa), and if BOTH fail — or persist fails\n * with no `--out` given at all — a recovery copy goes to `reports/` rather\n * than the run ending with nothing anywhere. `--json` only changes the SHAPE\n * of stdout; it still runs the same `--out`/persist/recovery logic beneath it.\n */\nexport async function runProspectAuditCommand(\n url: string,\n opts: ProspectAuditCliOptions,\n): Promise<{ output: string; code: number }> {\n if (!isHttpUrl(url)) {\n return fail(`\"${url}\" is not a URL. Pass the full address, e.g. https://example.com`);\n }\n // #612 review: this validated only the SHAPE. The sole private-address guard\n // was in `triggerProspectAudit` — one layer at the far end of the chain — so\n // anyone running the CLI directly, or any future second caller, had none.\n // `crawlSite` now refuses too; this keeps the refusal a clean message instead\n // of a thrown crawl error.\n if (isPrivateOrLoopbackHost(new URL(url).hostname)) {\n return fail(`\"${url}\" is a private or loopback address — refusing to audit it.`);\n }\n const canPersist = Boolean(process.env.TURSO_DATABASE_URL);\n if (!canPersist && !opts.out) {\n return fail(\n \"No TURSO_DATABASE_URL, so the report cannot be saved or shared. Re-run with --out <file>, or set the Turso credentials.\",\n );\n }\n\n const { runProspectAudit } = await import(\"../../prospect/pipeline.js\");\n const onStage = (name: StageName, status: \"start\" | \"ok\" | \"fail\", detail?: string): void => {\n if (status === \"start\") console.error(`… ${name}`);\n else if (status === \"ok\") console.error(`✓ ${name}`);\n else console.error(`! ${name} — ${detail ?? \"failed\"}`);\n };\n\n const business = opts.business?.trim();\n const result = await runProspectAudit(\n url,\n {\n ...(business ? { business } : {}),\n ...(opts.competitors\n ? {\n competitors: opts.competitors\n .split(\",\")\n .map((c) => c.trim())\n .filter(Boolean),\n }\n : {}),\n ...(opts.probes === false ? { probes: false } : {}),\n },\n { ...(opts.deps ?? {}), onStage },\n );\n\n const { renderProspectReport } = await import(\"../../prospect/render.js\");\n const html = renderProspectReport(result);\n\n const warnings: string[] = [];\n\n let file: string | null = null;\n if (opts.out) {\n try {\n await writeFile(opts.out, html, \"utf-8\");\n file = opts.out;\n } catch (err) {\n warnings.push(`Could not write --out (${opts.out}): ${errorMessage(err)}`);\n }\n }\n\n let link: string | null = null;\n let token: string | null = null;\n let auditId: string | null = null;\n if (canPersist) {\n try {\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { createProspectAudit } = await import(\"../../db/prospect-audits.js\");\n const db = await openDb(readDbConfig());\n const created = await createProspectAudit(db, {\n url: result.url,\n // Map at the boundary: ProspectAuditResult.businessName is the field\n // name (Item 2 — it's a resolved NAME, not a description); the\n // `prospect_audits.business` column keeps its existing name, since\n // renaming a column needs its own migration for no benefit here.\n business: result.businessName,\n status: auditStatus(result),\n resultJson: JSON.stringify(result),\n });\n auditId = created.id;\n token = created.token;\n link = reportUrl(token);\n } catch (err) {\n warnings.push(`Could not save to the database: ${errorMessage(err)}`);\n }\n }\n\n const delivered = file !== null || link !== null;\n const recovery = delivered ? null : await writeRecoveryFiles(result, html);\n const code = delivered ? 0 : 1;\n\n // The audit is already delivered (or recovered) above — an email failure\n // must never take that away. Same independent try/catch shape as the\n // --out write and the Turso persist: record a warning, keep going, never\n // touch `code`. Dynamically imported for the same reason db/client.js and\n // prospect/render.js are above: it reaches `resend` (a devDependency) and\n // must stay out of bin.js's static import graph (see bin.ts's\n // central-dep-blocker comment).\n // The PDF leave-behind, printed from the website's print route — a document\n // designed for paper, not the interactive report with its evidence folded\n // away behind disclosures.\n //\n // Best-effort by design. Every other stage in this pipeline degrades rather\n // than throwing, and an attachment is not worth losing a delivered report\n // over: if this fails the email still goes, with the link, and a warning says\n // the PDF is missing. It also needs a token — with no persisted report there\n // is no page to print.\n let pdf: Buffer | null = null;\n if (opts.email && token) {\n try {\n const { renderReportPdf } = await import(\"../../prospect/pdf.js\");\n pdf = await renderReportPdf(reportPrintUrl(token));\n } catch (err) {\n warnings.push(`Could not attach the PDF: ${errorMessage(err)}`);\n }\n }\n\n let email: SendAuditEmailResult | null = null;\n if (opts.email) {\n try {\n const { sendAuditEmail, parseProspectAuditRecipients } =\n await import(\"../../prospect/email.js\");\n const recipients = parseProspectAuditRecipients(process.env.PROSPECT_AUDIT_RECIPIENTS);\n email = await sendAuditEmail(result, { link, recipients, auditId, pdf });\n if (!email.sent) {\n warnings.push(`Could not email the audit sheet: ${email.reason}`);\n }\n } catch (err) {\n warnings.push(`Could not email the audit sheet: ${errorMessage(err)}`);\n }\n }\n\n if (opts.json) {\n return {\n output: JSON.stringify(\n { result, out: file, link, token, recovery, warnings, email },\n null,\n 2,\n ),\n code,\n };\n }\n\n return { output: summarize(result, link, file, warnings, recovery, email), code };\n}\n","/** Where the prospect-facing report lives now: a real, branded route on the\n * marketing site rather than generated HTML on the ops app's domain. */\nexport const DEFAULT_REPORT_BASE_URL = \"https://reddoorla.com\";\n\n/**\n * Resolve the public report origin.\n *\n * Deliberately its own variable rather than reusing `DASHBOARD_BASE_URL`. That\n * one addresses operators and points at the ops app; this one addresses a\n * prospect and points at reddoorla.com. They are different audiences on\n * different domains, and sharing a variable would mean one of them silently\n * moving the day the other is repointed.\n */\nexport function resolveReportBaseUrl(raw: string | undefined): string {\n return (raw?.trim() || DEFAULT_REPORT_BASE_URL).replace(/\\/$/, \"\");\n}\n\n/** The link a prospect opens. */\nexport function reportUrl(token: string, raw = process.env.REPORT_BASE_URL): string {\n return `${resolveReportBaseUrl(raw)}/audit/${token}`;\n}\n\n/** The print-designed variant, which the runner renders to a PDF. Same token,\n * same guards — a separate document, not a stylesheet over the page. */\nexport function reportPrintUrl(token: string, raw = process.env.REPORT_BASE_URL): string {\n return `${reportUrl(token, raw)}/print`;\n}\n"],"mappings":";;;;;;;AAAA,SAAS,OAAO,iBAAiB;AACjC,SAAS,eAAe;;;ACCjB,IAAM,0BAA0B;AAWhC,SAAS,qBAAqB,KAAiC;AACpE,UAAQ,KAAK,KAAK,KAAK,yBAAyB,QAAQ,OAAO,EAAE;AACnE;AAGO,SAAS,UAAU,OAAe,MAAM,QAAQ,IAAI,iBAAyB;AAClF,SAAO,GAAG,qBAAqB,GAAG,CAAC,UAAU,KAAK;AACpD;AAIO,SAAS,eAAe,OAAe,MAAM,QAAQ,IAAI,iBAAyB;AACvF,SAAO,GAAG,UAAU,OAAO,GAAG,CAAC;AACjC;;;ADMA,SAAS,KAAK,SAAmD;AAC/D,SAAO,EAAE,QAAQ,SAAS,MAAM,EAAE;AACpC;AAEA,SAAS,aAAa,KAAsB;AAC1C,SAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;AAEA,SAAS,UAAU,OAAe,OAA8B;AAC9D,SAAO,GAAG,MAAM,OAAO,EAAE,CAAC,IAAI,UAAU,OAAO,iBAAiB,OAAO,KAAK,EAAE,SAAS,CAAC,CAAC;AAC3F;AAUA,SAAS,YAAY,QAAkD;AACrE,QAAM,cACJ,OAAO,OAAO,MAAM,OAAO,WAAW,MAAM,OAAO,QAAQ,MAAM,OAAO,OAAO;AACjF,SAAO,cAAc,aAAa;AACpC;AAIA,SAAS,QAAQ,GAAmB;AAClC,QAAM,OAAO,EACV,YAAY,EACZ,QAAQ,iBAAiB,GAAG,EAC5B,QAAQ,YAAY,EAAE;AACzB,SAAO,QAAQ;AACjB;AAUA,eAAe,mBACb,QACA,MACwB;AACxB,QAAM,MAAM,QAAQ,SAAS;AAC7B,QAAM,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AACpC,QAAM,SAAQ,oBAAI,KAAK,GAAE,YAAY,EAAE,QAAQ,SAAS,GAAG;AAC3D,QAAM,OAAO,kBAAkB,QAAQ,WAAW,OAAO,GAAG,CAAC,CAAC,IAAI,KAAK;AACvE,QAAM,WAAW,QAAQ,KAAK,GAAG,IAAI,OAAO;AAC5C,QAAM,WAAW,QAAQ,KAAK,GAAG,IAAI,OAAO;AAC5C,QAAM,UAAU,UAAU,MAAM,OAAO;AACvC,QAAM,UAAU,UAAU,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,OAAO;AAClE,SAAO,EAAE,UAAU,SAAS;AAC9B;AAEA,SAAS,UACP,QACA,MACA,MACA,UACA,UACA,OACQ;AACR,QAAM,QAAQ;AAAA,IACZ,yBAAoB,OAAO,gBAAgB,OAAO,GAAG;AAAA,IACrD;AAAA,IACA,UAAU,eAAe,OAAO,OAAO,WAAW;AAAA,IAClD,UAAU,eAAe,OAAO,OAAO,WAAW;AAAA,IAClD,UAAU,WAAW,OAAO,OAAO,OAAO;AAAA,IAC1C,UAAU,iBAAiB,OAAO,OAAO,YAAY;AAAA,EACvD;AACA,aAAW,CAAC,MAAM,KAAK,KAAK;AAAA,IAC1B,CAAC,UAAU,OAAO,MAAM;AAAA,IACxB,CAAC,cAAc,OAAO,UAAU;AAAA,IAChC,CAAC,WAAW,OAAO,OAAO;AAAA,IAC1B,CAAC,UAAU,OAAO,MAAM;AAAA,EAC1B,GAAY;AACV,QAAI,CAAC,MAAM,GAAI,OAAM,KAAK,OAAO,IAAI,wBAAmB,MAAM,KAAK,EAAE;AAAA,EACvE;AACA,MAAI,KAAM,OAAM,KAAK,IAAI,qBAAqB,IAAI,EAAE;AACpD,MAAI,KAAM,OAAM,KAAK,IAAI,mBAAmB,IAAI,EAAE;AAClD,MAAI,OAAO,MAAM;AACf,UAAM,KAAK,IAAI,cAAc,MAAM,WAAW,KAAK,IAAI,CAAC,KAAK,MAAM,SAAS,GAAG;AAAA,EACjF;AACA,aAAW,KAAK,SAAU,OAAM,KAAK,IAAI,KAAK,CAAC,EAAE;AACjD,MAAI,UAAU;AACZ,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,KAAK,SAAS,QAAQ;AAAA,MACtB,KAAK,SAAS,QAAQ;AAAA,IACxB;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAgBA,eAAsB,wBACpB,KACA,MAC2C;AAC3C,MAAI,CAAC,UAAU,GAAG,GAAG;AACnB,WAAO,KAAK,IAAI,GAAG,iEAAiE;AAAA,EACtF;AAMA,MAAI,wBAAwB,IAAI,IAAI,GAAG,EAAE,QAAQ,GAAG;AAClD,WAAO,KAAK,IAAI,GAAG,iEAA4D;AAAA,EACjF;AACA,QAAM,aAAa,QAAQ,QAAQ,IAAI,kBAAkB;AACzD,MAAI,CAAC,cAAc,CAAC,KAAK,KAAK;AAC5B,WAAO;AAAA,MACL;AAAA,IACF;AAAA,EACF;AAEA,QAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,wBAA4B;AACtE,QAAM,UAAU,CAAC,MAAiB,QAAiC,WAA0B;AAC3F,QAAI,WAAW,QAAS,SAAQ,MAAM,UAAK,IAAI,EAAE;AAAA,aACxC,WAAW,KAAM,SAAQ,MAAM,UAAK,IAAI,EAAE;AAAA,QAC9C,SAAQ,MAAM,KAAK,IAAI,WAAM,UAAU,QAAQ,EAAE;AAAA,EACxD;AAEA,QAAM,WAAW,KAAK,UAAU,KAAK;AACrC,QAAM,SAAS,MAAM;AAAA,IACnB;AAAA,IACA;AAAA,MACE,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,MAC/B,GAAI,KAAK,cACL;AAAA,QACE,aAAa,KAAK,YACf,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AAAA,MACnB,IACA,CAAC;AAAA,MACL,GAAI,KAAK,WAAW,QAAQ,EAAE,QAAQ,MAAM,IAAI,CAAC;AAAA,IACnD;AAAA,IACA,EAAE,GAAI,KAAK,QAAQ,CAAC,GAAI,QAAQ;AAAA,EAClC;AAEA,QAAM,EAAE,qBAAqB,IAAI,MAAM,OAAO,sBAA0B;AACxE,QAAM,OAAO,qBAAqB,MAAM;AAExC,QAAM,WAAqB,CAAC;AAE5B,MAAI,OAAsB;AAC1B,MAAI,KAAK,KAAK;AACZ,QAAI;AACF,YAAM,UAAU,KAAK,KAAK,MAAM,OAAO;AACvC,aAAO,KAAK;AAAA,IACd,SAAS,KAAK;AACZ,eAAS,KAAK,0BAA0B,KAAK,GAAG,MAAM,aAAa,GAAG,CAAC,EAAE;AAAA,IAC3E;AAAA,EACF;AAEA,MAAI,OAAsB;AAC1B,MAAI,QAAuB;AAC3B,MAAI,UAAyB;AAC7B,MAAI,YAAY;AACd,QAAI;AACF,YAAM,EAAE,QAAQ,aAAa,IAAI,MAAM,OAAO,sBAAoB;AAClE,YAAM,EAAE,oBAAoB,IAAI,MAAM,OAAO,+BAA6B;AAC1E,YAAM,KAAK,MAAM,OAAO,aAAa,CAAC;AACtC,YAAM,UAAU,MAAM,oBAAoB,IAAI;AAAA,QAC5C,KAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA,QAKZ,UAAU,OAAO;AAAA,QACjB,QAAQ,YAAY,MAAM;AAAA,QAC1B,YAAY,KAAK,UAAU,MAAM;AAAA,MACnC,CAAC;AACD,gBAAU,QAAQ;AAClB,cAAQ,QAAQ;AAChB,aAAO,UAAU,KAAK;AAAA,IACxB,SAAS,KAAK;AACZ,eAAS,KAAK,mCAAmC,aAAa,GAAG,CAAC,EAAE;AAAA,IACtE;AAAA,EACF;AAEA,QAAM,YAAY,SAAS,QAAQ,SAAS;AAC5C,QAAM,WAAW,YAAY,OAAO,MAAM,mBAAmB,QAAQ,IAAI;AACzE,QAAM,OAAO,YAAY,IAAI;AAkB7B,MAAI,MAAqB;AACzB,MAAI,KAAK,SAAS,OAAO;AACvB,QAAI;AACF,YAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,mBAAuB;AAChE,YAAM,MAAM,gBAAgB,eAAe,KAAK,CAAC;AAAA,IACnD,SAAS,KAAK;AACZ,eAAS,KAAK,6BAA6B,aAAa,GAAG,CAAC,EAAE;AAAA,IAChE;AAAA,EACF;AAEA,MAAI,QAAqC;AACzC,MAAI,KAAK,OAAO;AACd,QAAI;AACF,YAAM,EAAE,gBAAgB,6BAA6B,IACnD,MAAM,OAAO,qBAAyB;AACxC,YAAM,aAAa,6BAA6B,QAAQ,IAAI,yBAAyB;AACrF,cAAQ,MAAM,eAAe,QAAQ,EAAE,MAAM,YAAY,SAAS,IAAI,CAAC;AACvE,UAAI,CAAC,MAAM,MAAM;AACf,iBAAS,KAAK,oCAAoC,MAAM,MAAM,EAAE;AAAA,MAClE;AAAA,IACF,SAAS,KAAK;AACZ,eAAS,KAAK,oCAAoC,aAAa,GAAG,CAAC,EAAE;AAAA,IACvE;AAAA,EACF;AAEA,MAAI,KAAK,MAAM;AACb,WAAO;AAAA,MACL,QAAQ,KAAK;AAAA,QACX,EAAE,QAAQ,KAAK,MAAM,MAAM,OAAO,UAAU,UAAU,MAAM;AAAA,QAC5D;AAAA,QACA;AAAA,MACF;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,QAAQ,UAAU,QAAQ,MAAM,MAAM,UAAU,UAAU,KAAK,GAAG,KAAK;AAClF;","names":[]}
@@ -2,9 +2,9 @@ import {
2
2
  ALL_CONFIG_NAMES,
3
3
  isConfigName,
4
4
  syncConfigs
5
- } from "../chunk-3VQWSE3F.js";
6
- import "../chunk-3G25KIWW.js";
5
+ } from "../chunk-7UAYMAKR.js";
7
6
  import "../chunk-73UFRZKY.js";
7
+ import "../chunk-3G25KIWW.js";
8
8
  import "../chunk-XTK5VIQB.js";
9
9
  import "../chunk-XXTZBPUY.js";
10
10
  export {
@@ -0,0 +1,10 @@
1
+ import {
2
+ renderProspectReport
3
+ } from "./chunk-A2UNJIKH.js";
4
+ import "./chunk-7TMWBXX7.js";
5
+ import "./chunk-BGTYPVLT.js";
6
+ import "./chunk-RARREDQE.js";
7
+ export {
8
+ renderProspectReport
9
+ };
10
+ //# sourceMappingURL=render-VX72AESY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,17 @@
1
+ import {
2
+ renderReportFromRow,
3
+ requireLighthouse,
4
+ windowDays
5
+ } from "./chunk-XP5WM2LV.js";
6
+ import "./chunk-GECESMVS.js";
7
+ import "./chunk-5UINJNAJ.js";
8
+ import "./chunk-KJGKZLCT.js";
9
+ import "./chunk-BGTYPVLT.js";
10
+ import "./chunk-7NYN2P6I.js";
11
+ import "./chunk-RARREDQE.js";
12
+ export {
13
+ renderReportFromRow,
14
+ requireLighthouse,
15
+ windowDays
16
+ };
17
+ //# sourceMappingURL=render-from-row-DOYJDO2L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -5,16 +5,16 @@ import {
5
5
  openBase,
6
6
  readAirtableConfig
7
7
  } from "./chunk-HN6I2XWI.js";
8
- import {
9
- makeGitHub
10
- } from "./chunk-XHHMM5HX.js";
11
- import "./chunk-LBYOLBW7.js";
12
8
  import {
13
9
  allActionableVulnsTransitive,
14
10
  isDashboardVisible,
15
11
  listWebsites,
16
12
  updateAutoFixAttempts
17
- } from "./chunk-LDWZSLSU.js";
13
+ } from "./chunk-7NYN2P6I.js";
14
+ import {
15
+ makeGitHub
16
+ } from "./chunk-XHHMM5HX.js";
17
+ import "./chunk-LBYOLBW7.js";
18
18
  import "./chunk-6VAL7XJT.js";
19
19
 
20
20
  // src/github/renovate-dispatch.ts
@@ -130,10 +130,10 @@ async function runRenovateDispatchCommand(opts) {
130
130
  for (const f of result.failed) lines.push(` \u21B3 ${f.repo}: ${f.error}`);
131
131
  }
132
132
  const attemptUpdates = computeAutoFixAttemptUpdates(websites, result);
133
- const attemptTally = await applyAutoFixAttemptUpdates(
134
- attemptUpdates,
135
- (id, attempts) => updateAutoFixAttempts(base, id, attempts)
136
- );
133
+ const attemptTally = await applyAutoFixAttemptUpdates(attemptUpdates, async (id, attempts) => {
134
+ const fields = await updateAutoFixAttempts(base, id, attempts);
135
+ await opts.siteMirror?.health(id, fields);
136
+ });
137
137
  lines.push(formatRenovateDispatchSummary(result));
138
138
  lines.push(formatAutoFixAttemptsSummary(attemptTally));
139
139
  return { output: lines.join("\n"), code: 0 };
@@ -141,4 +141,4 @@ async function runRenovateDispatchCommand(opts) {
141
141
  export {
142
142
  runRenovateDispatchCommand
143
143
  };
144
- //# sourceMappingURL=renovate-dispatch-QJY75ODR.js.map
144
+ //# sourceMappingURL=renovate-dispatch-6GQWTRKX.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/github/renovate-dispatch.ts","../src/cli/commands/renovate-dispatch.ts"],"sourcesContent":["import {\n allActionableVulnsTransitive,\n isDashboardVisible,\n type WebsiteRow,\n} from \"../reports/airtable/websites.js\";\nimport { isRenovatePR } from \"../alerts/renovate.js\";\nimport type { PullRequestSummary } from \"./gh.js\";\n\n/**\n * On-demand Renovate trigger for fleet sites.\n *\n * The nightly security sweep (`fleet-security.yml`) writes each active site's\n * vulnerability counts to Airtable. This module turns that into action: for the\n * sites the sweep just flagged with *actionable* (critical or high) vulns, it\n * dispatches that repo's `renovate.yml` `workflow_dispatch` so Renovate runs off\n * its twice-daily schedule. Renovate's OSV vulnerability alerts bypass the\n * schedule, so the remediation PR opens immediately — and because GitHub's\n * platform auto-merge is disabled fleet-wide, Renovate merges it from inside that\n * same run (or a later scheduled one) per the shared preset, which is what keeps\n * the preset's \"majors are never auto-merged\" rule authoritative. Moderate/low\n * vulns are left to the normal schedule — not urgent enough to dispatch + churn\n * CI nightly.\n */\n\n/** The per-repo workflow file the fleet uses for its self-hosted Renovate run. */\nexport const RENOVATE_WORKFLOW_FILE = \"renovate.yml\";\n\n/**\n * Does the repo have a HEALTHY open Renovate PR — one that is NOT conflicting?\n *\n * A healthy PR means remediation is genuinely in flight (moving toward merge), so\n * re-dispatching would just churn CI — skip it. A CONFLICTING Renovate PR is STUCK\n * (its branch fell behind the base after another PR merged the same lockfile), so\n * it does NOT count as healthy: re-dispatching triggers Renovate to rebase it.\n * `UNKNOWN` mergeability (GitHub still computing it) is treated as healthy — don't\n * churn on uncertainty. Reuses {@link isRenovatePR} for the branch-prefix check.\n */\nexport function hasHealthyRenovatePr(prs: PullRequestSummary[]): boolean {\n return prs.some((pr) => isRenovatePR(pr) && pr.mergeable !== \"CONFLICTING\");\n}\n\n/** A site selected for an on-demand Renovate run, with the reason (vuln counts). */\nexport type RenovateDispatchTarget = {\n repo: string; // owner/repo, from the Websites \"Git repo\" field\n siteName: string;\n critical: number;\n high: number;\n};\n\n/**\n * Pick the active, repo-backed sites whose latest security sweep found a\n * critical or high vulnerability. A `null` vuln count is treated as zero (a site\n * that was never swept, or had its counts cleared, is not dispatched). Inactive\n * sites and sites without a `Git repo` are excluded — there's nothing to kick.\n *\n * Counts are only guaranteed fresh for sites the sweep actually audited (active +\n * URL present); a site whose write-back failed/was skipped this run is selected on\n * its last-known counts. That's safe — a stale-high false dispatch is an idempotent\n * Renovate no-op, and the open-PR skip in `dispatchRenovateAcross` avoids re-firing\n * when remediation is already in flight.\n */\nexport function selectRenovateTargets(sites: WebsiteRow[]): RenovateDispatchTarget[] {\n const targets: RenovateDispatchTarget[] = [];\n for (const s of sites) {\n if (!isDashboardVisible(s)) continue; // active (maintenance / launch period) only\n const repo = s.gitRepo?.trim();\n if (!repo) continue; // repo-backed only — the dispatch target is its renovate.yml\n const critical = s.securityVulnsCritical ?? 0;\n const high = s.securityVulnsHigh ?? 0;\n if (critical + high <= 0) continue; // actionable (critical/high) vulns only\n targets.push({ repo, siteName: s.name, critical, high });\n }\n return targets;\n}\n\n/** Injected GitHub operations — real ones come from `makeGitHub`, fakes from tests. */\nexport type RenovateDispatchDeps = {\n /** True iff the repo has a HEALTHY (non-conflicting) open Renovate PR —\n * remediation in flight, so don't re-dispatch. A conflicting/stuck PR returns\n * false so the dispatch goes through and Renovate rebases it. */\n hasHealthyOpenRenovatePr: (repo: string) => Promise<boolean>;\n defaultBranch: (repo: string) => Promise<string>;\n dispatch: (repo: string, workflow: string, ref: string) => Promise<void>;\n};\n\nexport type RenovateDispatchResult = {\n dispatched: string[];\n /** Repos skipped because a Renovate PR is already open — no point re-firing. */\n skipped: string[];\n failed: { repo: string; error: string }[];\n};\n\n/**\n * Dispatch `renovate.yml` for each target on its default branch.\n *\n * Skips a repo with a HEALTHY (non-conflicting) open Renovate PR — remediation is\n * in flight, so re-dispatching would just churn CI (keeps a persistent vuln from\n * re-firing every nightly run while its fix PR waits, e.g. a human-gated major\n * bump). A repo whose open Renovate PR is CONFLICTING/stuck is NOT skipped:\n * dispatching triggers Renovate to rebase it. Best-effort and isolated: a repo\n * that lacks `renovate.yml`, or whose\n * dispatch/PR-check is refused (the token's `actions:write` scope), is recorded in\n * `failed` and the rest still run. Never throws — the caller (a follow-up step on\n * the security sweep) must not be able to fail the sweep over a dispatch hiccup.\n */\nexport async function dispatchRenovateAcross(\n targets: RenovateDispatchTarget[],\n deps: RenovateDispatchDeps,\n): Promise<RenovateDispatchResult> {\n const dispatched: string[] = [];\n const skipped: string[] = [];\n const failed: { repo: string; error: string }[] = [];\n for (const t of targets) {\n try {\n if (await deps.hasHealthyOpenRenovatePr(t.repo)) {\n skipped.push(t.repo);\n continue;\n }\n const ref = await deps.defaultBranch(t.repo);\n await deps.dispatch(t.repo, RENOVATE_WORKFLOW_FILE, ref);\n dispatched.push(t.repo);\n } catch (e) {\n failed.push({ repo: t.repo, error: e instanceof Error ? e.message : String(e) });\n }\n }\n return { dispatched, skipped, failed };\n}\n\n/** Machine-readable counts line the workflow greps to annotate partial failures. */\nexport function formatRenovateDispatchSummary(result: RenovateDispatchResult): string {\n return `RENOVATE_DISPATCH_SUMMARY dispatched=${result.dispatched.length} skipped=${result.skipped.length} failed=${result.failed.length}`;\n}\n\n/**\n * Plan the per-site auto-fix-attempt counter writes from a dispatch result. PURE.\n * For each site:\n * - vulns now 0 (or never counted) → reset to 0 (episode resolved)\n * - else dispatched this run → attempts + 1 (a fresh failed-so-far attempt)\n * - else (skipped / failed / untouched) → unchanged\n * The RESET applies fleet-wide — regardless of Status or Git repo — so a site\n * archived or un-repo'd after its episode can't carry a stale counter into its\n * next vuln (Alamo sat at 7 from a long-closed episode). A null vuln count\n * (never-audited) reads as zero, consistent with the existing null-as-zero\n * semantics, so a lingering counter on such a row also clears. The INCREMENT\n * keeps its active + repo-backed filters (only those sites are dispatched).\n * Returns only the rows whose value CHANGES (a steady fleet writes nothing —\n * a 0→0 site emits no write). A skipped repo (healthy Renovate PR in flight) is\n * NOT a failed attempt — a fix is genuinely moving toward merge — so its counter\n * holds. A dispatched site whose critical/high advisories are ALL proven\n * TRANSITIVE also holds: Renovate has no direct-dep bump to open, so the run is a\n * known green no-op waiting on the weekly lockfile window, not a failed fix\n * attempt (Sonder rode 5 such no-ops to a false \"auto-fix failed\", 2026-08-10).\n * Missing/unknown relationship data keeps the increment — never mute on absence.\n */\nexport function computeAutoFixAttemptUpdates(\n sites: WebsiteRow[],\n result: RenovateDispatchResult,\n): { id: string; attempts: number }[] {\n const dispatched = new Set(result.dispatched);\n const updates: { id: string; attempts: number }[] = [];\n for (const s of sites) {\n const current = s.securityAutoFixAttempts ?? 0;\n const vulns = (s.securityVulnsCritical ?? 0) + (s.securityVulnsHigh ?? 0);\n if (vulns === 0) {\n // Episode resolved (or never confirmed): reset REGARDLESS of status/repo —\n // a site archived or un-repo'd after its episode must not carry a stale\n // counter into its next vuln (Alamo sat at 7 from a long-closed episode).\n if (current !== 0) updates.push({ id: s.id, attempts: 0 });\n continue;\n }\n if (!isDashboardVisible(s)) continue; // increments stay active-only\n const repo = s.gitRepo?.trim();\n if (!repo) continue; // …and repo-backed-only\n if (!dispatched.has(repo)) continue;\n // Transitive-only episode: the dispatch can't produce a fix PR (no direct dep\n // to bump) — don't count the no-op as a failed attempt.\n if (allActionableVulnsTransitive(s.securityAdvisories)) continue;\n updates.push({ id: s.id, attempts: current + 1 });\n }\n return updates;\n}\n\n/**\n * Apply the planned counter updates with an injected writer, BEST-EFFORT: a writer\n * that throws (e.g. the Airtable field not yet created, or a transient error) is\n * counted in `failed` and never propagates — the security sweep must not fail over\n * counter bookkeeping. Returns the applied/failed tallies for the summary line.\n */\nexport async function applyAutoFixAttemptUpdates(\n updates: { id: string; attempts: number }[],\n write: (id: string, attempts: number) => Promise<void>,\n): Promise<{ written: number; failed: number }> {\n let written = 0;\n let failed = 0;\n for (const u of updates) {\n try {\n await write(u.id, u.attempts);\n written++;\n } catch {\n failed++;\n }\n }\n return { written, failed };\n}\n\n/** Machine-readable counts line the workflow can grep, mirroring RENOVATE_DISPATCH_SUMMARY. */\nexport function formatAutoFixAttemptsSummary(tally: { written: number; failed: number }): string {\n return `AUTO_FIX_ATTEMPTS_SUMMARY written=${tally.written} failed=${tally.failed}`;\n}\n","import { openBase, readAirtableConfig, type AirtableBase } from \"../../reports/airtable/client.js\";\nimport { listWebsites, updateAutoFixAttempts } from \"../../reports/airtable/websites.js\";\nimport { makeGitHub } from \"../../github/gh.js\";\nimport {\n selectRenovateTargets,\n dispatchRenovateAcross,\n formatRenovateDispatchSummary,\n hasHealthyRenovatePr,\n computeAutoFixAttemptUpdates,\n applyAutoFixAttemptUpdates,\n formatAutoFixAttemptsSummary,\n type RenovateDispatchResult,\n} from \"../../github/renovate-dispatch.js\";\n\n/**\n * `renovate-dispatch --fleet`: read the Websites table, pick the active,\n * repo-backed sites the latest security sweep flagged with critical/high vulns,\n * and fire each one's `renovate.yml` `workflow_dispatch` so Renovate's OSV\n * vulnerability alerts open the remediation PR now (instead of waiting for the\n * weekly schedule). Designed to run as a best-effort follow-up step on\n * `fleet-security.yml` AFTER the sweep has written fresh counts to Airtable.\n *\n * Always exits 0: a missing token is a clean skip; a partial dispatch failure\n * (a repo without `renovate.yml`, or a token lacking `actions:write`) is reported\n * in the summary for the workflow to annotate, never failing the security sweep.\n */\nexport async function runRenovateDispatchCommand(opts: {\n fleet?: boolean | undefined;\n /** Inject a pre-opened Airtable base (tests). Defaults to env config. */\n base?: AirtableBase;\n}): Promise<{ output: string; code: number }> {\n if (!opts.fleet) {\n return { output: \"renovate-dispatch currently supports only --fleet\", code: 2 };\n }\n const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();\n if (!token) {\n return {\n output: \"renovate-dispatch skipped: no RENOVATE_TOKEN/GH_TOKEN (fleet dispatch) configured.\",\n code: 0,\n };\n }\n\n const base = opts.base ?? openBase(readAirtableConfig());\n const websites = await listWebsites(base);\n const targets = selectRenovateTargets(websites);\n\n const lines: string[] = [];\n let result: RenovateDispatchResult = { dispatched: [], skipped: [], failed: [] };\n if (targets.length === 0) {\n lines.push(\n \"No active repo-backed sites with critical/high vulnerabilities — nothing to dispatch.\",\n );\n } else {\n const gh = makeGitHub({ token });\n result = await dispatchRenovateAcross(targets, {\n hasHealthyOpenRenovatePr: async (repo) =>\n hasHealthyRenovatePr(await gh.openPullRequests(repo)),\n defaultBranch: (repo) => gh.defaultBranch(repo),\n dispatch: (repo, workflow, ref) => gh.dispatchWorkflow(repo, workflow, ref),\n });\n\n const failedRepos = new Set(result.failed.map((f) => f.repo));\n const skippedRepos = new Set(result.skipped);\n for (const t of targets) {\n const status = failedRepos.has(t.repo)\n ? \"FAILED \"\n : skippedRepos.has(t.repo)\n ? \"skipped (open Renovate PR) \"\n : \"dispatched \";\n lines.push(`${status} ${t.repo} (critical=${t.critical} high=${t.high}) — ${t.siteName}`);\n }\n for (const f of result.failed) lines.push(` ↳ ${f.repo}: ${f.error}`);\n }\n\n // Counter bookkeeping runs on EVERY fleet run — the reset-on-clean branch was\n // previously unreachable behind the zero-targets early return, so a fully-clean\n // fleet never cleared stale counters (Alamo stuck at 7, 2026-07). Best-effort:\n // a failed write is tallied, never thrown. Uses the full `websites` list so\n // 0-vuln sites reset.\n const attemptUpdates = computeAutoFixAttemptUpdates(websites, result);\n const attemptTally = await applyAutoFixAttemptUpdates(attemptUpdates, (id, attempts) =>\n updateAutoFixAttempts(base, id, attempts),\n );\n\n lines.push(formatRenovateDispatchSummary(result));\n lines.push(formatAutoFixAttemptsSummary(attemptTally));\n\n return { output: lines.join(\"\\n\"), code: 0 };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/github/renovate-dispatch.ts","../src/cli/commands/renovate-dispatch.ts"],"sourcesContent":["import {\n allActionableVulnsTransitive,\n isDashboardVisible,\n type WebsiteRow,\n} from \"../reports/airtable/websites.js\";\nimport { isRenovatePR } from \"../alerts/renovate.js\";\nimport type { PullRequestSummary } from \"./gh.js\";\n\n/**\n * On-demand Renovate trigger for fleet sites.\n *\n * The nightly security sweep (`fleet-security.yml`) writes each active site's\n * vulnerability counts to Airtable. This module turns that into action: for the\n * sites the sweep just flagged with *actionable* (critical or high) vulns, it\n * dispatches that repo's `renovate.yml` `workflow_dispatch` so Renovate runs off\n * its twice-daily schedule. Renovate's OSV vulnerability alerts bypass the\n * schedule, so the remediation PR opens immediately — and because GitHub's\n * platform auto-merge is disabled fleet-wide, Renovate merges it from inside that\n * same run (or a later scheduled one) per the shared preset, which is what keeps\n * the preset's \"majors are never auto-merged\" rule authoritative. Moderate/low\n * vulns are left to the normal schedule — not urgent enough to dispatch + churn\n * CI nightly.\n */\n\n/** The per-repo workflow file the fleet uses for its self-hosted Renovate run. */\nexport const RENOVATE_WORKFLOW_FILE = \"renovate.yml\";\n\n/**\n * Does the repo have a HEALTHY open Renovate PR — one that is NOT conflicting?\n *\n * A healthy PR means remediation is genuinely in flight (moving toward merge), so\n * re-dispatching would just churn CI — skip it. A CONFLICTING Renovate PR is STUCK\n * (its branch fell behind the base after another PR merged the same lockfile), so\n * it does NOT count as healthy: re-dispatching triggers Renovate to rebase it.\n * `UNKNOWN` mergeability (GitHub still computing it) is treated as healthy — don't\n * churn on uncertainty. Reuses {@link isRenovatePR} for the branch-prefix check.\n */\nexport function hasHealthyRenovatePr(prs: PullRequestSummary[]): boolean {\n return prs.some((pr) => isRenovatePR(pr) && pr.mergeable !== \"CONFLICTING\");\n}\n\n/** A site selected for an on-demand Renovate run, with the reason (vuln counts). */\nexport type RenovateDispatchTarget = {\n repo: string; // owner/repo, from the Websites \"Git repo\" field\n siteName: string;\n critical: number;\n high: number;\n};\n\n/**\n * Pick the active, repo-backed sites whose latest security sweep found a\n * critical or high vulnerability. A `null` vuln count is treated as zero (a site\n * that was never swept, or had its counts cleared, is not dispatched). Inactive\n * sites and sites without a `Git repo` are excluded — there's nothing to kick.\n *\n * Counts are only guaranteed fresh for sites the sweep actually audited (active +\n * URL present); a site whose write-back failed/was skipped this run is selected on\n * its last-known counts. That's safe — a stale-high false dispatch is an idempotent\n * Renovate no-op, and the open-PR skip in `dispatchRenovateAcross` avoids re-firing\n * when remediation is already in flight.\n */\nexport function selectRenovateTargets(sites: WebsiteRow[]): RenovateDispatchTarget[] {\n const targets: RenovateDispatchTarget[] = [];\n for (const s of sites) {\n if (!isDashboardVisible(s)) continue; // active (maintenance / launch period) only\n const repo = s.gitRepo?.trim();\n if (!repo) continue; // repo-backed only — the dispatch target is its renovate.yml\n const critical = s.securityVulnsCritical ?? 0;\n const high = s.securityVulnsHigh ?? 0;\n if (critical + high <= 0) continue; // actionable (critical/high) vulns only\n targets.push({ repo, siteName: s.name, critical, high });\n }\n return targets;\n}\n\n/** Injected GitHub operations — real ones come from `makeGitHub`, fakes from tests. */\nexport type RenovateDispatchDeps = {\n /** True iff the repo has a HEALTHY (non-conflicting) open Renovate PR —\n * remediation in flight, so don't re-dispatch. A conflicting/stuck PR returns\n * false so the dispatch goes through and Renovate rebases it. */\n hasHealthyOpenRenovatePr: (repo: string) => Promise<boolean>;\n defaultBranch: (repo: string) => Promise<string>;\n dispatch: (repo: string, workflow: string, ref: string) => Promise<void>;\n};\n\nexport type RenovateDispatchResult = {\n dispatched: string[];\n /** Repos skipped because a Renovate PR is already open — no point re-firing. */\n skipped: string[];\n failed: { repo: string; error: string }[];\n};\n\n/**\n * Dispatch `renovate.yml` for each target on its default branch.\n *\n * Skips a repo with a HEALTHY (non-conflicting) open Renovate PR — remediation is\n * in flight, so re-dispatching would just churn CI (keeps a persistent vuln from\n * re-firing every nightly run while its fix PR waits, e.g. a human-gated major\n * bump). A repo whose open Renovate PR is CONFLICTING/stuck is NOT skipped:\n * dispatching triggers Renovate to rebase it. Best-effort and isolated: a repo\n * that lacks `renovate.yml`, or whose\n * dispatch/PR-check is refused (the token's `actions:write` scope), is recorded in\n * `failed` and the rest still run. Never throws — the caller (a follow-up step on\n * the security sweep) must not be able to fail the sweep over a dispatch hiccup.\n */\nexport async function dispatchRenovateAcross(\n targets: RenovateDispatchTarget[],\n deps: RenovateDispatchDeps,\n): Promise<RenovateDispatchResult> {\n const dispatched: string[] = [];\n const skipped: string[] = [];\n const failed: { repo: string; error: string }[] = [];\n for (const t of targets) {\n try {\n if (await deps.hasHealthyOpenRenovatePr(t.repo)) {\n skipped.push(t.repo);\n continue;\n }\n const ref = await deps.defaultBranch(t.repo);\n await deps.dispatch(t.repo, RENOVATE_WORKFLOW_FILE, ref);\n dispatched.push(t.repo);\n } catch (e) {\n failed.push({ repo: t.repo, error: e instanceof Error ? e.message : String(e) });\n }\n }\n return { dispatched, skipped, failed };\n}\n\n/** Machine-readable counts line the workflow greps to annotate partial failures. */\nexport function formatRenovateDispatchSummary(result: RenovateDispatchResult): string {\n return `RENOVATE_DISPATCH_SUMMARY dispatched=${result.dispatched.length} skipped=${result.skipped.length} failed=${result.failed.length}`;\n}\n\n/**\n * Plan the per-site auto-fix-attempt counter writes from a dispatch result. PURE.\n * For each site:\n * - vulns now 0 (or never counted) → reset to 0 (episode resolved)\n * - else dispatched this run → attempts + 1 (a fresh failed-so-far attempt)\n * - else (skipped / failed / untouched) → unchanged\n * The RESET applies fleet-wide — regardless of Status or Git repo — so a site\n * archived or un-repo'd after its episode can't carry a stale counter into its\n * next vuln (Alamo sat at 7 from a long-closed episode). A null vuln count\n * (never-audited) reads as zero, consistent with the existing null-as-zero\n * semantics, so a lingering counter on such a row also clears. The INCREMENT\n * keeps its active + repo-backed filters (only those sites are dispatched).\n * Returns only the rows whose value CHANGES (a steady fleet writes nothing —\n * a 0→0 site emits no write). A skipped repo (healthy Renovate PR in flight) is\n * NOT a failed attempt — a fix is genuinely moving toward merge — so its counter\n * holds. A dispatched site whose critical/high advisories are ALL proven\n * TRANSITIVE also holds: Renovate has no direct-dep bump to open, so the run is a\n * known green no-op waiting on the weekly lockfile window, not a failed fix\n * attempt (Sonder rode 5 such no-ops to a false \"auto-fix failed\", 2026-08-10).\n * Missing/unknown relationship data keeps the increment — never mute on absence.\n */\nexport function computeAutoFixAttemptUpdates(\n sites: WebsiteRow[],\n result: RenovateDispatchResult,\n): { id: string; attempts: number }[] {\n const dispatched = new Set(result.dispatched);\n const updates: { id: string; attempts: number }[] = [];\n for (const s of sites) {\n const current = s.securityAutoFixAttempts ?? 0;\n const vulns = (s.securityVulnsCritical ?? 0) + (s.securityVulnsHigh ?? 0);\n if (vulns === 0) {\n // Episode resolved (or never confirmed): reset REGARDLESS of status/repo —\n // a site archived or un-repo'd after its episode must not carry a stale\n // counter into its next vuln (Alamo sat at 7 from a long-closed episode).\n if (current !== 0) updates.push({ id: s.id, attempts: 0 });\n continue;\n }\n if (!isDashboardVisible(s)) continue; // increments stay active-only\n const repo = s.gitRepo?.trim();\n if (!repo) continue; // …and repo-backed-only\n if (!dispatched.has(repo)) continue;\n // Transitive-only episode: the dispatch can't produce a fix PR (no direct dep\n // to bump) — don't count the no-op as a failed attempt.\n if (allActionableVulnsTransitive(s.securityAdvisories)) continue;\n updates.push({ id: s.id, attempts: current + 1 });\n }\n return updates;\n}\n\n/**\n * Apply the planned counter updates with an injected writer, BEST-EFFORT: a writer\n * that throws (e.g. the Airtable field not yet created, or a transient error) is\n * counted in `failed` and never propagates — the security sweep must not fail over\n * counter bookkeeping. Returns the applied/failed tallies for the summary line.\n */\nexport async function applyAutoFixAttemptUpdates(\n updates: { id: string; attempts: number }[],\n write: (id: string, attempts: number) => Promise<void>,\n): Promise<{ written: number; failed: number }> {\n let written = 0;\n let failed = 0;\n for (const u of updates) {\n try {\n await write(u.id, u.attempts);\n written++;\n } catch {\n failed++;\n }\n }\n return { written, failed };\n}\n\n/** Machine-readable counts line the workflow can grep, mirroring RENOVATE_DISPATCH_SUMMARY. */\nexport function formatAutoFixAttemptsSummary(tally: { written: number; failed: number }): string {\n return `AUTO_FIX_ATTEMPTS_SUMMARY written=${tally.written} failed=${tally.failed}`;\n}\n","import { openBase, readAirtableConfig, type AirtableBase } from \"../../reports/airtable/client.js\";\nimport type { SiteMirror } from \"../../db/site-mirror.js\";\nimport { listWebsites, updateAutoFixAttempts } from \"../../reports/airtable/websites.js\";\nimport { makeGitHub } from \"../../github/gh.js\";\nimport {\n selectRenovateTargets,\n dispatchRenovateAcross,\n formatRenovateDispatchSummary,\n hasHealthyRenovatePr,\n computeAutoFixAttemptUpdates,\n applyAutoFixAttemptUpdates,\n formatAutoFixAttemptsSummary,\n type RenovateDispatchResult,\n} from \"../../github/renovate-dispatch.js\";\n\n/**\n * `renovate-dispatch --fleet`: read the Websites table, pick the active,\n * repo-backed sites the latest security sweep flagged with critical/high vulns,\n * and fire each one's `renovate.yml` `workflow_dispatch` so Renovate's OSV\n * vulnerability alerts open the remediation PR now (instead of waiting for the\n * weekly schedule). Designed to run as a best-effort follow-up step on\n * `fleet-security.yml` AFTER the sweep has written fresh counts to Airtable.\n *\n * Always exits 0: a missing token is a clean skip; a partial dispatch failure\n * (a repo without `renovate.yml`, or a token lacking `actions:write`) is reported\n * in the summary for the workflow to annotate, never failing the security sweep.\n */\nexport async function runRenovateDispatchCommand(opts: {\n fleet?: boolean | undefined;\n /** Inject a pre-opened Airtable base (tests). Defaults to env config. */\n base?: AirtableBase;\n /** #539 Phase 5: Turso write-through for the auto-fix counter. Injected from\n * bin.ts rather than defaulted here — this function is called directly by\n * tests, and a default would open a real libSQL handle inside the suite. */\n siteMirror?: SiteMirror;\n}): Promise<{ output: string; code: number }> {\n if (!opts.fleet) {\n return { output: \"renovate-dispatch currently supports only --fleet\", code: 2 };\n }\n const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();\n if (!token) {\n return {\n output: \"renovate-dispatch skipped: no RENOVATE_TOKEN/GH_TOKEN (fleet dispatch) configured.\",\n code: 0,\n };\n }\n\n const base = opts.base ?? openBase(readAirtableConfig());\n const websites = await listWebsites(base);\n const targets = selectRenovateTargets(websites);\n\n const lines: string[] = [];\n let result: RenovateDispatchResult = { dispatched: [], skipped: [], failed: [] };\n if (targets.length === 0) {\n lines.push(\n \"No active repo-backed sites with critical/high vulnerabilities — nothing to dispatch.\",\n );\n } else {\n const gh = makeGitHub({ token });\n result = await dispatchRenovateAcross(targets, {\n hasHealthyOpenRenovatePr: async (repo) =>\n hasHealthyRenovatePr(await gh.openPullRequests(repo)),\n defaultBranch: (repo) => gh.defaultBranch(repo),\n dispatch: (repo, workflow, ref) => gh.dispatchWorkflow(repo, workflow, ref),\n });\n\n const failedRepos = new Set(result.failed.map((f) => f.repo));\n const skippedRepos = new Set(result.skipped);\n for (const t of targets) {\n const status = failedRepos.has(t.repo)\n ? \"FAILED \"\n : skippedRepos.has(t.repo)\n ? \"skipped (open Renovate PR) \"\n : \"dispatched \";\n lines.push(`${status} ${t.repo} (critical=${t.critical} high=${t.high}) — ${t.siteName}`);\n }\n for (const f of result.failed) lines.push(` ↳ ${f.repo}: ${f.error}`);\n }\n\n // Counter bookkeeping runs on EVERY fleet run — the reset-on-clean branch was\n // previously unreachable behind the zero-targets early return, so a fully-clean\n // fleet never cleared stale counters (Alamo stuck at 7, 2026-07). Best-effort:\n // a failed write is tallied, never thrown. Uses the full `websites` list so\n // 0-vuln sites reset.\n const attemptUpdates = computeAutoFixAttemptUpdates(websites, result);\n const attemptTally = await applyAutoFixAttemptUpdates(attemptUpdates, async (id, attempts) => {\n const fields = await updateAutoFixAttempts(base, id, attempts);\n await opts.siteMirror?.health(id, fields);\n });\n\n lines.push(formatRenovateDispatchSummary(result));\n lines.push(formatAutoFixAttemptsSummary(attemptTally));\n\n return { output: lines.join(\"\\n\"), code: 0 };\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ingestSubmission
3
- } from "./chunk-BNM5POSJ.js";
4
- import "./chunk-SNSTJLQW.js";
3
+ } from "./chunk-JRHIFA5R.js";
4
+ import "./chunk-NO74DXDP.js";
5
5
  import "./chunk-2OP4JKIQ.js";
6
6
 
7
7
  // src/db/deadletter.ts
@@ -63,4 +63,4 @@ async function replayOne(db, deps, row) {
63
63
  export {
64
64
  replayDeadLetters
65
65
  };
66
- //# sourceMappingURL=replay-C2LXMJZP.js.map
66
+ //# sourceMappingURL=replay-4S6ENYCH.js.map