@reddoorla/maintenance 0.96.0 → 0.98.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/README.md +5 -6
  2. package/dist/{announce-45HVP4J4.js → announce-2FH2KBQQ.js} +39 -31
  3. package/dist/announce-2FH2KBQQ.js.map +1 -0
  4. package/dist/attachments-JFGFQRXQ.js +10 -0
  5. package/dist/{blux-RUFMNOUJ.js → blux-NXQLMS4W.js} +5 -5
  6. package/dist/{bump-deps-EL3TKMIL.js → bump-deps-54K4ECP3.js} +6 -6
  7. package/dist/{chunk-IMYL67JV.js → chunk-3RLDX3Q2.js} +5 -5
  8. package/dist/{chunk-EQQ4H6L3.js → chunk-3YALLSBF.js} +2 -2
  9. package/dist/{chunk-LAJ3FF54.js → chunk-5UI4FERC.js} +16 -5
  10. package/dist/chunk-5UI4FERC.js.map +1 -0
  11. package/dist/{chunk-CA6ADGOC.js → chunk-6PGXKN6F.js} +2 -2
  12. package/dist/{chunk-Y5BVR4HW.js → chunk-6THEZQLA.js} +3 -3
  13. package/dist/chunk-ASLDCWBF.js +41 -0
  14. package/dist/chunk-ASLDCWBF.js.map +1 -0
  15. package/dist/{chunk-DVJNYPFR.js → chunk-AYDQQBNG.js} +2 -2
  16. package/dist/{chunk-OTEVKRAH.js → chunk-B2BRFWJY.js} +7 -16
  17. package/dist/chunk-B2BRFWJY.js.map +1 -0
  18. package/dist/{chunk-GEUC5VGM.js → chunk-BINFZQ2T.js} +49 -4
  19. package/dist/chunk-BINFZQ2T.js.map +1 -0
  20. package/dist/{chunk-T76MA2FH.js → chunk-CP6MVZ5X.js} +27 -1
  21. package/dist/{chunk-T76MA2FH.js.map → chunk-CP6MVZ5X.js.map} +1 -1
  22. package/dist/{chunk-72FJZ6LO.js → chunk-DRNZVORX.js} +26 -22
  23. package/dist/chunk-DRNZVORX.js.map +1 -0
  24. package/dist/{chunk-XY7YV2YQ.js → chunk-DXZRWW2F.js} +65 -10
  25. package/dist/chunk-DXZRWW2F.js.map +1 -0
  26. package/dist/{chunk-SHGIDTYF.js → chunk-DYTYPKFN.js} +2 -2
  27. package/dist/chunk-DZZ5KRTB.js +23 -0
  28. package/dist/chunk-DZZ5KRTB.js.map +1 -0
  29. package/dist/{chunk-NVLREB5R.js → chunk-EN6CTBUN.js} +7 -7
  30. package/dist/{chunk-ZH5LOQET.js → chunk-FHFXJ3EM.js} +7 -5
  31. package/dist/chunk-FHFXJ3EM.js.map +1 -0
  32. package/dist/{chunk-3EA43S3R.js → chunk-GRF2RYEO.js} +12 -12
  33. package/dist/{chunk-RRE74VWI.js → chunk-HSMN35LU.js} +2 -2
  34. package/dist/chunk-ICSRWW5Q.js +148 -0
  35. package/dist/chunk-ICSRWW5Q.js.map +1 -0
  36. package/dist/{chunk-XG2A6GIG.js → chunk-IDBPII6G.js} +4 -4
  37. package/dist/{chunk-XG2A6GIG.js.map → chunk-IDBPII6G.js.map} +1 -1
  38. package/dist/{chunk-ECDJDHKT.js → chunk-JHJNNJLG.js} +2 -2
  39. package/dist/{chunk-QFSL2L2B.js → chunk-L5F3WYIY.js} +81 -31
  40. package/dist/chunk-L5F3WYIY.js.map +1 -0
  41. package/dist/{chunk-XED2DNAI.js → chunk-LAG6IP7Y.js} +7 -7
  42. package/dist/{chunk-UIITN37J.js → chunk-LF2TJN4X.js} +2 -2
  43. package/dist/chunk-LF2TJN4X.js.map +1 -0
  44. package/dist/{chunk-SQ4FSNYL.js → chunk-LUF52EIY.js} +2 -2
  45. package/dist/{chunk-MZZDMBDT.js → chunk-LZMAG73U.js} +19 -19
  46. package/dist/chunk-LZMAG73U.js.map +1 -0
  47. package/dist/{chunk-6INOBNRO.js → chunk-MNRWHSFS.js} +2 -2
  48. package/dist/{chunk-CGQZFQAY.js → chunk-MVKNU4QD.js} +2 -3
  49. package/dist/chunk-MVKNU4QD.js.map +1 -0
  50. package/dist/{chunk-62YWOFVN.js → chunk-ORTOMEYR.js} +3 -3
  51. package/dist/{chunk-62YWOFVN.js.map → chunk-ORTOMEYR.js.map} +1 -1
  52. package/dist/{chunk-44SDGPGQ.js → chunk-PZZ6HM5T.js} +3 -3
  53. package/dist/chunk-RQJFGK3A.js +51 -0
  54. package/dist/chunk-RQJFGK3A.js.map +1 -0
  55. package/dist/{chunk-H445EH33.js → chunk-S2PLX4XK.js} +10 -7
  56. package/dist/chunk-S2PLX4XK.js.map +1 -0
  57. package/dist/{chunk-KN52PM63.js → chunk-TC4GANMX.js} +47 -149
  58. package/dist/chunk-TC4GANMX.js.map +1 -0
  59. package/dist/{chunk-F3JWOPTG.js → chunk-TEMQPSCM.js} +45 -105
  60. package/dist/chunk-TEMQPSCM.js.map +1 -0
  61. package/dist/{chunk-4UTJ4G6P.js → chunk-TJLKWQOX.js} +2 -2
  62. package/dist/{chunk-A5QKNOXW.js → chunk-TTT6HYPW.js} +47 -10
  63. package/dist/chunk-TTT6HYPW.js.map +1 -0
  64. package/dist/{chunk-VEIIUCYL.js → chunk-TV76A6GN.js} +2 -2
  65. package/dist/{chunk-D3CTOCUR.js → chunk-TVZ6QCOE.js} +3 -3
  66. package/dist/chunk-UPE6D4AT.js +69 -0
  67. package/dist/chunk-UPE6D4AT.js.map +1 -0
  68. package/dist/{chunk-BIYQN7FH.js → chunk-VCPTOZ56.js} +31 -1
  69. package/dist/{chunk-BIYQN7FH.js.map → chunk-VCPTOZ56.js.map} +1 -1
  70. package/dist/chunk-VQMAAQ5M.js +53 -0
  71. package/dist/chunk-VQMAAQ5M.js.map +1 -0
  72. package/dist/{chunk-EASYCD6F.js → chunk-WMF5S2MB.js} +1 -1
  73. package/dist/{chunk-EASYCD6F.js.map → chunk-WMF5S2MB.js.map} +1 -1
  74. package/dist/{chunk-37B4LVB3.js → chunk-X56LAKPR.js} +3 -3
  75. package/dist/chunk-X56LAKPR.js.map +1 -0
  76. package/dist/{chunk-IY6256GK.js → chunk-YJBSB4LP.js} +6 -1
  77. package/dist/chunk-YJBSB4LP.js.map +1 -0
  78. package/dist/{chunk-P4A6D6HO.js → chunk-ZEE5WQYU.js} +32 -18
  79. package/dist/chunk-ZEE5WQYU.js.map +1 -0
  80. package/dist/cli/bin.js +92 -50
  81. package/dist/cli/bin.js.map +1 -1
  82. package/dist/cli/commands/audit.d.ts +33 -3
  83. package/dist/cli/commands/audit.js +26 -23
  84. package/dist/cli/commands/audit.js.map +1 -1
  85. package/dist/client-3XGSZMLU.js +11 -0
  86. package/dist/{convert-to-pnpm-4DON7IFS.js → convert-to-pnpm-U2CRNKNY.js} +6 -6
  87. package/dist/{db-5K4CDEGJ.js → db-7XYTBIPH.js} +25 -18
  88. package/dist/db-7XYTBIPH.js.map +1 -0
  89. package/dist/{deadletter-G4M3HSVM.js → deadletter-A7RQVNG3.js} +8 -4
  90. package/dist/{digest-5AJGWS33.js → digest-AMXRGWZA.js} +34 -33
  91. package/dist/digest-AMXRGWZA.js.map +1 -0
  92. package/dist/{digest-collectors-MC5NWEUA.js → digest-collectors-CX2NCGS7.js} +15 -12
  93. package/dist/ensure-site-F2WFCWWA.js +70 -0
  94. package/dist/ensure-site-F2WFCWWA.js.map +1 -0
  95. package/dist/ensure-site-JEKV2ODE.js +105 -0
  96. package/dist/ensure-site-JEKV2ODE.js.map +1 -0
  97. package/dist/{ensure-site-XESZHZEB.js → ensure-site-OO4UPWNQ.js} +19 -42
  98. package/dist/ensure-site-OO4UPWNQ.js.map +1 -0
  99. package/dist/fleet-events-CkOZpMVC.d.ts +435 -0
  100. package/dist/{fleet-state-GJWGKTLD.js → fleet-state-RJROYXKO.js} +17 -7
  101. package/dist/{forms-notify-target-4NPLX2UG.js → forms-notify-target-DK5R326N.js} +17 -13
  102. package/dist/forms-notify-target-DK5R326N.js.map +1 -0
  103. package/dist/{github-signals-DMU74SAX.js → github-signals-RGRPYTUB.js} +20 -15
  104. package/dist/github-signals-RGRPYTUB.js.map +1 -0
  105. package/dist/{header-image-BRTOLHJF.js → header-image-43ZIEJKP.js} +13 -17
  106. package/dist/header-image-43ZIEJKP.js.map +1 -0
  107. package/dist/{health-endpoint-UCLMSSCN.js → health-endpoint-K5LXDPGE.js} +7 -7
  108. package/dist/{health-mirror-ONRT4DQ5.js → health-mirror-WZUHWCCV.js} +10 -10
  109. package/dist/{import-airtable-IIK2QUUF.js → import-airtable-UBF5GYAL.js} +3 -3
  110. package/dist/index.d.ts +102 -494
  111. package/dist/index.js +62 -52
  112. package/dist/index.js.map +1 -1
  113. package/dist/{init-3LMZTCVD.js → init-OFITGMOP.js} +16 -16
  114. package/dist/{launch-XTDMC6LD.js → launch-AJY3TGTQ.js} +37 -35
  115. package/dist/launch-AJY3TGTQ.js.map +1 -0
  116. package/dist/{lighthouse-52WSPZ5D.js → lighthouse-RFMFREEX.js} +2 -2
  117. package/dist/lighthouse-airtable-M45OPVRV.js +14 -0
  118. package/dist/{chunk-IHHAYXKY.js → match-harness-CFELJA57.js} +70 -4
  119. package/dist/match-harness-CFELJA57.js.map +1 -0
  120. package/dist/migrate-EZL55AWA.js +7 -0
  121. package/dist/notify-K3WVH3DM.js +1 -1
  122. package/dist/{onboard-CZYBWVWH.js → onboard-F7RTLMDZ.js} +6 -6
  123. package/dist/{orchestrate-TGDFGY52.js → orchestrate-SAIFXZIW.js} +13 -10
  124. package/dist/parity-2JGVUCZD.js +14 -0
  125. package/dist/{pipeline-UUPXOS2J.js → pipeline-TCSNVDQZ.js} +3 -3
  126. package/dist/{preflight-POZZI3GN.js → preflight-6XGVVO2U.js} +35 -11
  127. package/dist/preflight-6XGVVO2U.js.map +1 -0
  128. package/dist/{prismic-ci-AHY5ULJR.js → prismic-ci-XMX7A5E4.js} +11 -11
  129. package/dist/prismic-ci-XMX7A5E4.js.map +1 -0
  130. package/dist/{prismic-models-URYGODNY.js → prismic-models-7GKFAAFA.js} +19 -16
  131. package/dist/prismic-models-7GKFAAFA.js.map +1 -0
  132. package/dist/prospect/types.d.ts +44 -2
  133. package/dist/prospect/types.js +1 -1
  134. package/dist/{prospect-audit-AJEE3Z4L.js → prospect-audit-KWP5P5U2.js} +37 -5
  135. package/dist/prospect-audit-KWP5P5U2.js.map +1 -0
  136. package/dist/{protection-audit-3J3UEOVT.js → protection-audit-YXJKFU6O.js} +3 -3
  137. package/dist/{protection-audit-3J3UEOVT.js.map → protection-audit-YXJKFU6O.js.map} +1 -1
  138. package/dist/recipes/sync-configs.js +3 -3
  139. package/dist/{render-MHUUHPLN.js → render-NWUAX6RN.js} +3 -3
  140. package/dist/{render-from-row-65DC6ZP3.js → render-from-row-75JTED2Z.js} +6 -4
  141. package/dist/{renovate-dispatch-ONJMBUHJ.js → renovate-dispatch-ZZQXT7DA.js} +13 -8
  142. package/dist/{renovate-dispatch-ONJMBUHJ.js.map → renovate-dispatch-ZZQXT7DA.js.map} +1 -1
  143. package/dist/replay-HB44TAII.js +77 -0
  144. package/dist/replay-HB44TAII.js.map +1 -0
  145. package/dist/{report-RC5LGPU3.js → report-MVPWS7WP.js} +102 -58
  146. package/dist/report-MVPWS7WP.js.map +1 -0
  147. package/dist/{report-mirror-KKZZ6RMZ.js → report-mirror-O6IDFDWU.js} +28 -10
  148. package/dist/report-mirror-O6IDFDWU.js.map +1 -0
  149. package/dist/roster-EJ3ILR52.js +19 -0
  150. package/dist/roster-EJ3ILR52.js.map +1 -0
  151. package/dist/{screenouts-7P5GOZMV.js → screenouts-7NQUWHOJ.js} +14 -1
  152. package/dist/screenouts-7NQUWHOJ.js.map +1 -0
  153. package/dist/{self-updating-RKKIYNV4.js → self-updating-KX5FMGEO.js} +7 -7
  154. package/dist/{selftest-AFLR7YV7.js → selftest-KABY2CYJ.js} +21 -17
  155. package/dist/{selftest-AFLR7YV7.js.map → selftest-KABY2CYJ.js.map} +1 -1
  156. package/dist/site-create-ZU3ACBTO.js +45 -0
  157. package/dist/site-create-ZU3ACBTO.js.map +1 -0
  158. package/dist/{site-mirror-H2FYMN55.js → site-mirror-TKTDWYP3.js} +10 -10
  159. package/dist/site-mirror-TKTDWYP3.js.map +1 -0
  160. package/dist/{smoke-suite-BWHR3XX5.js → smoke-suite-QQMV7TNA.js} +7 -7
  161. package/dist/{submissions-JDZP35JM.js → submissions-4I66W6QK.js} +11 -1
  162. package/dist/{submissions-JDZP35JM.js.map → submissions-4I66W6QK.js.map} +1 -1
  163. package/dist/{submissions-II5J35QM.js → submissions-K5UXGZUN.js} +12 -7
  164. package/dist/submissions-K5UXGZUN.js.map +1 -0
  165. package/dist/{svelte-codemods-QWGIRNN6.js → svelte-codemods-YFN4TEGV.js} +6 -6
  166. package/dist/{sync-NKPDCMYW.js → sync-F4USL4K5.js} +6 -6
  167. package/dist/{sync-configs-HGLQV55H.js → sync-configs-HBXS3D7C.js} +6 -6
  168. package/dist/turso-KQKBBQCK.js +24 -0
  169. package/dist/turso-KQKBBQCK.js.map +1 -0
  170. package/dist/{upgrade-WZ44IARG.js → upgrade-XOJMDIT4.js} +6 -6
  171. package/dist/util/git.d.ts +1 -1
  172. package/dist/util/git.js +1 -1
  173. package/dist/{websites-6DHIHJ2D.js → websites-Y6FN2XFD.js} +26 -21
  174. package/dist/{write-audits-to-airtable-BIK6LNVC.js → write-audits-to-airtable-TI3JMWSY.js} +6 -4
  175. package/package.json +1 -1
  176. package/dist/airtable-SJV4ZSMS.js +0 -9
  177. package/dist/announce-45HVP4J4.js.map +0 -1
  178. package/dist/attachments-NLZIJ5NU.js +0 -9
  179. package/dist/chunk-37B4LVB3.js.map +0 -1
  180. package/dist/chunk-72FJZ6LO.js.map +0 -1
  181. package/dist/chunk-74QRWE6G.js +0 -53
  182. package/dist/chunk-74QRWE6G.js.map +0 -1
  183. package/dist/chunk-A5QKNOXW.js.map +0 -1
  184. package/dist/chunk-CGQZFQAY.js.map +0 -1
  185. package/dist/chunk-F3JWOPTG.js.map +0 -1
  186. package/dist/chunk-GEUC5VGM.js.map +0 -1
  187. package/dist/chunk-H445EH33.js.map +0 -1
  188. package/dist/chunk-IHHAYXKY.js.map +0 -1
  189. package/dist/chunk-IY6256GK.js.map +0 -1
  190. package/dist/chunk-KN52PM63.js.map +0 -1
  191. package/dist/chunk-LAJ3FF54.js.map +0 -1
  192. package/dist/chunk-MZZDMBDT.js.map +0 -1
  193. package/dist/chunk-OTEVKRAH.js.map +0 -1
  194. package/dist/chunk-P4A6D6HO.js.map +0 -1
  195. package/dist/chunk-QFSL2L2B.js.map +0 -1
  196. package/dist/chunk-UIITN37J.js.map +0 -1
  197. package/dist/chunk-XY7YV2YQ.js.map +0 -1
  198. package/dist/chunk-YFAXOD7Z.js +0 -42
  199. package/dist/chunk-YFAXOD7Z.js.map +0 -1
  200. package/dist/chunk-ZH5LOQET.js.map +0 -1
  201. package/dist/client-L7NAUG6J.js +0 -11
  202. package/dist/db-5K4CDEGJ.js.map +0 -1
  203. package/dist/digest-5AJGWS33.js.map +0 -1
  204. package/dist/ensure-site-XESZHZEB.js.map +0 -1
  205. package/dist/fleet-events-cF2Om88w.d.ts +0 -24
  206. package/dist/forms-notify-target-4NPLX2UG.js.map +0 -1
  207. package/dist/github-signals-DMU74SAX.js.map +0 -1
  208. package/dist/header-image-BRTOLHJF.js.map +0 -1
  209. package/dist/launch-XTDMC6LD.js.map +0 -1
  210. package/dist/lighthouse-airtable-3NOFB42B.js +0 -12
  211. package/dist/match-harness-OW45CB5H.js +0 -77
  212. package/dist/match-harness-OW45CB5H.js.map +0 -1
  213. package/dist/migrate-NE3DUMTM.js +0 -7
  214. package/dist/parity-AEKYSLN6.js +0 -14
  215. package/dist/preflight-POZZI3GN.js.map +0 -1
  216. package/dist/prismic-ci-AHY5ULJR.js.map +0 -1
  217. package/dist/prismic-models-URYGODNY.js.map +0 -1
  218. package/dist/prospect-audit-AJEE3Z4L.js.map +0 -1
  219. package/dist/replay-4FVFOMHH.js +0 -54
  220. package/dist/replay-4FVFOMHH.js.map +0 -1
  221. package/dist/report-RC5LGPU3.js.map +0 -1
  222. package/dist/report-mirror-KKZZ6RMZ.js.map +0 -1
  223. package/dist/screenouts-7P5GOZMV.js.map +0 -1
  224. package/dist/site-mirror-H2FYMN55.js.map +0 -1
  225. package/dist/submissions-II5J35QM.js.map +0 -1
  226. package/dist/write-audits-to-airtable-BIK6LNVC.js.map +0 -1
  227. /package/dist/{airtable-SJV4ZSMS.js.map → attachments-JFGFQRXQ.js.map} +0 -0
  228. /package/dist/{blux-RUFMNOUJ.js.map → blux-NXQLMS4W.js.map} +0 -0
  229. /package/dist/{bump-deps-EL3TKMIL.js.map → bump-deps-54K4ECP3.js.map} +0 -0
  230. /package/dist/{chunk-IMYL67JV.js.map → chunk-3RLDX3Q2.js.map} +0 -0
  231. /package/dist/{chunk-EQQ4H6L3.js.map → chunk-3YALLSBF.js.map} +0 -0
  232. /package/dist/{chunk-CA6ADGOC.js.map → chunk-6PGXKN6F.js.map} +0 -0
  233. /package/dist/{chunk-Y5BVR4HW.js.map → chunk-6THEZQLA.js.map} +0 -0
  234. /package/dist/{chunk-DVJNYPFR.js.map → chunk-AYDQQBNG.js.map} +0 -0
  235. /package/dist/{chunk-SHGIDTYF.js.map → chunk-DYTYPKFN.js.map} +0 -0
  236. /package/dist/{chunk-NVLREB5R.js.map → chunk-EN6CTBUN.js.map} +0 -0
  237. /package/dist/{chunk-3EA43S3R.js.map → chunk-GRF2RYEO.js.map} +0 -0
  238. /package/dist/{chunk-RRE74VWI.js.map → chunk-HSMN35LU.js.map} +0 -0
  239. /package/dist/{chunk-ECDJDHKT.js.map → chunk-JHJNNJLG.js.map} +0 -0
  240. /package/dist/{chunk-XED2DNAI.js.map → chunk-LAG6IP7Y.js.map} +0 -0
  241. /package/dist/{chunk-SQ4FSNYL.js.map → chunk-LUF52EIY.js.map} +0 -0
  242. /package/dist/{chunk-6INOBNRO.js.map → chunk-MNRWHSFS.js.map} +0 -0
  243. /package/dist/{chunk-44SDGPGQ.js.map → chunk-PZZ6HM5T.js.map} +0 -0
  244. /package/dist/{chunk-4UTJ4G6P.js.map → chunk-TJLKWQOX.js.map} +0 -0
  245. /package/dist/{chunk-VEIIUCYL.js.map → chunk-TV76A6GN.js.map} +0 -0
  246. /package/dist/{chunk-D3CTOCUR.js.map → chunk-TVZ6QCOE.js.map} +0 -0
  247. /package/dist/{attachments-NLZIJ5NU.js.map → client-3XGSZMLU.js.map} +0 -0
  248. /package/dist/{convert-to-pnpm-4DON7IFS.js.map → convert-to-pnpm-U2CRNKNY.js.map} +0 -0
  249. /package/dist/{client-L7NAUG6J.js.map → deadletter-A7RQVNG3.js.map} +0 -0
  250. /package/dist/{deadletter-G4M3HSVM.js.map → digest-collectors-CX2NCGS7.js.map} +0 -0
  251. /package/dist/{digest-collectors-MC5NWEUA.js.map → fleet-state-RJROYXKO.js.map} +0 -0
  252. /package/dist/{health-endpoint-UCLMSSCN.js.map → health-endpoint-K5LXDPGE.js.map} +0 -0
  253. /package/dist/{health-mirror-ONRT4DQ5.js.map → health-mirror-WZUHWCCV.js.map} +0 -0
  254. /package/dist/{fleet-state-GJWGKTLD.js.map → import-airtable-UBF5GYAL.js.map} +0 -0
  255. /package/dist/{init-3LMZTCVD.js.map → init-OFITGMOP.js.map} +0 -0
  256. /package/dist/{import-airtable-IIK2QUUF.js.map → lighthouse-RFMFREEX.js.map} +0 -0
  257. /package/dist/{lighthouse-52WSPZ5D.js.map → lighthouse-airtable-M45OPVRV.js.map} +0 -0
  258. /package/dist/{lighthouse-airtable-3NOFB42B.js.map → migrate-EZL55AWA.js.map} +0 -0
  259. /package/dist/{onboard-CZYBWVWH.js.map → onboard-F7RTLMDZ.js.map} +0 -0
  260. /package/dist/{migrate-NE3DUMTM.js.map → orchestrate-SAIFXZIW.js.map} +0 -0
  261. /package/dist/{orchestrate-TGDFGY52.js.map → parity-2JGVUCZD.js.map} +0 -0
  262. /package/dist/{parity-AEKYSLN6.js.map → pipeline-TCSNVDQZ.js.map} +0 -0
  263. /package/dist/{render-MHUUHPLN.js.map → render-NWUAX6RN.js.map} +0 -0
  264. /package/dist/{pipeline-UUPXOS2J.js.map → render-from-row-75JTED2Z.js.map} +0 -0
  265. /package/dist/{self-updating-RKKIYNV4.js.map → self-updating-KX5FMGEO.js.map} +0 -0
  266. /package/dist/{smoke-suite-BWHR3XX5.js.map → smoke-suite-QQMV7TNA.js.map} +0 -0
  267. /package/dist/{svelte-codemods-QWGIRNN6.js.map → svelte-codemods-YFN4TEGV.js.map} +0 -0
  268. /package/dist/{sync-NKPDCMYW.js.map → sync-F4USL4K5.js.map} +0 -0
  269. /package/dist/{sync-configs-HGLQV55H.js.map → sync-configs-HBXS3D7C.js.map} +0 -0
  270. /package/dist/{upgrade-WZ44IARG.js.map → upgrade-XOJMDIT4.js.map} +0 -0
  271. /package/dist/{render-from-row-65DC6ZP3.js.map → websites-Y6FN2XFD.js.map} +0 -0
  272. /package/dist/{websites-6DHIHJ2D.js.map → write-audits-to-airtable-TI3JMWSY.js.map} +0 -0
@@ -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\";\nimport type { SiteGoal } from \"../../prospect/goals.js\";\nimport {\n PROSPECT_AUDIT_DAILY_CAP,\n DAILY_CAP_LOOKBACK,\n countAuditsInDailyWindow,\n isOverDailyCap,\n dailyCapMessage,\n type DatedAudit,\n} from \"../../prospect/daily-cap.js\";\n\n/** The operator-selectable goals, in the order the dispatch dropdown lists\n * them. `unknown` is deliberately absent: it is a finding the audit can reach\n * on its own, never something an operator would choose. */\nexport const GOALS = [\n \"book\",\n \"enquire\",\n \"call\",\n \"visit\",\n \"buy\",\n \"demo\",\n \"partner\",\n] as const satisfies readonly SiteGoal[];\n\nfunction isSiteGoal(value: string): value is SiteGoal {\n return (GOALS as readonly string[]).includes(value);\n}\n\nexport type ProspectAuditCliOptions = {\n business?: string;\n /**\n * The one action the site should produce from a visitor, chosen by the\n * operator instead of inferred from the site.\n *\n * Worth overriding because the operator knows things the site does not say.\n * A practice that wants online bookings but has never built one reads as\n * \"call\" from its own pages, and grading it as \"call\" would score it well for\n * doing the thing they are trying to stop doing.\n */\n goal?: 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 /**\n * #676. Search terms and buyer questions chosen by hand, comma-separated.\n *\n * Blank generates them, exactly as before — this is an override for a client\n * we already know, and for a comparison over time, where the terms have to\n * stay fixed between runs.\n */\n terms?: string;\n questions?: string;\n /** Test seam: injected pipeline deps. Never set from the CLI. */\n deps?: PipelineDeps;\n /**\n * Test seam: newest-first recent audits for the daily cap. Never set from the\n * CLI — the real implementation opens Turso below. Same injection shape\n * `ProspectAuditTriggerDeps.listRecent` uses on the dashboard side, so both\n * paths' cap tests drive the brake the same way and neither needs a database.\n */\n listRecent?: (limit: number) => Promise<DatedAudit[]>;\n /** Test seam: injectable clock for the cap's 24h window. */\n now?: () => Date;\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 // No AI Visibility line: the score was removed from every presentation\n // surface (web, print, email, here) — the measurement lives on as the\n // report's receipts section, not as a number that reads as ours to move.\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/** What the runaway brake concluded before any money was spent. */\ntype DailyCapCheck =\n | { kind: \"under\"; count: number }\n | { kind: \"over\"; count: number }\n /** The brake could not be applied at all — carries the sentence the operator\n * must see, because a guard that quietly abstains is worse than none. */\n | { kind: \"unchecked\"; warning: string };\n\nconst NO_DB_CAP_WARNING =\n \"No TURSO_DATABASE_URL, so prior audits cannot be counted — the 24h runaway brake \" +\n `(cap ${PROSPECT_AUDIT_DAILY_CAP}) is NOT protecting this run.`;\n\n/**\n * MED-15 (2026-09-02): the daily cap was enforced only in the dashboard\n * dispatch path, and every batch to date — including the 29-site corpus — ran\n * through this CLI, which had none. #618 fixed exactly this shape for the\n * private-host guard twenty lines above, and its comment describes the guard it\n * did NOT fix: \"one layer at the far end of the chain — so anyone running the\n * CLI directly, or any future second caller, had none.\"\n *\n * Counted, not re-implemented: the window, the threshold and the wording all\n * come from `src/prospect/daily-cap.ts`, which the dashboard path uses too.\n *\n * **When there is no database, this WARNS and lets the run proceed.** A run with\n * no persistence cannot count prior audits, and refusing would break the\n * documented `--out`-only local path — which is also, by construction, a\n * one-off: an unpersisted run is invisible to every later count, so it is not\n * the automated path the brake exists for. What it must never do is pass in\n * silence, so the sentence goes to stderr AND into the run's warnings, where\n * `--json` consumers see it too. Same answer if the count itself throws: a\n * Turso blip must not block a legitimate audit, but must not look like a\n * passed check either.\n */\nasync function checkDailyCap(\n canPersist: boolean,\n opts: ProspectAuditCliOptions,\n): Promise<DailyCapCheck> {\n const now = (opts.now ?? (() => new Date()))();\n const listRecent =\n opts.listRecent ??\n (canPersist\n ? async (limit: number): Promise<DatedAudit[]> => {\n const { openDb, readDbConfig } = await import(\"../../db/client.js\");\n const { listRecentProspectAudits } = await import(\"../../db/prospect-audits.js\");\n return listRecentProspectAudits(await openDb(readDbConfig()), limit);\n }\n : null);\n if (listRecent === null) return { kind: \"unchecked\", warning: NO_DB_CAP_WARNING };\n\n try {\n const count = countAuditsInDailyWindow(await listRecent(DAILY_CAP_LOOKBACK), now);\n return isOverDailyCap(count) ? { kind: \"over\", count } : { kind: \"under\", count };\n } catch (err) {\n return {\n kind: \"unchecked\",\n warning:\n `Could not read recent audits, so the 24h runaway brake (cap ${PROSPECT_AUDIT_DAILY_CAP}) ` +\n `is NOT protecting this run: ${errorMessage(err)}`,\n };\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 // Collected from here on, so the brake's verdict can be recorded BEFORE the\n // pipeline runs — a warning printed only in the summary arrives after the\n // money is spent.\n const warnings: string[] = [];\n\n // MED-15. The last refusal before anything is spent (see checkDailyCap).\n const cap = await checkDailyCap(canPersist, opts);\n if (cap.kind === \"over\") return fail(dailyCapMessage(cap.count));\n if (cap.kind === \"unchecked\") {\n warnings.push(cap.warning);\n console.error(`! ${cap.warning}`);\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 goal = opts.goal?.trim();\n // #676. Comma-separated on a flag (a newline is not typeable in one); the\n // cockpit's textareas split on newlines instead. Both drop blanks, and both\n // treat \"nothing usable\" as \"generate\".\n const splitList = (v: string | undefined): string[] =>\n (v ?? \"\")\n .split(\",\")\n .map((x) => x.trim())\n .filter((x) => x !== \"\");\n const chosenTerms = splitList(opts.terms);\n const chosenQuestions = splitList(opts.questions);\n if (goal !== undefined && goal !== \"\" && !isSiteGoal(goal)) {\n return fail(`--goal must be one of: ${GOALS.join(\", \")}`);\n }\n\n const result = await runProspectAudit(\n url,\n {\n ...(business ? { business } : {}),\n // An empty string is what a blank dispatch field sends; it must read as\n // \"not supplied\" so the model's own inference still runs, rather than\n // overriding it with nothing.\n ...(goal ? { goal } : {}),\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 ...(chosenTerms.length > 0 ? { terms: chosenTerms } : {}),\n ...(chosenQuestions.length > 0 ? { questions: chosenQuestions } : {}),\n },\n { ...(opts.deps ?? {}), onStage },\n );\n\n const { renderProspectReport } = await import(\"../../prospect/render.js\");\n const html = renderProspectReport(result);\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 // #676: on the ROW, not only in result_json — a re-run reuses them and\n // the /audits listing marks them without reading the blob.\n chosenTerms: chosenTerms.length > 0 ? chosenTerms : null,\n chosenQuestions: chosenQuestions.length > 0 ? chosenQuestions : null,\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":"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","names":[]}
@@ -367,10 +367,10 @@ async function runProtectionAuditCommand(opts, depsOverride) {
367
367
  if (!org) {
368
368
  return { output: "protection-audit requires --org <org>", code: 2 };
369
369
  }
370
- const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();
370
+ const token = process.env.GH_TOKEN?.trim();
371
371
  if (!depsOverride && !token) {
372
372
  return {
373
- output: "protection-audit skipped: no RENOVATE_TOKEN/GH_TOKEN (fleet read) configured.",
373
+ output: "protection-audit skipped: no GH_TOKEN (fleet read) configured.",
374
374
  code: 0
375
375
  };
376
376
  }
@@ -410,4 +410,4 @@ export {
410
410
  protectionAuditExitCode,
411
411
  runProtectionAuditCommand
412
412
  };
413
- //# sourceMappingURL=protection-audit-3J3UEOVT.js.map
413
+ //# sourceMappingURL=protection-audit-YXJKFU6O.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/audits/protection-coverage.ts","../src/github/package-manager-pin.ts","../src/cli/commands/protection-audit.ts"],"sourcesContent":["import { rulesetGaps, type ExistingRuleset } from \"../github/rulesets.js\";\nimport type {\n BranchTip,\n DependencyDashboard,\n RenovateMergeWindow,\n WorkflowHealth,\n} from \"../github/gh.js\";\n\n/**\n * Org-wide protection-coverage sweep (spec:\n * docs/superpowers/specs/2026-08-02-ruleset-self-healing-design.md).\n *\n * The `self-updating` recipe HEALS fleet sites, but it runs only when someone\n * runs it, and only over Airtable-listed sites — which is exactly how the\n * `.github` repo (no Airtable row) sat with zero protection until 2026-08-01.\n * This sweep is the layer that would have caught that on its own: it\n * enumerates the org FROM THE GITHUB API — never a hand-typed or\n * Airtable-scoped list, both of which have already produced false \"all\n * clear\"s — and judges every public repo's protection by SHAPE, not by name,\n * so a differently-named but sound ruleset (reddoor-maintenance's \"Main\n * Protection\") counts.\n *\n * Since 2026-08-02 the same sweep also verifies three more posture surfaces\n * per public repo, because each regresses silently and none has any other\n * watcher:\n * - secret scanning + push protection stay ENABLED (a repo transferred in,\n * or an org-default flip, arrives with them off), and — since 2026-09-14 —\n * that what the detector FOUND has been read: a repo with open\n * secret-scanning alerts is a gap however green its settings are, because\n * an enabled scanner nobody reads produced five untriaged alerts, the\n * oldest 99 days old, on repos this sweep was calling covered;\n * - the renovate workflow is registered, active, and has actually RUN\n * recently. GitHub auto-disables schedules after 60 quiet days, and\n * schedule triggers in template-cloned files may never register at all —\n * either way a fully-quiet repo stops receiving updates with zero signal\n * (the 2026-08-02 sweep found two never-run and two 5-days-stale repos on\n * its first pass);\n * - renovate is not only alive but ALLOWED TO ACT — a workflow can run green\n * forever while Renovate has quietly stopped managing a branch, which froze\n * updates on nine repos for a week in August 2026 (see\n * renovateBlockedGaps). Liveness cannot see this; only the outcome can.\n */\nexport type ProtectionCoverageRow = {\n repo: string; // owner/repo\n status: \"covered\" | \"gap\" | \"skipped\";\n /** Human-readable: covering ruleset + whether it gates on CI, the specific\n * gaps, or why the repo was skipped. */\n detail: string;\n /** The one OUTCOME measurement on this sweep: did a feature update actually\n * LAND here (see renovateOutcome). Deliberately not folded into `status` —\n * it warns, it never gaps, so it can never file or hold open the tracking\n * issue. Absent on skipped repos, which are not measured at all. */\n renovateOutcome?: RenovateOutcome;\n};\n\n/** The reads the sweep needs — a subset of the GitHub factory, injected so the\n * sweep is pure + testable (same pattern as collectGitHubSignals). */\nexport type ProtectionCoverageDeps = {\n listOrgRepos: (org: string) => Promise<\n Array<{\n name: string;\n visibility: string;\n archived: boolean;\n secretScanning: string;\n pushProtection: string;\n }>\n >;\n listRepoRulesets: (repo: string) => Promise<Array<{ id: number; name: string }>>;\n getRuleset: (repo: string, id: number) => Promise<ExistingRuleset>;\n workflowHealth: (repo: string, filename: string) => Promise<WorkflowHealth>;\n dependencyDashboard: (repo: string) => Promise<DependencyDashboard>;\n branchTip: (repo: string, branch: string) => Promise<BranchTip | null>;\n openSecretAlerts: (repo: string) => Promise<number | \"unavailable\">;\n renovateMergeWindow: (repo: string) => Promise<RenovateMergeWindow>;\n};\n\nexport const RENOVATE_WORKFLOW_FILE = \"renovate.yml\";\n\n/** Cron is twice daily, so 3 days without a SUCCESSFUL run = 6+ consecutive\n * missed-or-failing runs — dead scheduling, dead credential, or broken\n * config, never jitter. (Success, not run-existence: a revoked App key still\n * creates a fresh failing run every tick.) */\nexport const RENOVATE_STALE_AFTER_DAYS = 3;\n\n/**\n * Operator-accepted gaps — the protection-audit analogue of the cockpit's\n * accepted-Watch acks: a KNOWN condition pending a real decision must not\n * comment the tracking issue every night, or the channel is tuned out by the\n * time a genuinely new gap lands in it. Constraints that keep this honest:\n * every entry is PR-reviewed, names ONE repo + ONE surface (never a blanket\n * mute), states the pending decision, and EXPIRES — past `until` the gap\n * returns on its own, so an acceptance can never quietly become permanent.\n */\nexport type AcceptedGap = {\n repo: string; // owner/repo\n /** Matched as a prefix of the gap detail line — surface-scoped, so an\n * accepted renovate gap never swallows a ruleset or secret-scanning gap. */\n detailPrefix: string;\n reason: string;\n until: string; // ISO date; exclusive — the gap is live again ON this date\n};\n\n// Currently EMPTY — the only two entries (no-renovate on reddoor-maintenance\n// and .github, pending an operator supply-chain call) were retired 2026-08-02\n// when the operator approved planting Renovate on both. Add entries only for\n// a named pending decision, never as a mute.\nexport const ACCEPTED_GAPS: AcceptedGap[] = [];\n\n/** Split gaps into live vs accepted-for-now. Acceptance is per-surface (detail\n * prefix) and expires: an entry past `until` accepts nothing. */\nexport function partitionAcceptedGaps(\n repo: string,\n gaps: string[],\n now: Date,\n accepted: AcceptedGap[] = ACCEPTED_GAPS,\n): { live: string[]; accepted: string[] } {\n const live: string[] = [];\n const acceptedOut: string[] = [];\n for (const gap of gaps) {\n const match = accepted.find(\n (a) => a.repo === repo && gap.startsWith(a.detailPrefix) && now < new Date(a.until),\n );\n if (match) acceptedOut.push(`${gap} [accepted until ${match.until}: ${match.reason}]`);\n else live.push(gap);\n }\n return { live, accepted: acceptedOut };\n}\n\n/**\n * Public repos only — secret scanning is plan-gated off on private Free\n * repos, and those are skipped before this is consulted. \"unavailable\" means\n * the listing couldn't read security_and_analysis (token lacks admin read),\n * which must read as unverified, not as fine.\n *\n * Three surfaces, and the third is the one the first two cannot see. Scanning\n * being ENABLED says a detector is running; it says nothing about whether\n * anyone read what it found. On 2026-09-12 a hand-run sweep found five open\n * alerts across five public repos, every one `resolution: null`, the oldest 99\n * days old — on repos this audit had been calling `covered` every night, truth-\n * fully, because both statuses were `enabled`. An alert is not self-clearing\n * and redaction at HEAD does not close one (beachfront's key was redacted in\n * `e30c756` and the alert stayed open the following 37 days), so an unread\n * alert stays unread until a human is told. `openAlerts` is that third clause.\n *\n * `openAlerts: \"unavailable\"` is the token that cannot read alerts (403/404 —\n * the endpoint needs `security_events`). It is a gap for the same reason\n * \"unavailable\" scanning status is: this sweep exists because \"couldn't check\"\n * reading as \"fine\" is the failure mode, and a scope error must not quietly\n * turn the whole fleet green on a question it can no longer ask.\n *\n * The clause is skipped entirely when scanning is not enabled: the endpoint\n * 404s by construction there, the row already names the disabled scanning as\n * its gap, and a second \"unverified\" line would only attach noise to a cause\n * already stated.\n */\nexport function secretScanningGaps(\n repo: string, // owner/repo — named in the alert gap, which is per-repo actionable\n state: { secretScanning: string; pushProtection: string },\n openAlerts: number | \"unavailable\",\n): string[] {\n const gaps: string[] = [];\n if (state.secretScanning !== \"enabled\")\n gaps.push(`secret scanning ${state.secretScanning} (fleet floor is enabled)`);\n if (state.pushProtection !== \"enabled\")\n gaps.push(`push protection ${state.pushProtection} (fleet floor is enabled)`);\n if (state.secretScanning === \"enabled\") {\n if (openAlerts === \"unavailable\")\n gaps.push(\n `open secret-scanning alerts unreadable on ${repo} (token lacks security_events/admin ` +\n `read — unverified, not clean)`,\n );\n else if (openAlerts > 0)\n gaps.push(\n `${openAlerts} open secret-scanning alert${openAlerts === 1 ? \"\" : \"s\"} on ${repo} — a ` +\n `leaked credential nobody has triaged. Fix: rotate or restrict the secret, then close ` +\n `the alert with a resolution at https://github.com/${repo}/security/secret-scanning ` +\n `(redacting the value at HEAD does NOT close it — the history is still public).`,\n );\n }\n return gaps;\n}\n\n/** Renovate liveness verdict — judged on the last SUCCESSFUL run. The cron IS\n * the merge cadence (platform auto-merge is off fleet-wide), so a workflow\n * that isn't succeeding means the repo silently stops updating — the exact\n * class the 2026-07-26 incident review called \"zero-run blindness\". */\nexport function renovateGaps(health: WorkflowHealth, now: Date): string[] {\n if (!health.present) return [\"no renovate workflow (dependency updates never run here)\"];\n if (health.state !== \"active\")\n return [\n // A disabled workflow REJECTS workflow_dispatch (HTTP 403) — it must be\n // re-enabled first; \"just dispatch it\" is a dead-end runbook line here.\n `renovate workflow is ${health.state} (re-enable it: \\`gh workflow enable renovate.yml -R <repo>\\`, then dispatch once)`,\n ];\n if (health.lastSuccessAt === null)\n return [\n \"renovate workflow has never succeeded (dispatch it once; if this gap returns, the schedule never registered — push a commit touching the workflow file)\",\n ];\n const succeededAt = Date.parse(health.lastSuccessAt);\n if (Number.isNaN(succeededAt))\n return [`renovate last-success timestamp unreadable: ${health.lastSuccessAt}`];\n const days = (now.getTime() - succeededAt) / 86_400_000;\n if (days > RENOVATE_STALE_AFTER_DAYS)\n return [`renovate last succeeded ${Math.floor(days)}d ago (cron is twice daily)`];\n return [];\n}\n\n/**\n * Renovate EFFECTIVENESS, which is a different question from liveness.\n *\n * `renovateGaps` above asks \"did the workflow run and succeed?\" — and on\n * 2026-08-03 nine repos answered YES to that while shipping zero updates for a\n * week. The `reddoor-renovate` App migration (2026-08-02) left each repo\n * holding a `renovate/all-minor-patch` branch whose tip was authored by the\n * OLD `renovate-bot` PAT identity. Renovate compares that author to its own,\n * concludes a human edited the branch, files it under \"PR Edited (Blocked) —\n * Renovate will no longer make changes\", and never touches it again. Every run\n * kept succeeding; `@reddoorla/maintenance`, `@sveltejs/kit`, `vite`,\n * `@playwright/test` and `node` were all frozen behind it; CI stayed green and\n * nothing anywhere raised a hand.\n *\n * A liveness probe structurally cannot see this: the workflow is doing its job\n * perfectly, it is simply forbidden to act. So this reads the outcome Renovate\n * itself publishes — the one place it admits to having given up.\n *\n * Absent dashboard is NOT a gap: `dependencyDashboard` is a config a repo may\n * legitimately disable, and the liveness probe already owns the dead-Renovate\n * case.\n *\n * Nor is every blocked branch a gap — see resolveBlockedBranches, which is\n * where the orphan/in-flight distinction is made. Both exclusions serve the\n * same property: this surface reports only what it can stand behind, which is\n * what decides whether an alarm channel still gets read a month from now.\n */\nexport function renovateBlockedGaps(blocked: BlockedBranchState[]): string[] {\n const orphaned = blocked.filter((b) => b.orphaned);\n if (orphaned.length === 0) return [];\n return [\n `renovate is BLOCKED on ${orphaned.length} branch(es) — ` +\n `${orphaned.map((b) => b.branch).join(\", \")} — it runs and succeeds but refuses to ` +\n `update them, so every dependency grouped there is frozen. Usual cause: the branch ` +\n `tip was authored by a retired bot identity, which reads to Renovate as a human ` +\n `edit. Fix: confirm the tip author is a bot, then delete the branch and let ` +\n `Renovate recreate it (\\`gh api -X DELETE repos/<repo>/git/refs/heads/<branch>\\`).`,\n ];\n}\n\n/**\n * A dashboard whose section vocabulary we do not share. Reported separately\n * from blocked branches because it is a different claim: not \"Renovate is\n * stuck\" but \"this audit can no longer tell whether Renovate is stuck.\" The\n * blocked heading was renamed once already (Edited/Blocked -> PR Edited\n * (Blocked) in Renovate 43), and the fleet inherits its Renovate major from\n * whatever renovatebot/github-action bakes in — so the next rename arrives via\n * an ordinary dependency PR that Renovate merges in-run, with nobody reading\n * dashboard wording. Without this, that rename would flip every repo to\n * covered on the same night and the surface would go permanently blind.\n */\nexport function dashboardVocabularyGaps(dash: DependencyDashboard): string[] {\n if (!dash.present || dash.unknownSections.length === 0) return [];\n return [\n `renovate dashboard has ${dash.unknownSections.length} section(s) this audit does ` +\n `not recognise — ${dash.unknownSections.join(\", \")}. Nothing is necessarily wrong ` +\n `with the repo, but the blocked-branch check reads Renovate's own headings, so ` +\n `drift there means it can no longer be trusted. Fix: reconcile ` +\n `KNOWN_DASHBOARD_SECTIONS in src/github/gh.ts against Renovate's ` +\n `dependency-dashboard.ts.`,\n ];\n}\n\n/* ------------------------------------------------------------------------ *\n * The OUTCOME metric (S7).\n *\n * The three Renovate surfaces above are all green, all literally correct, and\n * jointly blind to the live condition. They ask, in order: did the workflow\n * RUN (renovateGaps), does the dashboard name a branch Renovate has STOPPED\n * managing (renovateBlockedGaps), and is that dashboard still written in\n * vocabulary we can parse (dashboardVocabularyGaps). Every one of them is a\n * question about the machinery. None is a question about the result.\n *\n * The 2026-08-03 detector was built for \"Renovate REFUSES to touch the\n * branch\". Measured on 2026-09-14, the live condition is its mirror image:\n * Renovate touches `renovate/all-minor-patch` on all 26 repos that hold one —\n * every single tip is dated 2026-09-14, rewritten between 02:27Z and 17:58Z —\n * and opens a PR on none of them. Liveness green, blocked-branch check green,\n * vocabulary green, and the last feature update to actually land fleet-wide\n * merged on 2026-08-12.\n *\n * So this asks the only question the others cannot: when did an update last\n * LAND here?\n * ------------------------------------------------------------------------ */\n\n/**\n * Does a merged `renovate/*` head count as a FEATURE update?\n *\n * The two exclusions are the whole point of the metric. Renovate opened 78 PRs\n * fleet-wide between 2026-08-31 and 2026-09-14 — 32 on 09-09 alone — every one\n * of them on a `renovate/npm-*-vulnerability` or `renovate/lock-file-maintenance`\n * branch. Those two channels bypass the preset's Monday schedule and kept\n * flowing straight through the drought, so any metric that counts them reads\n * green while feature-version drift accumulates for a month. (This is also why\n * \"Renovate is dead\" is a false description of the fleet and will discredit the\n * real finding: only the GROUPED NON-MAJOR channel stopped.)\n */\nexport function isFeatureUpdateBranch(headRef: string): boolean {\n if (!headRef.startsWith(\"renovate/\")) return false;\n if (headRef === \"renovate/lock-file-maintenance\") return false;\n return !/^renovate\\/npm-.+-vulnerability$/.test(headRef);\n}\n\n/**\n * Days without a landed feature update before the sweep says so.\n *\n * Justified from the two snapshots this metric was proven against, not chosen\n * for roundness (see tests/audits/protection-coverage.test.ts):\n * - 2026-08-10, the channel demonstrably delivering: 20 measured repos, worst\n * case 14.0 days (caltex-landing), median 6.6.\n * - 2026-09-14, today: 21 measured repos, BEST case 32.3 days, worst 34.8.\n * The two populations are separated by a clean 18-day band (14.1 … 32.3) with\n * no repo in it. 21 sits inside that band with ~7 days of margin on each side,\n * and has a mechanical reading as well as an empirical one: the shared preset's\n * window is `before 11am on monday`, so 21 days is three consecutive missed\n * Monday windows — a cadence failure, never jitter.\n *\n * Note what the margin buys. At 14 days the 2026-08-10 control would have\n * warned on 2 of 20 repos — i.e. the instrument would have fired during known\n * healthy operation, which is the state in which a threshold is worth nothing.\n */\nexport const RENOVATE_DROUGHT_WARN_DAYS = 21;\n\nexport type RenovateOutcome =\n | {\n state: \"delivering\" | \"drought\";\n days: number;\n lastBranch: string;\n lastMergedAt: string;\n }\n | { state: \"unmeasured\"; reason: string };\n\n/**\n * Days since this repo last MERGED a feature-update Renovate PR.\n *\n * Three states, and the third is the one that keeps this honest. A repo with\n * no feature merge inside the scanned window is `unmeasured`, NEVER a drought:\n * - on a busy repo the window itself is the limit (100 closed PRs reach back\n * ~12 days on reddoor-maintenance and ~90 on a quiet site repo), so\n * `truncated` says the answer is older than we looked, not that it is bad;\n * - on a repo that has genuinely never merged one there is no elapsed time to\n * measure at all, and calling that a drought would fire forever on every\n * freshly bootstrapped repo.\n * Both print every night with their reason. \"I could not measure this\" must\n * never render as \"this is fine\" — that silent-green reading is the failure\n * this whole sweep exists to kill.\n */\nexport function renovateOutcome(window: RenovateMergeWindow, now: Date): RenovateOutcome {\n const last = window.merges.filter((m) => isFeatureUpdateBranch(m.headRef))[0];\n if (!last) {\n return {\n state: \"unmeasured\",\n reason: window.truncated\n ? \"no feature-update Renovate merge inside the scanned window, and the window is FULL \" +\n \"(older history not read — this repo merges too many PRs to see back that far)\"\n : \"this repo has never merged a feature-update Renovate PR (no baseline to measure from)\",\n };\n }\n const days = Math.floor((now.getTime() - new Date(last.mergedAt).getTime()) / 86_400_000);\n return {\n state: days > RENOVATE_DROUGHT_WARN_DAYS ? \"drought\" : \"delivering\",\n days,\n lastBranch: last.headRef,\n lastMergedAt: last.mergedAt,\n };\n}\n\n/** One operator-facing line per measured repo, or `null` when the outcome is\n * healthy and needs no line. Warnings and unmeasured reads are reported\n * side by side because they are the same claim — \"no update landed here\" —\n * differing only in whether we know why. */\nexport function renovateOutcomeLine(outcome: RenovateOutcome): string | null {\n if (outcome.state === \"delivering\") return null;\n if (outcome.state === \"unmeasured\") return `renovate outcome UNMEASURED — ${outcome.reason}`;\n return (\n `no feature update has LANDED in ${outcome.days}d ` +\n `(warns past ${RENOVATE_DROUGHT_WARN_DAYS}d; last was ${outcome.lastBranch} ` +\n `merged ${outcome.lastMergedAt.slice(0, 10)}). Security + lockfile Renovate PRs are ` +\n `excluded, so this is feature-version drift, not a security signal.`\n );\n}\n\n/** The machine-readable outcome line, in the same shape as PROTECTION_AUDIT.\n * Deliberately a SEPARATE line with a separate prefix: the nightly workflow\n * gates its tracking issue on `PROTECTION_AUDIT gaps=0 `, and this metric must\n * be able to say \"nothing is landing\" for a month without opening, holding\n * open, or blocking the close of a posture issue. Shared with the tests so the\n * proof asserts the product's real output rather than a restatement of it. */\nexport function renovateOutcomeSummary(outcomes: RenovateOutcome[]): string {\n const n = (state: RenovateOutcome[\"state\"]) => outcomes.filter((o) => o.state === state).length;\n return (\n `RENOVATE_OUTCOME drought=${n(\"drought\")} delivering=${n(\"delivering\")} ` +\n `unmeasured=${n(\"unmeasured\")} threshold=${RENOVATE_DROUGHT_WARN_DAYS}d`\n );\n}\n\n/** How long a human-owned blocked branch may sit before it stops reading as\n * in-flight work. Sized to clear a weekend plus slack: the fleet's own\n * human-edited Renovate PRs merge in minutes, while the branches that froze\n * nine repos sat for a week. */\nexport const BLOCKED_STALE_DAYS = 5;\n\nexport type BlockedBranchState = { branch: string; orphaned: boolean };\n\n/**\n * Decide which blocked branches are actually a posture problem.\n *\n * `PR Edited (Blocked)` is not by itself a fault — it is also Renovate's\n * designed way of saying \"a human owns this branch now\", and pushing a commit\n * onto an open Renovate PR is routine fleet practice (16 such PRs across 14\n * repos on a single day). Alarming on those would fire the nightly issue for\n * perfectly healthy in-flight work, and would attach a remediation that says\n * to delete the branch — i.e. the human's commits.\n *\n * A branch counts as ORPHANED, and therefore a gap, when either:\n * - its tip was authored by a machine, so no human owns it and nothing will\n * ever clear it on its own; or\n * - it has sat untouched past BLOCKED_STALE_DAYS, whoever authored it —\n * because Renovate updates a branch it is managing, so tip age IS time\n * spent frozen.\n *\n * The authorship test is the fast path and the staleness test is the backstop:\n * if machine-identity detection ever drifts, detection degrades to slower, not\n * to blind. A branch that no longer exists is not a gap — that is a dashboard\n * Renovate has not rewritten yet.\n */\nexport async function resolveBlockedBranches(\n repo: string,\n dash: DependencyDashboard,\n deps: Pick<ProtectionCoverageDeps, \"branchTip\">,\n now: Date,\n): Promise<BlockedBranchState[]> {\n if (!dash.present) return [];\n const states: BlockedBranchState[] = [];\n for (const branch of dash.blockedBranches) {\n const tip = await deps.branchTip(repo, branch);\n if (!tip) continue;\n const ageDays = (now.getTime() - new Date(tip.committedAt).getTime()) / 86_400_000;\n states.push({ branch, orphaned: tip.authorIsMachine || ageDays > BLOCKED_STALE_DAYS });\n }\n return states;\n}\n\n/**\n * One row per org repo. Coverage = SOME repo-sourced ruleset with zero\n * stage-1 gaps (active, empty bypass, default branch covered, deletion +\n * non_fast_forward + pull_request) AND secret scanning enabled AND a live\n * renovate workflow. Whether a covering ruleset also gates on CI is reported\n * as detail, not judged: each repo's required context differs (and\n * reddoor-maintenance deliberately has none pending release-path review), so\n * the CI-gate invariant belongs to the per-site heal, which has the evidence\n * to require the RIGHT context safely.\n *\n * Private/archived repos are skipped (rulesets on private repos need a paid\n * plan; archived repos are read-only). A repo whose probe THROWS is a `gap`,\n * not a skip — \"couldn't verify\" reading as \"fine\" is the silent-green\n * failure mode this sweep exists to kill, and the tracking issue auto-closes\n * on the next clean pass, so a transient API blip costs one issue-comment.\n */\nexport async function collectProtectionCoverage(\n org: string,\n deps: ProtectionCoverageDeps,\n now: Date = new Date(),\n accepted: AcceptedGap[] = ACCEPTED_GAPS,\n): Promise<ProtectionCoverageRow[]> {\n const rows: ProtectionCoverageRow[] = [];\n for (const r of await deps.listOrgRepos(org)) {\n const repo = `${org}/${r.name}`;\n if (r.archived || r.visibility !== \"public\") {\n rows.push({\n repo,\n status: \"skipped\",\n detail: r.archived ? \"archived\" : `${r.visibility} (rulesets need a paid plan)`,\n });\n continue;\n }\n try {\n const gaps: string[] = [];\n let coveredDetail = \"\";\n const rulesets = await deps.listRepoRulesets(repo);\n if (rulesets.length === 0) {\n gaps.push(\"no repo rulesets at all\");\n } else {\n const judged = await Promise.all(\n rulesets.map(async (rs) => {\n const full = await deps.getRuleset(repo, rs.id);\n return { name: rs.name, full, gaps: rulesetGaps(full, null) };\n }),\n );\n const covering = judged.find((j) => j.gaps.length === 0);\n if (covering) {\n const ciGated = (covering.full.rules ?? []).some(\n (rule) => rule.type === \"required_status_checks\",\n );\n coveredDetail = `\"${covering.name}\"${ciGated ? \"\" : \" — NO CI gate (refs rules only)\"}`;\n } else {\n gaps.push(judged.map((j) => `\"${j.name}\": ${j.gaps.join(\"; \")}`).join(\" | \"));\n }\n }\n // Only ask for alerts where scanning is on — see secretScanningGaps for\n // why a 404-by-construction is not worth a second gap line.\n const openAlerts =\n r.secretScanning === \"enabled\" ? await deps.openSecretAlerts(repo) : \"unavailable\";\n gaps.push(...secretScanningGaps(repo, r, openAlerts));\n gaps.push(...renovateGaps(await deps.workflowHealth(repo, RENOVATE_WORKFLOW_FILE), now));\n const dash = await deps.dependencyDashboard(repo);\n gaps.push(...dashboardVocabularyGaps(dash));\n gaps.push(...renovateBlockedGaps(await resolveBlockedBranches(repo, dash, deps, now)));\n // MEASUREMENT, not a gap: never pushed into `gaps`, so it cannot change\n // the exit code, the PROTECTION_AUDIT counts, or the tracking issue.\n const outcome = renovateOutcome(await deps.renovateMergeWindow(repo), now);\n const { live, accepted: acked } = partitionAcceptedGaps(repo, gaps, now, accepted);\n if (live.length > 0) {\n // Accepted annotations still ride along for context, but only LIVE\n // gaps make the row a gap (and thus the sweep exit 1).\n rows.push({\n repo,\n status: \"gap\",\n detail: [...live, ...acked].join(\" | \"),\n renovateOutcome: outcome,\n });\n } else if (acked.length > 0) {\n // Judged, found wanting, deliberately acked — reported as skipped\n // (visible every night with the reason + expiry), never as covered.\n rows.push({\n repo,\n status: \"skipped\",\n detail: acked.join(\" | \"),\n renovateOutcome: outcome,\n });\n } else {\n rows.push({\n repo,\n status: \"covered\",\n detail: coveredDetail,\n renovateOutcome: outcome,\n });\n }\n } catch (e) {\n rows.push({\n repo,\n status: \"gap\",\n detail: `probe failed: ${e instanceof Error ? e.message : String(e)}`,\n });\n }\n }\n return rows;\n}\n","import { partitionAcceptedGaps, type AcceptedGap } from \"../audits/protection-coverage.js\";\n\n/**\n * The pnpm `packageManager` surface of the nightly protection sweep (#690).\n *\n * Three repos sat on a pnpm security bump for two months and nothing said so.\n * The drift itself is fixed — re-measured against live GitHub 2026-09-15, all\n * 25 repos carrying a `packageManager` field are on `pnpm@11.11.0` — but the\n * reason it went\n * unseen is not: NOTHING in `src/` or `.github/workflows/` ever asked the\n * question. This module is that question, wired into the sweep that already\n * walks every repo (see `runProtectionAuditCommand`), because a fleet\n * invariant with no instrument is a fleet invariant that drifts again.\n *\n * Three gaps, each with its own failure history:\n *\n * - **No `packageManager` field** where a `package.json` exists: CI then\n * installs whatever pnpm the runner happens to default to, so the version\n * that resolves the lockfile is decided by GitHub's image, not by the repo.\n * - **A pin that disagrees with the fleet's**: the original #690 condition.\n * - **A workflow pinning a pnpm `version:` input while a `packageManager`\n * field also exists**: `pnpm/action-setup` does not prefer one over the\n * other, it HARD-ERRORS on a mismatch. As of 2026-09-15 NO repo in the\n * fleet pins a workflow input — all 75 workflow files were parsed by\n * `parsePnpmActionSetupPins` and none carries one. `reddoor-md-pdf` used to\n * be the single exception, safe only because it had no field to disagree\n * with; it has since gained the field AND dropped the input, which is the\n * correct shape. This clause is now a pure regression guard, so its failing\n * arm is injected in the test rather than borrowed from a live repo.\n *\n * Scope is deliberately the SAME population the coverage sweep judges —\n * non-archived PUBLIC repos. Not an oversight, and not because private repos\n * cannot drift: `fleet-security.yml` closes the tracking issue only when every\n * repo the issue named as a `GAP` comes back as `COVERED` in a later run, and\n * the coverage sweep never prints `COVERED` for a repo it skips. A gap raised\n * here on a repo that can only ever print `SKIPPED` would be an issue the\n * machine could never close — the exact orphan-issue failure that had to be\n * cleaned up by hand on 2026-09-14.\n */\n\n/** A repo's raw pnpm-pin facts, as read from its default branch. */\nexport type PackageManagerFacts = {\n /** `package.json` at the default branch, or `null` when the repo has none —\n * which is OUT OF SCOPE, never a gap. In the judged population that is\n * `.github` alone (measured 2026-09-15); `reddoor-prospect-runner` and\n * `reddoor-rfp-analyses` are also package-less but are PRIVATE, so they are\n * skipped before this is ever consulted. A check that gapped them would\n * light repos every night forever and teach the operator to skim past this\n * surface. */\n packageJson: string | null;\n /** Every `pnpm/action-setup` step that pins a `version:` input, by workflow\n * file. Empty is the healthy fleet-wide default. */\n workflowPins: Array<{ file: string; version: string }>;\n};\n\n/** The reads this sweep needs, injected so the verdicts stay pure and\n * fixture-testable (same pattern as ProtectionCoverageDeps). */\nexport type PackageManagerPinDeps = {\n listOrgRepos: (org: string) => Promise<\n Array<{\n name: string;\n visibility: string;\n archived: boolean;\n secretScanning: string;\n pushProtection: string;\n }>\n >;\n /** A text file at the repo's default branch, or `null` when it does not\n * exist. `null` is an ANSWER (no package.json = not a node repo); any other\n * failure throws, so an API hiccup can never arrive here as \"no pin\". */\n repoTextFile: (repo: string, path: string) => Promise<string | null>;\n /** Paths of every file in `.github/workflows`, or `[]` when the directory is\n * absent. */\n listWorkflowPaths: (repo: string) => Promise<string[]>;\n};\n\n/**\n * Operator-accepted gaps, in the shape and under the doctrine of\n * `ACCEPTED_GAPS`: one repo, one surface, a NAMED pending fix, and an expiry\n * past which the gap returns on its own.\n *\n * Currently EMPTY, and that is the healthy state. The two original entries —\n * `claude-skills` and `reddoor-md-pdf`, the two repos the 2026-09-16\n * measurement found carrying no `packageManager` field — were retired\n * 2026-09-15 when the fix they named actually landed: both now carry\n * `pnpm@11.11.0`, and `reddoor-md-pdf`'s `pnpm/action-setup` step no longer\n * takes a `version:` input at all (verified against live GitHub, not inferred).\n *\n * An acceptance outliving its fix is not inert. It is scoped by repo AND\n * surface, so for as long as it stands these two repos are the only two in the\n * fleet whose \"no packageManager field\" gap CANNOT gate — precisely the repos\n * that just proved they are the ones that lose the field. Had either regressed\n * before 2026-10-01 the nightly would have reported it as accepted-with-expiry\n * and gated nothing. A mute that survives its reason silences exactly the\n * repo it was written about.\n */\nexport const PACKAGE_MANAGER_ACCEPTED_GAPS: AcceptedGap[] = [];\n\n/** What a repo's `package.json` says about pnpm. The three states are distinct\n * shapes on purpose: a caller cannot read \"I could not parse this\" as \"no pin\"\n * by forgetting to check a flag. */\nexport type PackageManagerField =\n | { state: \"present\"; value: string }\n | { state: \"absent\" }\n | { state: \"unreadable\"; reason: string };\n\n/** The `packageManager` field of one `package.json`. */\nexport function parsePackageManagerField(packageJson: string): PackageManagerField {\n let parsed: unknown;\n try {\n parsed = JSON.parse(packageJson);\n } catch (e) {\n return {\n state: \"unreadable\",\n reason: `package.json is not valid JSON (${e instanceof Error ? e.message : String(e)})`,\n };\n }\n if (typeof parsed !== \"object\" || parsed === null) {\n return { state: \"unreadable\", reason: \"package.json is not a JSON object\" };\n }\n const value = (parsed as Record<string, unknown>).packageManager;\n if (value === undefined) return { state: \"absent\" };\n if (typeof value !== \"string\") {\n return { state: \"unreadable\", reason: `packageManager is ${typeof value}, not a string` };\n }\n return { state: \"present\", value };\n}\n\n/**\n * Every pnpm version pinned by a `pnpm/action-setup` step in one workflow file.\n *\n * THE TRAP THIS FUNCTION EXISTS FOR. The first hand-run measurement of this\n * question grepped `version:` anywhere in any file that mentioned\n * `pnpm/action-setup`, and reported ELEVEN healthy workflows as pinning a pnpm\n * version. Every one of those matches was `node-version: 24`, from an\n * `actions/setup-node` step elsewhere in the same file. Posted, it would have\n * sent someone editing eleven workflows that were already correct.\n *\n * So the scan is bounded to the action-setup step's OWN block, with two\n * independent defenses against that exact miss:\n * 1. the key regex is anchored at the start of the key, so `node-version:`\n * cannot match `version:`; and\n * 2. the scan stops at the end of the step, so a sibling step's `with:`\n * block is never entered at all.\n * Either alone would have caught it. Both are here because this surface has\n * already produced a confidently wrong number once.\n *\n * Parsed by a targeted scan rather than a YAML dependency, following\n * `readLockedCliVersion`: this needs one input out of one step, and the repo\n * deliberately carries no YAML parser.\n */\nexport function parsePnpmActionSetupPins(workflow: string): string[] {\n const lines = workflow.split(\"\\n\");\n const pins: string[] = [];\n\n const indentOf = (line: string): number => line.length - line.trimStart().length;\n const isBlank = (line: string): boolean => line.trim().length === 0 || /^\\s*#/.test(line);\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!;\n const uses = /^(\\s*)(-\\s+)?uses:\\s*['\"]?pnpm\\/action-setup(?=[@'\"\\s]|$)/.exec(line);\n if (!uses) continue;\n // A step written as `- uses: …` puts the step's other keys at the column\n // AFTER the dash; one written as `uses:` under an earlier `- name:` puts\n // them at its own column.\n const contentIndent = uses[1]!.length + (uses[2] ? uses[2].length : 0);\n\n for (let j = i + 1; j < lines.length; j++) {\n const body = lines[j]!;\n if (isBlank(body)) continue;\n const indent = indentOf(body);\n // Dedent past the step's keys, or a new `- ` item at the same column:\n // either way this step is over and nothing below belongs to it.\n if (indent < contentIndent) break;\n if (indent === contentIndent && /^\\s*-\\s/.test(body)) break;\n if (indent !== contentIndent || !/^\\s*with:\\s*$/.test(body)) continue;\n\n // Inside this step's `with:` mapping — and only this one.\n for (let k = j + 1; k < lines.length; k++) {\n const input = lines[k]!;\n if (isBlank(input)) continue;\n if (indentOf(input) <= contentIndent) break;\n const m = /^\\s*version:\\s*['\"]?([^'\"#\\s]+)/.exec(input);\n if (m) pins.push(m[1]!);\n }\n break;\n }\n }\n return pins;\n}\n\n/** The fleet's pnpm pin, DERIVED from the fleet rather than hand-typed here.\n * A constant in this file would be one more copy of the number to drift —\n * `convert-to-pnpm.ts` carried `DEFAULT_PNPM_VERSION = \"10.33.1\"` for about a\n * year while the fleet ran `11.11.0`, which is exactly what a hand-typed pin\n * does when nothing watches it. That constant is corrected and now has a test\n * watching it (#835); this value is derived, so it cannot need one. */\nexport type FleetPin =\n | { state: \"majority\"; pin: string; count: number; total: number }\n | { state: \"split\"; counts: Array<{ pin: string; count: number }>; total: number }\n | { state: \"none\" };\n\n/**\n * The pin a strict majority of pinned repos agree on.\n *\n * A STRICT majority, not merely the most common value: with no majority the\n * fleet has no single answer, and picking the modal value would gap every repo\n * on the other side of a genuine split — 24 GAP lines for one unmade decision.\n * `split` reports the condition and gaps nobody on this clause; the missing-\n * field and workflow-collision clauses keep working underneath it.\n */\nexport function fleetPin(pins: string[]): FleetPin {\n if (pins.length === 0) return { state: \"none\" };\n const tally = new Map<string, number>();\n for (const p of pins) tally.set(p, (tally.get(p) ?? 0) + 1);\n const counts = [...tally.entries()]\n .map(([pin, count]) => ({ pin, count }))\n .sort((a, b) => b.count - a.count || a.pin.localeCompare(b.pin));\n const top = counts[0]!;\n if (top.count * 2 > pins.length) {\n return { state: \"majority\", pin: top.pin, count: top.count, total: pins.length };\n }\n return { state: \"split\", counts, total: pins.length };\n}\n\n/** Gap lines for one in-scope repo. Empty = this repo's pnpm pin is sound. */\nexport function packageManagerGaps(facts: PackageManagerFacts, fleet: FleetPin): string[] {\n if (facts.packageJson === null) return [];\n const field = parsePackageManagerField(facts.packageJson);\n const gaps: string[] = [];\n const pinnedWorkflows = facts.workflowPins\n .map((w) => `${w.file} (version: ${w.version})`)\n .join(\", \");\n\n if (field.state === \"unreadable\") {\n gaps.push(`packageManager unreadable — ${field.reason} (unverified, not clean)`);\n return gaps;\n }\n\n if (field.state === \"absent\") {\n const fleetSays =\n fleet.state === \"majority\" ? ` (the fleet pin is ${fleet.pin})` : \" (fleet pin unsettled)\";\n // The remediation DIFFERS when a workflow already pins a version, and\n // getting this wrong breaks the repo it is trying to fix: adding the field\n // next to a surviving `version:` input is precisely the mismatch\n // `pnpm/action-setup` hard-errors on.\n gaps.push(\n facts.workflowPins.length > 0\n ? `no packageManager field in package.json${fleetSays}, and ${pinnedWorkflows} pins pnpm ` +\n `in a pnpm/action-setup step instead. Fix: add the field AND delete the workflow's ` +\n `version input in the SAME change — adding the field alone makes action-setup ` +\n `hard-error on the mismatch.`\n : `no packageManager field in package.json${fleetSays} — CI installs whatever pnpm the ` +\n `runner defaults to, so the version resolving this lockfile is GitHub's choice, not ` +\n `the repo's. Fix: add \"packageManager\" to package.json.`,\n );\n return gaps;\n }\n\n if (fleet.state === \"majority\" && field.value !== fleet.pin) {\n gaps.push(\n `packageManager is ${field.value} but the fleet pin is ${fleet.pin} ` +\n `(${fleet.count}/${fleet.total} repos agree). Fix: bump the field, or say why this repo ` +\n `is deliberately apart.`,\n );\n }\n\n if (facts.workflowPins.length > 0) {\n const agree = facts.workflowPins.every((w) => `pnpm@${w.version}` === field.value);\n gaps.push(\n `${pinnedWorkflows} pins a pnpm version input while package.json pins ${field.value}` +\n (agree ? \" — they agree today, but nothing keeps them in step\" : \" — THEY DISAGREE\") +\n `. pnpm/action-setup does not prefer either side, it hard-errors on a mismatch. ` +\n `Fix: delete the workflow's version input; packageManager is the single source of truth.`,\n );\n }\n\n return gaps;\n}\n\n/** One row per repo the sweep looked at. */\nexport type PackageManagerRow = {\n repo: string; // owner/repo\n scope: \"judged\" | \"out-of-scope\" | \"skipped\";\n /** Live gaps — these gate. Empty on a sound repo. */\n gaps: string[];\n /** Accepted-for-now gaps: reported every night with reason + expiry, never\n * gating (see PACKAGE_MANAGER_ACCEPTED_GAPS). */\n accepted: string[];\n detail: string;\n};\n\n/**\n * The pnpm-pin sweep over one org.\n *\n * Two passes, and the order matters: the fleet pin is DERIVED from what the\n * repos actually say, so every repo's facts must be read before any repo can\n * be judged against them.\n */\nexport async function collectPackageManagerPins(\n org: string,\n deps: PackageManagerPinDeps,\n now: Date = new Date(),\n accepted: AcceptedGap[] = PACKAGE_MANAGER_ACCEPTED_GAPS,\n): Promise<PackageManagerRow[]> {\n const rows: PackageManagerRow[] = [];\n const facts = new Map<string, PackageManagerFacts>();\n\n for (const r of await deps.listOrgRepos(org)) {\n const repo = `${org}/${r.name}`;\n if (r.archived || r.visibility !== \"public\") {\n rows.push({\n repo,\n scope: \"skipped\",\n gaps: [],\n accepted: [],\n detail: r.archived ? \"archived\" : `${r.visibility}`,\n });\n continue;\n }\n try {\n const packageJson = await deps.repoTextFile(repo, \"package.json\");\n if (packageJson === null) {\n // No package.json = no pnpm pin to hold. OUT OF SCOPE, never a gap.\n rows.push({\n repo,\n scope: \"out-of-scope\",\n gaps: [],\n accepted: [],\n detail: \"no package.json\",\n });\n continue;\n }\n const workflowPins: PackageManagerFacts[\"workflowPins\"] = [];\n for (const path of await deps.listWorkflowPaths(repo)) {\n const contents = await deps.repoTextFile(repo, path);\n if (contents === null) continue;\n for (const version of parsePnpmActionSetupPins(contents)) {\n workflowPins.push({ file: path.replace(/^\\.github\\/workflows\\//, \"\"), version });\n }\n }\n facts.set(repo, { packageJson, workflowPins });\n rows.push({ repo, scope: \"judged\", gaps: [], accepted: [], detail: \"\" });\n } catch (e) {\n // \"Couldn't read it\" is a gap, never a pass — the same doctrine the\n // coverage sweep applies to a probe that throws.\n rows.push({\n repo,\n scope: \"judged\",\n gaps: [`pnpm pin probe failed: ${e instanceof Error ? e.message : String(e)}`],\n accepted: [],\n detail: \"probe failed\",\n });\n }\n }\n\n const pinned: string[] = [];\n for (const f of facts.values()) {\n const field = parsePackageManagerField(f.packageJson!);\n if (field.state === \"present\") pinned.push(field.value);\n }\n const fleet = fleetPin(pinned);\n\n return rows.map((row) => {\n const f = facts.get(row.repo);\n if (!f) return row;\n const all = packageManagerGaps(f, fleet);\n const split = partitionAcceptedGaps(row.repo, all, now, accepted);\n const field = parsePackageManagerField(f.packageJson!);\n return {\n ...row,\n gaps: split.live,\n accepted: split.accepted,\n detail: field.state === \"present\" ? field.value : row.detail,\n };\n });\n}\n\n/** The machine-readable summary, in the shape of RENOVATE_OUTCOME: its own\n * prefix, so it can never be mistaken for a `GAP`/`COVERED` line or for the\n * `PROTECTION_AUDIT gaps=0 ` string the nightly gates its issue on. */\nexport function packageManagerPinSummary(rows: PackageManagerRow[]): string {\n const judged = rows.filter((r) => r.scope === \"judged\");\n const pins = judged.map((r) => r.detail).filter((d) => d.startsWith(\"pnpm@\"));\n const fleet = fleetPin(pins);\n const pin =\n fleet.state === \"majority\"\n ? fleet.pin\n : fleet.state === \"split\"\n ? `SPLIT(${fleet.counts.map((c) => `${c.pin}x${c.count}`).join(\",\")})`\n : \"none\";\n return (\n `PACKAGE_MANAGER_PIN gaps=${judged.filter((r) => r.gaps.length > 0).length} ` +\n `pinned=${pins.length} judged=${judged.length} ` +\n `out-of-scope=${rows.filter((r) => r.scope === \"out-of-scope\").length} ` +\n `skipped=${rows.filter((r) => r.scope === \"skipped\").length} fleetPin=${pin}`\n );\n}\n","import {\n collectProtectionCoverage,\n renovateOutcomeLine,\n renovateOutcomeSummary,\n type ProtectionCoverageDeps,\n} from \"../../audits/protection-coverage.js\";\nimport {\n collectPackageManagerPins,\n packageManagerPinSummary,\n type PackageManagerPinDeps,\n} from \"../../github/package-manager-pin.js\";\nimport { makeGitHub } from \"../../github/gh.js\";\n\n/** Everything the audit reads. The pnpm-pin sweep's reads are REQUIRED, not\n * optional: an optional dep that tests forget to supply is a check that\n * silently measures nothing, which is the failure this guard was built to\n * end. Widening the type makes the compiler name every call site instead. */\nexport type ProtectionAuditDeps = ProtectionCoverageDeps & PackageManagerPinDeps;\n\n/** Exit 1 on ANY gap: unlike the fleet write sweeps (majority-rule), a single\n * unprotected public repo is a real hole, not a flake to tolerate. */\nexport function protectionAuditExitCode(gaps: number): number {\n return gaps > 0 ? 1 : 0;\n}\n\n/** `protection-audit --org <org>`: verify every public repo in the org carries\n * a sound branch ruleset, live secret scanning + push protection, a renovate\n * workflow that actually runs (see collectProtectionCoverage for the\n * verdicts), and a pnpm `packageManager` pin that matches the fleet's (see\n * collectPackageManagerPins). Emits one line per repo + a machine-readable\n * PROTECTION_AUDIT summary the nightly workflow gates its tracking issue on.\n * A missing fleet token is a clean skip (local runs), matching github-signals. */\nexport async function runProtectionAuditCommand(\n opts: { org?: string | undefined },\n // Injected in tests; defaults to the real gh-CLI-backed factory.\n depsOverride?: ProtectionAuditDeps,\n): Promise<{ output: string; code: number }> {\n const org = opts.org?.trim();\n if (!org) {\n return { output: \"protection-audit requires --org <org>\", code: 2 };\n }\n const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();\n if (!depsOverride && !token) {\n return {\n output: \"protection-audit skipped: no RENOVATE_TOKEN/GH_TOKEN (fleet read) configured.\",\n code: 0,\n };\n }\n const deps = depsOverride ?? makeGitHub({ token: token! });\n\n const coverage = await collectProtectionCoverage(org, deps);\n const pinRows = await collectPackageManagerPins(org, deps);\n const pinByRepo = new Map(pinRows.map((p) => [p.repo, p]));\n\n // MERGED INTO THE REPO'S OWN ROW, never appended as extra GAP lines of their\n // own. `fleet-security.yml` closes the tracking issue only when every repo\n // the issue named as a GAP comes back COVERED, so a repo printing GAP and\n // COVERED in the same sweep would let it close on its own gap.\n const rows = coverage.map((row) => {\n const pin = pinByRepo.get(row.repo);\n if (!pin || pin.gaps.length === 0) return row;\n return {\n ...row,\n status: \"gap\" as const,\n detail: [row.detail, ...pin.gaps].filter((d) => d.length > 0).join(\" | \"),\n };\n });\n\n const gaps = rows.filter((r) => r.status === \"gap\");\n const covered = rows.filter((r) => r.status === \"covered\");\n const skipped = rows.filter((r) => r.status === \"skipped\");\n\n const lines = rows.map((r) => `${r.status.toUpperCase().padEnd(7)} ${r.repo} — ${r.detail}`);\n\n // The outcome metric rides in its OWN line prefixes and its OWN summary line.\n // The nightly workflow greps `^GAP` for the issue body, `^COVERED` for the\n // close list, and `PROTECTION_AUDIT gaps=0 ` to decide a clean sweep — none\n // of which these lines match, so the tracking-issue plumbing is untouched by\n // construction rather than by promise.\n const measured = rows.map((r) => r.renovateOutcome).filter((o) => o !== undefined);\n for (const r of rows) {\n const line = r.renovateOutcome && renovateOutcomeLine(r.renovateOutcome);\n if (line) lines.push(`WARN ${r.repo} — ${line}`);\n }\n // Accepted pnpm-pin gaps print every night with their reason and expiry, on\n // the same non-gating WARN prefix: visible, never silent, never an alarm.\n for (const p of pinRows) {\n for (const note of p.accepted) lines.push(`WARN ${p.repo} — ${note}`);\n }\n lines.push(renovateOutcomeSummary(measured));\n lines.push(packageManagerPinSummary(pinRows));\n lines.push(\n `PROTECTION_AUDIT gaps=${gaps.length} covered=${covered.length} skipped=${skipped.length} total=${rows.length}`,\n );\n return { output: lines.join(\"\\n\"), code: protectionAuditExitCode(gaps.length) };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/audits/protection-coverage.ts","../src/github/package-manager-pin.ts","../src/cli/commands/protection-audit.ts"],"sourcesContent":["import { rulesetGaps, type ExistingRuleset } from \"../github/rulesets.js\";\nimport type {\n BranchTip,\n DependencyDashboard,\n RenovateMergeWindow,\n WorkflowHealth,\n} from \"../github/gh.js\";\n\n/**\n * Org-wide protection-coverage sweep (spec:\n * docs/superpowers/specs/2026-08-02-ruleset-self-healing-design.md).\n *\n * The `self-updating` recipe HEALS fleet sites, but it runs only when someone\n * runs it, and only over Airtable-listed sites — which is exactly how the\n * `.github` repo (no Airtable row) sat with zero protection until 2026-08-01.\n * This sweep is the layer that would have caught that on its own: it\n * enumerates the org FROM THE GITHUB API — never a hand-typed or\n * Airtable-scoped list, both of which have already produced false \"all\n * clear\"s — and judges every public repo's protection by SHAPE, not by name,\n * so a differently-named but sound ruleset (reddoor-maintenance's \"Main\n * Protection\") counts.\n *\n * Since 2026-08-02 the same sweep also verifies three more posture surfaces\n * per public repo, because each regresses silently and none has any other\n * watcher:\n * - secret scanning + push protection stay ENABLED (a repo transferred in,\n * or an org-default flip, arrives with them off), and — since 2026-09-14 —\n * that what the detector FOUND has been read: a repo with open\n * secret-scanning alerts is a gap however green its settings are, because\n * an enabled scanner nobody reads produced five untriaged alerts, the\n * oldest 99 days old, on repos this sweep was calling covered;\n * - the renovate workflow is registered, active, and has actually RUN\n * recently. GitHub auto-disables schedules after 60 quiet days, and\n * schedule triggers in template-cloned files may never register at all —\n * either way a fully-quiet repo stops receiving updates with zero signal\n * (the 2026-08-02 sweep found two never-run and two 5-days-stale repos on\n * its first pass);\n * - renovate is not only alive but ALLOWED TO ACT — a workflow can run green\n * forever while Renovate has quietly stopped managing a branch, which froze\n * updates on nine repos for a week in August 2026 (see\n * renovateBlockedGaps). Liveness cannot see this; only the outcome can.\n */\nexport type ProtectionCoverageRow = {\n repo: string; // owner/repo\n status: \"covered\" | \"gap\" | \"skipped\";\n /** Human-readable: covering ruleset + whether it gates on CI, the specific\n * gaps, or why the repo was skipped. */\n detail: string;\n /** The one OUTCOME measurement on this sweep: did a feature update actually\n * LAND here (see renovateOutcome). Deliberately not folded into `status` —\n * it warns, it never gaps, so it can never file or hold open the tracking\n * issue. Absent on skipped repos, which are not measured at all. */\n renovateOutcome?: RenovateOutcome;\n};\n\n/** The reads the sweep needs — a subset of the GitHub factory, injected so the\n * sweep is pure + testable (same pattern as collectGitHubSignals). */\nexport type ProtectionCoverageDeps = {\n listOrgRepos: (org: string) => Promise<\n Array<{\n name: string;\n visibility: string;\n archived: boolean;\n secretScanning: string;\n pushProtection: string;\n }>\n >;\n listRepoRulesets: (repo: string) => Promise<Array<{ id: number; name: string }>>;\n getRuleset: (repo: string, id: number) => Promise<ExistingRuleset>;\n workflowHealth: (repo: string, filename: string) => Promise<WorkflowHealth>;\n dependencyDashboard: (repo: string) => Promise<DependencyDashboard>;\n branchTip: (repo: string, branch: string) => Promise<BranchTip | null>;\n openSecretAlerts: (repo: string) => Promise<number | \"unavailable\">;\n renovateMergeWindow: (repo: string) => Promise<RenovateMergeWindow>;\n};\n\nexport const RENOVATE_WORKFLOW_FILE = \"renovate.yml\";\n\n/** Cron is twice daily, so 3 days without a SUCCESSFUL run = 6+ consecutive\n * missed-or-failing runs — dead scheduling, dead credential, or broken\n * config, never jitter. (Success, not run-existence: a revoked App key still\n * creates a fresh failing run every tick.) */\nexport const RENOVATE_STALE_AFTER_DAYS = 3;\n\n/**\n * Operator-accepted gaps — the protection-audit analogue of the cockpit's\n * accepted-Watch acks: a KNOWN condition pending a real decision must not\n * comment the tracking issue every night, or the channel is tuned out by the\n * time a genuinely new gap lands in it. Constraints that keep this honest:\n * every entry is PR-reviewed, names ONE repo + ONE surface (never a blanket\n * mute), states the pending decision, and EXPIRES — past `until` the gap\n * returns on its own, so an acceptance can never quietly become permanent.\n */\nexport type AcceptedGap = {\n repo: string; // owner/repo\n /** Matched as a prefix of the gap detail line — surface-scoped, so an\n * accepted renovate gap never swallows a ruleset or secret-scanning gap. */\n detailPrefix: string;\n reason: string;\n until: string; // ISO date; exclusive — the gap is live again ON this date\n};\n\n// Currently EMPTY — the only two entries (no-renovate on reddoor-maintenance\n// and .github, pending an operator supply-chain call) were retired 2026-08-02\n// when the operator approved planting Renovate on both. Add entries only for\n// a named pending decision, never as a mute.\nexport const ACCEPTED_GAPS: AcceptedGap[] = [];\n\n/** Split gaps into live vs accepted-for-now. Acceptance is per-surface (detail\n * prefix) and expires: an entry past `until` accepts nothing. */\nexport function partitionAcceptedGaps(\n repo: string,\n gaps: string[],\n now: Date,\n accepted: AcceptedGap[] = ACCEPTED_GAPS,\n): { live: string[]; accepted: string[] } {\n const live: string[] = [];\n const acceptedOut: string[] = [];\n for (const gap of gaps) {\n const match = accepted.find(\n (a) => a.repo === repo && gap.startsWith(a.detailPrefix) && now < new Date(a.until),\n );\n if (match) acceptedOut.push(`${gap} [accepted until ${match.until}: ${match.reason}]`);\n else live.push(gap);\n }\n return { live, accepted: acceptedOut };\n}\n\n/**\n * Public repos only — secret scanning is plan-gated off on private Free\n * repos, and those are skipped before this is consulted. \"unavailable\" means\n * the listing couldn't read security_and_analysis (token lacks admin read),\n * which must read as unverified, not as fine.\n *\n * Three surfaces, and the third is the one the first two cannot see. Scanning\n * being ENABLED says a detector is running; it says nothing about whether\n * anyone read what it found. On 2026-09-12 a hand-run sweep found five open\n * alerts across five public repos, every one `resolution: null`, the oldest 99\n * days old — on repos this audit had been calling `covered` every night, truth-\n * fully, because both statuses were `enabled`. An alert is not self-clearing\n * and redaction at HEAD does not close one (beachfront's key was redacted in\n * `e30c756` and the alert stayed open the following 37 days), so an unread\n * alert stays unread until a human is told. `openAlerts` is that third clause.\n *\n * `openAlerts: \"unavailable\"` is the token that cannot read alerts (403/404 —\n * the endpoint needs `security_events`). It is a gap for the same reason\n * \"unavailable\" scanning status is: this sweep exists because \"couldn't check\"\n * reading as \"fine\" is the failure mode, and a scope error must not quietly\n * turn the whole fleet green on a question it can no longer ask.\n *\n * The clause is skipped entirely when scanning is not enabled: the endpoint\n * 404s by construction there, the row already names the disabled scanning as\n * its gap, and a second \"unverified\" line would only attach noise to a cause\n * already stated.\n */\nexport function secretScanningGaps(\n repo: string, // owner/repo — named in the alert gap, which is per-repo actionable\n state: { secretScanning: string; pushProtection: string },\n openAlerts: number | \"unavailable\",\n): string[] {\n const gaps: string[] = [];\n if (state.secretScanning !== \"enabled\")\n gaps.push(`secret scanning ${state.secretScanning} (fleet floor is enabled)`);\n if (state.pushProtection !== \"enabled\")\n gaps.push(`push protection ${state.pushProtection} (fleet floor is enabled)`);\n if (state.secretScanning === \"enabled\") {\n if (openAlerts === \"unavailable\")\n gaps.push(\n `open secret-scanning alerts unreadable on ${repo} (token lacks security_events/admin ` +\n `read — unverified, not clean)`,\n );\n else if (openAlerts > 0)\n gaps.push(\n `${openAlerts} open secret-scanning alert${openAlerts === 1 ? \"\" : \"s\"} on ${repo} — a ` +\n `leaked credential nobody has triaged. Fix: rotate or restrict the secret, then close ` +\n `the alert with a resolution at https://github.com/${repo}/security/secret-scanning ` +\n `(redacting the value at HEAD does NOT close it — the history is still public).`,\n );\n }\n return gaps;\n}\n\n/** Renovate liveness verdict — judged on the last SUCCESSFUL run. The cron IS\n * the merge cadence (platform auto-merge is off fleet-wide), so a workflow\n * that isn't succeeding means the repo silently stops updating — the exact\n * class the 2026-07-26 incident review called \"zero-run blindness\". */\nexport function renovateGaps(health: WorkflowHealth, now: Date): string[] {\n if (!health.present) return [\"no renovate workflow (dependency updates never run here)\"];\n if (health.state !== \"active\")\n return [\n // A disabled workflow REJECTS workflow_dispatch (HTTP 403) — it must be\n // re-enabled first; \"just dispatch it\" is a dead-end runbook line here.\n `renovate workflow is ${health.state} (re-enable it: \\`gh workflow enable renovate.yml -R <repo>\\`, then dispatch once)`,\n ];\n if (health.lastSuccessAt === null)\n return [\n \"renovate workflow has never succeeded (dispatch it once; if this gap returns, the schedule never registered — push a commit touching the workflow file)\",\n ];\n const succeededAt = Date.parse(health.lastSuccessAt);\n if (Number.isNaN(succeededAt))\n return [`renovate last-success timestamp unreadable: ${health.lastSuccessAt}`];\n const days = (now.getTime() - succeededAt) / 86_400_000;\n if (days > RENOVATE_STALE_AFTER_DAYS)\n return [`renovate last succeeded ${Math.floor(days)}d ago (cron is twice daily)`];\n return [];\n}\n\n/**\n * Renovate EFFECTIVENESS, which is a different question from liveness.\n *\n * `renovateGaps` above asks \"did the workflow run and succeed?\" — and on\n * 2026-08-03 nine repos answered YES to that while shipping zero updates for a\n * week. The `reddoor-renovate` App migration (2026-08-02) left each repo\n * holding a `renovate/all-minor-patch` branch whose tip was authored by the\n * OLD `renovate-bot` PAT identity. Renovate compares that author to its own,\n * concludes a human edited the branch, files it under \"PR Edited (Blocked) —\n * Renovate will no longer make changes\", and never touches it again. Every run\n * kept succeeding; `@reddoorla/maintenance`, `@sveltejs/kit`, `vite`,\n * `@playwright/test` and `node` were all frozen behind it; CI stayed green and\n * nothing anywhere raised a hand.\n *\n * A liveness probe structurally cannot see this: the workflow is doing its job\n * perfectly, it is simply forbidden to act. So this reads the outcome Renovate\n * itself publishes — the one place it admits to having given up.\n *\n * Absent dashboard is NOT a gap: `dependencyDashboard` is a config a repo may\n * legitimately disable, and the liveness probe already owns the dead-Renovate\n * case.\n *\n * Nor is every blocked branch a gap — see resolveBlockedBranches, which is\n * where the orphan/in-flight distinction is made. Both exclusions serve the\n * same property: this surface reports only what it can stand behind, which is\n * what decides whether an alarm channel still gets read a month from now.\n */\nexport function renovateBlockedGaps(blocked: BlockedBranchState[]): string[] {\n const orphaned = blocked.filter((b) => b.orphaned);\n if (orphaned.length === 0) return [];\n return [\n `renovate is BLOCKED on ${orphaned.length} branch(es) — ` +\n `${orphaned.map((b) => b.branch).join(\", \")} — it runs and succeeds but refuses to ` +\n `update them, so every dependency grouped there is frozen. Usual cause: the branch ` +\n `tip was authored by a retired bot identity, which reads to Renovate as a human ` +\n `edit. Fix: confirm the tip author is a bot, then delete the branch and let ` +\n `Renovate recreate it (\\`gh api -X DELETE repos/<repo>/git/refs/heads/<branch>\\`).`,\n ];\n}\n\n/**\n * A dashboard whose section vocabulary we do not share. Reported separately\n * from blocked branches because it is a different claim: not \"Renovate is\n * stuck\" but \"this audit can no longer tell whether Renovate is stuck.\" The\n * blocked heading was renamed once already (Edited/Blocked -> PR Edited\n * (Blocked) in Renovate 43), and the fleet inherits its Renovate major from\n * whatever renovatebot/github-action bakes in — so the next rename arrives via\n * an ordinary dependency PR that Renovate merges in-run, with nobody reading\n * dashboard wording. Without this, that rename would flip every repo to\n * covered on the same night and the surface would go permanently blind.\n */\nexport function dashboardVocabularyGaps(dash: DependencyDashboard): string[] {\n if (!dash.present || dash.unknownSections.length === 0) return [];\n return [\n `renovate dashboard has ${dash.unknownSections.length} section(s) this audit does ` +\n `not recognise — ${dash.unknownSections.join(\", \")}. Nothing is necessarily wrong ` +\n `with the repo, but the blocked-branch check reads Renovate's own headings, so ` +\n `drift there means it can no longer be trusted. Fix: reconcile ` +\n `KNOWN_DASHBOARD_SECTIONS in src/github/gh.ts against Renovate's ` +\n `dependency-dashboard.ts.`,\n ];\n}\n\n/* ------------------------------------------------------------------------ *\n * The OUTCOME metric (S7).\n *\n * The three Renovate surfaces above are all green, all literally correct, and\n * jointly blind to the live condition. They ask, in order: did the workflow\n * RUN (renovateGaps), does the dashboard name a branch Renovate has STOPPED\n * managing (renovateBlockedGaps), and is that dashboard still written in\n * vocabulary we can parse (dashboardVocabularyGaps). Every one of them is a\n * question about the machinery. None is a question about the result.\n *\n * The 2026-08-03 detector was built for \"Renovate REFUSES to touch the\n * branch\". Measured on 2026-09-14, the live condition is its mirror image:\n * Renovate touches `renovate/all-minor-patch` on all 26 repos that hold one —\n * every single tip is dated 2026-09-14, rewritten between 02:27Z and 17:58Z —\n * and opens a PR on none of them. Liveness green, blocked-branch check green,\n * vocabulary green, and the last feature update to actually land fleet-wide\n * merged on 2026-08-12.\n *\n * So this asks the only question the others cannot: when did an update last\n * LAND here?\n * ------------------------------------------------------------------------ */\n\n/**\n * Does a merged `renovate/*` head count as a FEATURE update?\n *\n * The two exclusions are the whole point of the metric. Renovate opened 78 PRs\n * fleet-wide between 2026-08-31 and 2026-09-14 — 32 on 09-09 alone — every one\n * of them on a `renovate/npm-*-vulnerability` or `renovate/lock-file-maintenance`\n * branch. Those two channels bypass the preset's Monday schedule and kept\n * flowing straight through the drought, so any metric that counts them reads\n * green while feature-version drift accumulates for a month. (This is also why\n * \"Renovate is dead\" is a false description of the fleet and will discredit the\n * real finding: only the GROUPED NON-MAJOR channel stopped.)\n */\nexport function isFeatureUpdateBranch(headRef: string): boolean {\n if (!headRef.startsWith(\"renovate/\")) return false;\n if (headRef === \"renovate/lock-file-maintenance\") return false;\n return !/^renovate\\/npm-.+-vulnerability$/.test(headRef);\n}\n\n/**\n * Days without a landed feature update before the sweep says so.\n *\n * Justified from the two snapshots this metric was proven against, not chosen\n * for roundness (see tests/audits/protection-coverage.test.ts):\n * - 2026-08-10, the channel demonstrably delivering: 20 measured repos, worst\n * case 14.0 days (caltex-landing), median 6.6.\n * - 2026-09-14, today: 21 measured repos, BEST case 32.3 days, worst 34.8.\n * The two populations are separated by a clean 18-day band (14.1 … 32.3) with\n * no repo in it. 21 sits inside that band with ~7 days of margin on each side,\n * and has a mechanical reading as well as an empirical one: the shared preset's\n * window is `before 11am on monday`, so 21 days is three consecutive missed\n * Monday windows — a cadence failure, never jitter.\n *\n * Note what the margin buys. At 14 days the 2026-08-10 control would have\n * warned on 2 of 20 repos — i.e. the instrument would have fired during known\n * healthy operation, which is the state in which a threshold is worth nothing.\n */\nexport const RENOVATE_DROUGHT_WARN_DAYS = 21;\n\nexport type RenovateOutcome =\n | {\n state: \"delivering\" | \"drought\";\n days: number;\n lastBranch: string;\n lastMergedAt: string;\n }\n | { state: \"unmeasured\"; reason: string };\n\n/**\n * Days since this repo last MERGED a feature-update Renovate PR.\n *\n * Three states, and the third is the one that keeps this honest. A repo with\n * no feature merge inside the scanned window is `unmeasured`, NEVER a drought:\n * - on a busy repo the window itself is the limit (100 closed PRs reach back\n * ~12 days on reddoor-maintenance and ~90 on a quiet site repo), so\n * `truncated` says the answer is older than we looked, not that it is bad;\n * - on a repo that has genuinely never merged one there is no elapsed time to\n * measure at all, and calling that a drought would fire forever on every\n * freshly bootstrapped repo.\n * Both print every night with their reason. \"I could not measure this\" must\n * never render as \"this is fine\" — that silent-green reading is the failure\n * this whole sweep exists to kill.\n */\nexport function renovateOutcome(window: RenovateMergeWindow, now: Date): RenovateOutcome {\n const last = window.merges.filter((m) => isFeatureUpdateBranch(m.headRef))[0];\n if (!last) {\n return {\n state: \"unmeasured\",\n reason: window.truncated\n ? \"no feature-update Renovate merge inside the scanned window, and the window is FULL \" +\n \"(older history not read — this repo merges too many PRs to see back that far)\"\n : \"this repo has never merged a feature-update Renovate PR (no baseline to measure from)\",\n };\n }\n const days = Math.floor((now.getTime() - new Date(last.mergedAt).getTime()) / 86_400_000);\n return {\n state: days > RENOVATE_DROUGHT_WARN_DAYS ? \"drought\" : \"delivering\",\n days,\n lastBranch: last.headRef,\n lastMergedAt: last.mergedAt,\n };\n}\n\n/** One operator-facing line per measured repo, or `null` when the outcome is\n * healthy and needs no line. Warnings and unmeasured reads are reported\n * side by side because they are the same claim — \"no update landed here\" —\n * differing only in whether we know why. */\nexport function renovateOutcomeLine(outcome: RenovateOutcome): string | null {\n if (outcome.state === \"delivering\") return null;\n if (outcome.state === \"unmeasured\") return `renovate outcome UNMEASURED — ${outcome.reason}`;\n return (\n `no feature update has LANDED in ${outcome.days}d ` +\n `(warns past ${RENOVATE_DROUGHT_WARN_DAYS}d; last was ${outcome.lastBranch} ` +\n `merged ${outcome.lastMergedAt.slice(0, 10)}). Security + lockfile Renovate PRs are ` +\n `excluded, so this is feature-version drift, not a security signal.`\n );\n}\n\n/** The machine-readable outcome line, in the same shape as PROTECTION_AUDIT.\n * Deliberately a SEPARATE line with a separate prefix: the nightly workflow\n * gates its tracking issue on `PROTECTION_AUDIT gaps=0 `, and this metric must\n * be able to say \"nothing is landing\" for a month without opening, holding\n * open, or blocking the close of a posture issue. Shared with the tests so the\n * proof asserts the product's real output rather than a restatement of it. */\nexport function renovateOutcomeSummary(outcomes: RenovateOutcome[]): string {\n const n = (state: RenovateOutcome[\"state\"]) => outcomes.filter((o) => o.state === state).length;\n return (\n `RENOVATE_OUTCOME drought=${n(\"drought\")} delivering=${n(\"delivering\")} ` +\n `unmeasured=${n(\"unmeasured\")} threshold=${RENOVATE_DROUGHT_WARN_DAYS}d`\n );\n}\n\n/** How long a human-owned blocked branch may sit before it stops reading as\n * in-flight work. Sized to clear a weekend plus slack: the fleet's own\n * human-edited Renovate PRs merge in minutes, while the branches that froze\n * nine repos sat for a week. */\nexport const BLOCKED_STALE_DAYS = 5;\n\nexport type BlockedBranchState = { branch: string; orphaned: boolean };\n\n/**\n * Decide which blocked branches are actually a posture problem.\n *\n * `PR Edited (Blocked)` is not by itself a fault — it is also Renovate's\n * designed way of saying \"a human owns this branch now\", and pushing a commit\n * onto an open Renovate PR is routine fleet practice (16 such PRs across 14\n * repos on a single day). Alarming on those would fire the nightly issue for\n * perfectly healthy in-flight work, and would attach a remediation that says\n * to delete the branch — i.e. the human's commits.\n *\n * A branch counts as ORPHANED, and therefore a gap, when either:\n * - its tip was authored by a machine, so no human owns it and nothing will\n * ever clear it on its own; or\n * - it has sat untouched past BLOCKED_STALE_DAYS, whoever authored it —\n * because Renovate updates a branch it is managing, so tip age IS time\n * spent frozen.\n *\n * The authorship test is the fast path and the staleness test is the backstop:\n * if machine-identity detection ever drifts, detection degrades to slower, not\n * to blind. A branch that no longer exists is not a gap — that is a dashboard\n * Renovate has not rewritten yet.\n */\nexport async function resolveBlockedBranches(\n repo: string,\n dash: DependencyDashboard,\n deps: Pick<ProtectionCoverageDeps, \"branchTip\">,\n now: Date,\n): Promise<BlockedBranchState[]> {\n if (!dash.present) return [];\n const states: BlockedBranchState[] = [];\n for (const branch of dash.blockedBranches) {\n const tip = await deps.branchTip(repo, branch);\n if (!tip) continue;\n const ageDays = (now.getTime() - new Date(tip.committedAt).getTime()) / 86_400_000;\n states.push({ branch, orphaned: tip.authorIsMachine || ageDays > BLOCKED_STALE_DAYS });\n }\n return states;\n}\n\n/**\n * One row per org repo. Coverage = SOME repo-sourced ruleset with zero\n * stage-1 gaps (active, empty bypass, default branch covered, deletion +\n * non_fast_forward + pull_request) AND secret scanning enabled AND a live\n * renovate workflow. Whether a covering ruleset also gates on CI is reported\n * as detail, not judged: each repo's required context differs (and\n * reddoor-maintenance deliberately has none pending release-path review), so\n * the CI-gate invariant belongs to the per-site heal, which has the evidence\n * to require the RIGHT context safely.\n *\n * Private/archived repos are skipped (rulesets on private repos need a paid\n * plan; archived repos are read-only). A repo whose probe THROWS is a `gap`,\n * not a skip — \"couldn't verify\" reading as \"fine\" is the silent-green\n * failure mode this sweep exists to kill, and the tracking issue auto-closes\n * on the next clean pass, so a transient API blip costs one issue-comment.\n */\nexport async function collectProtectionCoverage(\n org: string,\n deps: ProtectionCoverageDeps,\n now: Date = new Date(),\n accepted: AcceptedGap[] = ACCEPTED_GAPS,\n): Promise<ProtectionCoverageRow[]> {\n const rows: ProtectionCoverageRow[] = [];\n for (const r of await deps.listOrgRepos(org)) {\n const repo = `${org}/${r.name}`;\n if (r.archived || r.visibility !== \"public\") {\n rows.push({\n repo,\n status: \"skipped\",\n detail: r.archived ? \"archived\" : `${r.visibility} (rulesets need a paid plan)`,\n });\n continue;\n }\n try {\n const gaps: string[] = [];\n let coveredDetail = \"\";\n const rulesets = await deps.listRepoRulesets(repo);\n if (rulesets.length === 0) {\n gaps.push(\"no repo rulesets at all\");\n } else {\n const judged = await Promise.all(\n rulesets.map(async (rs) => {\n const full = await deps.getRuleset(repo, rs.id);\n return { name: rs.name, full, gaps: rulesetGaps(full, null) };\n }),\n );\n const covering = judged.find((j) => j.gaps.length === 0);\n if (covering) {\n const ciGated = (covering.full.rules ?? []).some(\n (rule) => rule.type === \"required_status_checks\",\n );\n coveredDetail = `\"${covering.name}\"${ciGated ? \"\" : \" — NO CI gate (refs rules only)\"}`;\n } else {\n gaps.push(judged.map((j) => `\"${j.name}\": ${j.gaps.join(\"; \")}`).join(\" | \"));\n }\n }\n // Only ask for alerts where scanning is on — see secretScanningGaps for\n // why a 404-by-construction is not worth a second gap line.\n const openAlerts =\n r.secretScanning === \"enabled\" ? await deps.openSecretAlerts(repo) : \"unavailable\";\n gaps.push(...secretScanningGaps(repo, r, openAlerts));\n gaps.push(...renovateGaps(await deps.workflowHealth(repo, RENOVATE_WORKFLOW_FILE), now));\n const dash = await deps.dependencyDashboard(repo);\n gaps.push(...dashboardVocabularyGaps(dash));\n gaps.push(...renovateBlockedGaps(await resolveBlockedBranches(repo, dash, deps, now)));\n // MEASUREMENT, not a gap: never pushed into `gaps`, so it cannot change\n // the exit code, the PROTECTION_AUDIT counts, or the tracking issue.\n const outcome = renovateOutcome(await deps.renovateMergeWindow(repo), now);\n const { live, accepted: acked } = partitionAcceptedGaps(repo, gaps, now, accepted);\n if (live.length > 0) {\n // Accepted annotations still ride along for context, but only LIVE\n // gaps make the row a gap (and thus the sweep exit 1).\n rows.push({\n repo,\n status: \"gap\",\n detail: [...live, ...acked].join(\" | \"),\n renovateOutcome: outcome,\n });\n } else if (acked.length > 0) {\n // Judged, found wanting, deliberately acked — reported as skipped\n // (visible every night with the reason + expiry), never as covered.\n rows.push({\n repo,\n status: \"skipped\",\n detail: acked.join(\" | \"),\n renovateOutcome: outcome,\n });\n } else {\n rows.push({\n repo,\n status: \"covered\",\n detail: coveredDetail,\n renovateOutcome: outcome,\n });\n }\n } catch (e) {\n rows.push({\n repo,\n status: \"gap\",\n detail: `probe failed: ${e instanceof Error ? e.message : String(e)}`,\n });\n }\n }\n return rows;\n}\n","import { partitionAcceptedGaps, type AcceptedGap } from \"../audits/protection-coverage.js\";\n\n/**\n * The pnpm `packageManager` surface of the nightly protection sweep (#690).\n *\n * Three repos sat on a pnpm security bump for two months and nothing said so.\n * The drift itself is fixed — re-measured against live GitHub 2026-09-15, all\n * 25 repos carrying a `packageManager` field are on `pnpm@11.11.0` — but the\n * reason it went\n * unseen is not: NOTHING in `src/` or `.github/workflows/` ever asked the\n * question. This module is that question, wired into the sweep that already\n * walks every repo (see `runProtectionAuditCommand`), because a fleet\n * invariant with no instrument is a fleet invariant that drifts again.\n *\n * Three gaps, each with its own failure history:\n *\n * - **No `packageManager` field** where a `package.json` exists: CI then\n * installs whatever pnpm the runner happens to default to, so the version\n * that resolves the lockfile is decided by GitHub's image, not by the repo.\n * - **A pin that disagrees with the fleet's**: the original #690 condition.\n * - **A workflow pinning a pnpm `version:` input while a `packageManager`\n * field also exists**: `pnpm/action-setup` does not prefer one over the\n * other, it HARD-ERRORS on a mismatch. As of 2026-09-15 NO repo in the\n * fleet pins a workflow input — all 75 workflow files were parsed by\n * `parsePnpmActionSetupPins` and none carries one. `reddoor-md-pdf` used to\n * be the single exception, safe only because it had no field to disagree\n * with; it has since gained the field AND dropped the input, which is the\n * correct shape. This clause is now a pure regression guard, so its failing\n * arm is injected in the test rather than borrowed from a live repo.\n *\n * Scope is deliberately the SAME population the coverage sweep judges —\n * non-archived PUBLIC repos. Not an oversight, and not because private repos\n * cannot drift: `fleet-security.yml` closes the tracking issue only when every\n * repo the issue named as a `GAP` comes back as `COVERED` in a later run, and\n * the coverage sweep never prints `COVERED` for a repo it skips. A gap raised\n * here on a repo that can only ever print `SKIPPED` would be an issue the\n * machine could never close — the exact orphan-issue failure that had to be\n * cleaned up by hand on 2026-09-14.\n */\n\n/** A repo's raw pnpm-pin facts, as read from its default branch. */\nexport type PackageManagerFacts = {\n /** `package.json` at the default branch, or `null` when the repo has none —\n * which is OUT OF SCOPE, never a gap. In the judged population that is\n * `.github` alone (measured 2026-09-15); `reddoor-prospect-runner` and\n * `reddoor-rfp-analyses` are also package-less but are PRIVATE, so they are\n * skipped before this is ever consulted. A check that gapped them would\n * light repos every night forever and teach the operator to skim past this\n * surface. */\n packageJson: string | null;\n /** Every `pnpm/action-setup` step that pins a `version:` input, by workflow\n * file. Empty is the healthy fleet-wide default. */\n workflowPins: Array<{ file: string; version: string }>;\n};\n\n/** The reads this sweep needs, injected so the verdicts stay pure and\n * fixture-testable (same pattern as ProtectionCoverageDeps). */\nexport type PackageManagerPinDeps = {\n listOrgRepos: (org: string) => Promise<\n Array<{\n name: string;\n visibility: string;\n archived: boolean;\n secretScanning: string;\n pushProtection: string;\n }>\n >;\n /** A text file at the repo's default branch, or `null` when it does not\n * exist. `null` is an ANSWER (no package.json = not a node repo); any other\n * failure throws, so an API hiccup can never arrive here as \"no pin\". */\n repoTextFile: (repo: string, path: string) => Promise<string | null>;\n /** Paths of every file in `.github/workflows`, or `[]` when the directory is\n * absent. */\n listWorkflowPaths: (repo: string) => Promise<string[]>;\n};\n\n/**\n * Operator-accepted gaps, in the shape and under the doctrine of\n * `ACCEPTED_GAPS`: one repo, one surface, a NAMED pending fix, and an expiry\n * past which the gap returns on its own.\n *\n * Currently EMPTY, and that is the healthy state. The two original entries —\n * `claude-skills` and `reddoor-md-pdf`, the two repos the 2026-09-16\n * measurement found carrying no `packageManager` field — were retired\n * 2026-09-15 when the fix they named actually landed: both now carry\n * `pnpm@11.11.0`, and `reddoor-md-pdf`'s `pnpm/action-setup` step no longer\n * takes a `version:` input at all (verified against live GitHub, not inferred).\n *\n * An acceptance outliving its fix is not inert. It is scoped by repo AND\n * surface, so for as long as it stands these two repos are the only two in the\n * fleet whose \"no packageManager field\" gap CANNOT gate — precisely the repos\n * that just proved they are the ones that lose the field. Had either regressed\n * before 2026-10-01 the nightly would have reported it as accepted-with-expiry\n * and gated nothing. A mute that survives its reason silences exactly the\n * repo it was written about.\n */\nexport const PACKAGE_MANAGER_ACCEPTED_GAPS: AcceptedGap[] = [];\n\n/** What a repo's `package.json` says about pnpm. The three states are distinct\n * shapes on purpose: a caller cannot read \"I could not parse this\" as \"no pin\"\n * by forgetting to check a flag. */\nexport type PackageManagerField =\n | { state: \"present\"; value: string }\n | { state: \"absent\" }\n | { state: \"unreadable\"; reason: string };\n\n/** The `packageManager` field of one `package.json`. */\nexport function parsePackageManagerField(packageJson: string): PackageManagerField {\n let parsed: unknown;\n try {\n parsed = JSON.parse(packageJson);\n } catch (e) {\n return {\n state: \"unreadable\",\n reason: `package.json is not valid JSON (${e instanceof Error ? e.message : String(e)})`,\n };\n }\n if (typeof parsed !== \"object\" || parsed === null) {\n return { state: \"unreadable\", reason: \"package.json is not a JSON object\" };\n }\n const value = (parsed as Record<string, unknown>).packageManager;\n if (value === undefined) return { state: \"absent\" };\n if (typeof value !== \"string\") {\n return { state: \"unreadable\", reason: `packageManager is ${typeof value}, not a string` };\n }\n return { state: \"present\", value };\n}\n\n/**\n * Every pnpm version pinned by a `pnpm/action-setup` step in one workflow file.\n *\n * THE TRAP THIS FUNCTION EXISTS FOR. The first hand-run measurement of this\n * question grepped `version:` anywhere in any file that mentioned\n * `pnpm/action-setup`, and reported ELEVEN healthy workflows as pinning a pnpm\n * version. Every one of those matches was `node-version: 24`, from an\n * `actions/setup-node` step elsewhere in the same file. Posted, it would have\n * sent someone editing eleven workflows that were already correct.\n *\n * So the scan is bounded to the action-setup step's OWN block, with two\n * independent defenses against that exact miss:\n * 1. the key regex is anchored at the start of the key, so `node-version:`\n * cannot match `version:`; and\n * 2. the scan stops at the end of the step, so a sibling step's `with:`\n * block is never entered at all.\n * Either alone would have caught it. Both are here because this surface has\n * already produced a confidently wrong number once.\n *\n * Parsed by a targeted scan rather than a YAML dependency, following\n * `readLockedCliVersion`: this needs one input out of one step, and the repo\n * deliberately carries no YAML parser.\n */\nexport function parsePnpmActionSetupPins(workflow: string): string[] {\n const lines = workflow.split(\"\\n\");\n const pins: string[] = [];\n\n const indentOf = (line: string): number => line.length - line.trimStart().length;\n const isBlank = (line: string): boolean => line.trim().length === 0 || /^\\s*#/.test(line);\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!;\n const uses = /^(\\s*)(-\\s+)?uses:\\s*['\"]?pnpm\\/action-setup(?=[@'\"\\s]|$)/.exec(line);\n if (!uses) continue;\n // A step written as `- uses: …` puts the step's other keys at the column\n // AFTER the dash; one written as `uses:` under an earlier `- name:` puts\n // them at its own column.\n const contentIndent = uses[1]!.length + (uses[2] ? uses[2].length : 0);\n\n for (let j = i + 1; j < lines.length; j++) {\n const body = lines[j]!;\n if (isBlank(body)) continue;\n const indent = indentOf(body);\n // Dedent past the step's keys, or a new `- ` item at the same column:\n // either way this step is over and nothing below belongs to it.\n if (indent < contentIndent) break;\n if (indent === contentIndent && /^\\s*-\\s/.test(body)) break;\n if (indent !== contentIndent || !/^\\s*with:\\s*$/.test(body)) continue;\n\n // Inside this step's `with:` mapping — and only this one.\n for (let k = j + 1; k < lines.length; k++) {\n const input = lines[k]!;\n if (isBlank(input)) continue;\n if (indentOf(input) <= contentIndent) break;\n const m = /^\\s*version:\\s*['\"]?([^'\"#\\s]+)/.exec(input);\n if (m) pins.push(m[1]!);\n }\n break;\n }\n }\n return pins;\n}\n\n/** The fleet's pnpm pin, DERIVED from the fleet rather than hand-typed here.\n * A constant in this file would be one more copy of the number to drift —\n * `convert-to-pnpm.ts` carried `DEFAULT_PNPM_VERSION = \"10.33.1\"` for about a\n * year while the fleet ran `11.11.0`, which is exactly what a hand-typed pin\n * does when nothing watches it. That constant is corrected and now has a test\n * watching it (#835); this value is derived, so it cannot need one. */\nexport type FleetPin =\n | { state: \"majority\"; pin: string; count: number; total: number }\n | { state: \"split\"; counts: Array<{ pin: string; count: number }>; total: number }\n | { state: \"none\" };\n\n/**\n * The pin a strict majority of pinned repos agree on.\n *\n * A STRICT majority, not merely the most common value: with no majority the\n * fleet has no single answer, and picking the modal value would gap every repo\n * on the other side of a genuine split — 24 GAP lines for one unmade decision.\n * `split` reports the condition and gaps nobody on this clause; the missing-\n * field and workflow-collision clauses keep working underneath it.\n */\nexport function fleetPin(pins: string[]): FleetPin {\n if (pins.length === 0) return { state: \"none\" };\n const tally = new Map<string, number>();\n for (const p of pins) tally.set(p, (tally.get(p) ?? 0) + 1);\n const counts = [...tally.entries()]\n .map(([pin, count]) => ({ pin, count }))\n .sort((a, b) => b.count - a.count || a.pin.localeCompare(b.pin));\n const top = counts[0]!;\n if (top.count * 2 > pins.length) {\n return { state: \"majority\", pin: top.pin, count: top.count, total: pins.length };\n }\n return { state: \"split\", counts, total: pins.length };\n}\n\n/** Gap lines for one in-scope repo. Empty = this repo's pnpm pin is sound. */\nexport function packageManagerGaps(facts: PackageManagerFacts, fleet: FleetPin): string[] {\n if (facts.packageJson === null) return [];\n const field = parsePackageManagerField(facts.packageJson);\n const gaps: string[] = [];\n const pinnedWorkflows = facts.workflowPins\n .map((w) => `${w.file} (version: ${w.version})`)\n .join(\", \");\n\n if (field.state === \"unreadable\") {\n gaps.push(`packageManager unreadable — ${field.reason} (unverified, not clean)`);\n return gaps;\n }\n\n if (field.state === \"absent\") {\n const fleetSays =\n fleet.state === \"majority\" ? ` (the fleet pin is ${fleet.pin})` : \" (fleet pin unsettled)\";\n // The remediation DIFFERS when a workflow already pins a version, and\n // getting this wrong breaks the repo it is trying to fix: adding the field\n // next to a surviving `version:` input is precisely the mismatch\n // `pnpm/action-setup` hard-errors on.\n gaps.push(\n facts.workflowPins.length > 0\n ? `no packageManager field in package.json${fleetSays}, and ${pinnedWorkflows} pins pnpm ` +\n `in a pnpm/action-setup step instead. Fix: add the field AND delete the workflow's ` +\n `version input in the SAME change — adding the field alone makes action-setup ` +\n `hard-error on the mismatch.`\n : `no packageManager field in package.json${fleetSays} — CI installs whatever pnpm the ` +\n `runner defaults to, so the version resolving this lockfile is GitHub's choice, not ` +\n `the repo's. Fix: add \"packageManager\" to package.json.`,\n );\n return gaps;\n }\n\n if (fleet.state === \"majority\" && field.value !== fleet.pin) {\n gaps.push(\n `packageManager is ${field.value} but the fleet pin is ${fleet.pin} ` +\n `(${fleet.count}/${fleet.total} repos agree). Fix: bump the field, or say why this repo ` +\n `is deliberately apart.`,\n );\n }\n\n if (facts.workflowPins.length > 0) {\n const agree = facts.workflowPins.every((w) => `pnpm@${w.version}` === field.value);\n gaps.push(\n `${pinnedWorkflows} pins a pnpm version input while package.json pins ${field.value}` +\n (agree ? \" — they agree today, but nothing keeps them in step\" : \" — THEY DISAGREE\") +\n `. pnpm/action-setup does not prefer either side, it hard-errors on a mismatch. ` +\n `Fix: delete the workflow's version input; packageManager is the single source of truth.`,\n );\n }\n\n return gaps;\n}\n\n/** One row per repo the sweep looked at. */\nexport type PackageManagerRow = {\n repo: string; // owner/repo\n scope: \"judged\" | \"out-of-scope\" | \"skipped\";\n /** Live gaps — these gate. Empty on a sound repo. */\n gaps: string[];\n /** Accepted-for-now gaps: reported every night with reason + expiry, never\n * gating (see PACKAGE_MANAGER_ACCEPTED_GAPS). */\n accepted: string[];\n detail: string;\n};\n\n/**\n * The pnpm-pin sweep over one org.\n *\n * Two passes, and the order matters: the fleet pin is DERIVED from what the\n * repos actually say, so every repo's facts must be read before any repo can\n * be judged against them.\n */\nexport async function collectPackageManagerPins(\n org: string,\n deps: PackageManagerPinDeps,\n now: Date = new Date(),\n accepted: AcceptedGap[] = PACKAGE_MANAGER_ACCEPTED_GAPS,\n): Promise<PackageManagerRow[]> {\n const rows: PackageManagerRow[] = [];\n const facts = new Map<string, PackageManagerFacts>();\n\n for (const r of await deps.listOrgRepos(org)) {\n const repo = `${org}/${r.name}`;\n if (r.archived || r.visibility !== \"public\") {\n rows.push({\n repo,\n scope: \"skipped\",\n gaps: [],\n accepted: [],\n detail: r.archived ? \"archived\" : `${r.visibility}`,\n });\n continue;\n }\n try {\n const packageJson = await deps.repoTextFile(repo, \"package.json\");\n if (packageJson === null) {\n // No package.json = no pnpm pin to hold. OUT OF SCOPE, never a gap.\n rows.push({\n repo,\n scope: \"out-of-scope\",\n gaps: [],\n accepted: [],\n detail: \"no package.json\",\n });\n continue;\n }\n const workflowPins: PackageManagerFacts[\"workflowPins\"] = [];\n for (const path of await deps.listWorkflowPaths(repo)) {\n const contents = await deps.repoTextFile(repo, path);\n if (contents === null) continue;\n for (const version of parsePnpmActionSetupPins(contents)) {\n workflowPins.push({ file: path.replace(/^\\.github\\/workflows\\//, \"\"), version });\n }\n }\n facts.set(repo, { packageJson, workflowPins });\n rows.push({ repo, scope: \"judged\", gaps: [], accepted: [], detail: \"\" });\n } catch (e) {\n // \"Couldn't read it\" is a gap, never a pass — the same doctrine the\n // coverage sweep applies to a probe that throws.\n rows.push({\n repo,\n scope: \"judged\",\n gaps: [`pnpm pin probe failed: ${e instanceof Error ? e.message : String(e)}`],\n accepted: [],\n detail: \"probe failed\",\n });\n }\n }\n\n const pinned: string[] = [];\n for (const f of facts.values()) {\n const field = parsePackageManagerField(f.packageJson!);\n if (field.state === \"present\") pinned.push(field.value);\n }\n const fleet = fleetPin(pinned);\n\n return rows.map((row) => {\n const f = facts.get(row.repo);\n if (!f) return row;\n const all = packageManagerGaps(f, fleet);\n const split = partitionAcceptedGaps(row.repo, all, now, accepted);\n const field = parsePackageManagerField(f.packageJson!);\n return {\n ...row,\n gaps: split.live,\n accepted: split.accepted,\n detail: field.state === \"present\" ? field.value : row.detail,\n };\n });\n}\n\n/** The machine-readable summary, in the shape of RENOVATE_OUTCOME: its own\n * prefix, so it can never be mistaken for a `GAP`/`COVERED` line or for the\n * `PROTECTION_AUDIT gaps=0 ` string the nightly gates its issue on. */\nexport function packageManagerPinSummary(rows: PackageManagerRow[]): string {\n const judged = rows.filter((r) => r.scope === \"judged\");\n const pins = judged.map((r) => r.detail).filter((d) => d.startsWith(\"pnpm@\"));\n const fleet = fleetPin(pins);\n const pin =\n fleet.state === \"majority\"\n ? fleet.pin\n : fleet.state === \"split\"\n ? `SPLIT(${fleet.counts.map((c) => `${c.pin}x${c.count}`).join(\",\")})`\n : \"none\";\n return (\n `PACKAGE_MANAGER_PIN gaps=${judged.filter((r) => r.gaps.length > 0).length} ` +\n `pinned=${pins.length} judged=${judged.length} ` +\n `out-of-scope=${rows.filter((r) => r.scope === \"out-of-scope\").length} ` +\n `skipped=${rows.filter((r) => r.scope === \"skipped\").length} fleetPin=${pin}`\n );\n}\n","import {\n collectProtectionCoverage,\n renovateOutcomeLine,\n renovateOutcomeSummary,\n type ProtectionCoverageDeps,\n} from \"../../audits/protection-coverage.js\";\nimport {\n collectPackageManagerPins,\n packageManagerPinSummary,\n type PackageManagerPinDeps,\n} from \"../../github/package-manager-pin.js\";\nimport { makeGitHub } from \"../../github/gh.js\";\n\n/** Everything the audit reads. The pnpm-pin sweep's reads are REQUIRED, not\n * optional: an optional dep that tests forget to supply is a check that\n * silently measures nothing, which is the failure this guard was built to\n * end. Widening the type makes the compiler name every call site instead. */\nexport type ProtectionAuditDeps = ProtectionCoverageDeps & PackageManagerPinDeps;\n\n/** Exit 1 on ANY gap: unlike the fleet write sweeps (majority-rule), a single\n * unprotected public repo is a real hole, not a flake to tolerate. */\nexport function protectionAuditExitCode(gaps: number): number {\n return gaps > 0 ? 1 : 0;\n}\n\n/** `protection-audit --org <org>`: verify every public repo in the org carries\n * a sound branch ruleset, live secret scanning + push protection, a renovate\n * workflow that actually runs (see collectProtectionCoverage for the\n * verdicts), and a pnpm `packageManager` pin that matches the fleet's (see\n * collectPackageManagerPins). Emits one line per repo + a machine-readable\n * PROTECTION_AUDIT summary the nightly workflow gates its tracking issue on.\n * A missing fleet token is a clean skip (local runs), matching github-signals. */\nexport async function runProtectionAuditCommand(\n opts: { org?: string | undefined },\n // Injected in tests; defaults to the real gh-CLI-backed factory.\n depsOverride?: ProtectionAuditDeps,\n): Promise<{ output: string; code: number }> {\n const org = opts.org?.trim();\n if (!org) {\n return { output: \"protection-audit requires --org <org>\", code: 2 };\n }\n const token = process.env.GH_TOKEN?.trim();\n if (!depsOverride && !token) {\n return {\n output: \"protection-audit skipped: no GH_TOKEN (fleet read) configured.\",\n code: 0,\n };\n }\n const deps = depsOverride ?? makeGitHub({ token: token! });\n\n const coverage = await collectProtectionCoverage(org, deps);\n const pinRows = await collectPackageManagerPins(org, deps);\n const pinByRepo = new Map(pinRows.map((p) => [p.repo, p]));\n\n // MERGED INTO THE REPO'S OWN ROW, never appended as extra GAP lines of their\n // own. `fleet-security.yml` closes the tracking issue only when every repo\n // the issue named as a GAP comes back COVERED, so a repo printing GAP and\n // COVERED in the same sweep would let it close on its own gap.\n const rows = coverage.map((row) => {\n const pin = pinByRepo.get(row.repo);\n if (!pin || pin.gaps.length === 0) return row;\n return {\n ...row,\n status: \"gap\" as const,\n detail: [row.detail, ...pin.gaps].filter((d) => d.length > 0).join(\" | \"),\n };\n });\n\n const gaps = rows.filter((r) => r.status === \"gap\");\n const covered = rows.filter((r) => r.status === \"covered\");\n const skipped = rows.filter((r) => r.status === \"skipped\");\n\n const lines = rows.map((r) => `${r.status.toUpperCase().padEnd(7)} ${r.repo} — ${r.detail}`);\n\n // The outcome metric rides in its OWN line prefixes and its OWN summary line.\n // The nightly workflow greps `^GAP` for the issue body, `^COVERED` for the\n // close list, and `PROTECTION_AUDIT gaps=0 ` to decide a clean sweep — none\n // of which these lines match, so the tracking-issue plumbing is untouched by\n // construction rather than by promise.\n const measured = rows.map((r) => r.renovateOutcome).filter((o) => o !== undefined);\n for (const r of rows) {\n const line = r.renovateOutcome && renovateOutcomeLine(r.renovateOutcome);\n if (line) lines.push(`WARN ${r.repo} — ${line}`);\n }\n // Accepted pnpm-pin gaps print every night with their reason and expiry, on\n // the same non-gating WARN prefix: visible, never silent, never an alarm.\n for (const p of pinRows) {\n for (const note of p.accepted) lines.push(`WARN ${p.repo} — ${note}`);\n }\n lines.push(renovateOutcomeSummary(measured));\n lines.push(packageManagerPinSummary(pinRows));\n lines.push(\n `PROTECTION_AUDIT gaps=${gaps.length} covered=${covered.length} skipped=${skipped.length} total=${rows.length}`,\n );\n return { output: lines.join(\"\\n\"), code: protectionAuditExitCode(gaps.length) };\n}\n"],"mappings":";;;;;;;;;AA4EO,IAAM,yBAAyB;AAM/B,IAAM,4BAA4B;AAwBlC,IAAM,gBAA+B,CAAC;AAItC,SAAS,sBACd,MACA,MACA,KACA,WAA0B,eACc;AACxC,QAAM,OAAiB,CAAC;AACxB,QAAM,cAAwB,CAAC;AAC/B,aAAW,OAAO,MAAM;AACtB,UAAM,QAAQ,SAAS;AAAA,MACrB,CAAC,MAAM,EAAE,SAAS,QAAQ,IAAI,WAAW,EAAE,YAAY,KAAK,MAAM,IAAI,KAAK,EAAE,KAAK;AAAA,IACpF;AACA,QAAI,MAAO,aAAY,KAAK,GAAG,GAAG,oBAAoB,MAAM,KAAK,KAAK,MAAM,MAAM,GAAG;AAAA,QAChF,MAAK,KAAK,GAAG;AAAA,EACpB;AACA,SAAO,EAAE,MAAM,UAAU,YAAY;AACvC;AA6BO,SAAS,mBACd,MACA,OACA,YACU;AACV,QAAM,OAAiB,CAAC;AACxB,MAAI,MAAM,mBAAmB;AAC3B,SAAK,KAAK,mBAAmB,MAAM,cAAc,2BAA2B;AAC9E,MAAI,MAAM,mBAAmB;AAC3B,SAAK,KAAK,mBAAmB,MAAM,cAAc,2BAA2B;AAC9E,MAAI,MAAM,mBAAmB,WAAW;AACtC,QAAI,eAAe;AACjB,WAAK;AAAA,QACH,6CAA6C,IAAI;AAAA,MAEnD;AAAA,aACO,aAAa;AACpB,WAAK;AAAA,QACH,GAAG,UAAU,8BAA8B,eAAe,IAAI,KAAK,GAAG,OAAO,IAAI,oJAE1B,IAAI;AAAA,MAE7D;AAAA,EACJ;AACA,SAAO;AACT;AAMO,SAAS,aAAa,QAAwB,KAAqB;AACxE,MAAI,CAAC,OAAO,QAAS,QAAO,CAAC,0DAA0D;AACvF,MAAI,OAAO,UAAU;AACnB,WAAO;AAAA;AAAA;AAAA,MAGL,wBAAwB,OAAO,KAAK;AAAA,IACtC;AACF,MAAI,OAAO,kBAAkB;AAC3B,WAAO;AAAA,MACL;AAAA,IACF;AACF,QAAM,cAAc,KAAK,MAAM,OAAO,aAAa;AACnD,MAAI,OAAO,MAAM,WAAW;AAC1B,WAAO,CAAC,+CAA+C,OAAO,aAAa,EAAE;AAC/E,QAAM,QAAQ,IAAI,QAAQ,IAAI,eAAe;AAC7C,MAAI,OAAO;AACT,WAAO,CAAC,2BAA2B,KAAK,MAAM,IAAI,CAAC,6BAA6B;AAClF,SAAO,CAAC;AACV;AA6BO,SAAS,oBAAoB,SAAyC;AAC3E,QAAM,WAAW,QAAQ,OAAO,CAAC,MAAM,EAAE,QAAQ;AACjD,MAAI,SAAS,WAAW,EAAG,QAAO,CAAC;AACnC,SAAO;AAAA,IACL,0BAA0B,SAAS,MAAM,sBACpC,SAAS,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;AAAA,EAK/C;AACF;AAaO,SAAS,wBAAwB,MAAqC;AAC3E,MAAI,CAAC,KAAK,WAAW,KAAK,gBAAgB,WAAW,EAAG,QAAO,CAAC;AAChE,SAAO;AAAA,IACL,0BAA0B,KAAK,gBAAgB,MAAM,oDAChC,KAAK,gBAAgB,KAAK,IAAI,CAAC;AAAA,EAKtD;AACF;AAoCO,SAAS,sBAAsB,SAA0B;AAC9D,MAAI,CAAC,QAAQ,WAAW,WAAW,EAAG,QAAO;AAC7C,MAAI,YAAY,iCAAkC,QAAO;AACzD,SAAO,CAAC,mCAAmC,KAAK,OAAO;AACzD;AAoBO,IAAM,6BAA6B;AA0BnC,SAAS,gBAAgB,QAA6B,KAA4B;AACvF,QAAM,OAAO,OAAO,OAAO,OAAO,CAAC,MAAM,sBAAsB,EAAE,OAAO,CAAC,EAAE,CAAC;AAC5E,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,MACL,OAAO;AAAA,MACP,QAAQ,OAAO,YACX,0KAEA;AAAA,IACN;AAAA,EACF;AACA,QAAM,OAAO,KAAK,OAAO,IAAI,QAAQ,IAAI,IAAI,KAAK,KAAK,QAAQ,EAAE,QAAQ,KAAK,KAAU;AACxF,SAAO;AAAA,IACL,OAAO,OAAO,6BAA6B,YAAY;AAAA,IACvD;AAAA,IACA,YAAY,KAAK;AAAA,IACjB,cAAc,KAAK;AAAA,EACrB;AACF;AAMO,SAAS,oBAAoB,SAAyC;AAC3E,MAAI,QAAQ,UAAU,aAAc,QAAO;AAC3C,MAAI,QAAQ,UAAU,aAAc,QAAO,sCAAiC,QAAQ,MAAM;AAC1F,SACE,mCAAmC,QAAQ,IAAI,iBAChC,0BAA0B,eAAe,QAAQ,UAAU,WAChE,QAAQ,aAAa,MAAM,GAAG,EAAE,CAAC;AAG/C;AAQO,SAAS,uBAAuB,UAAqC;AAC1E,QAAM,IAAI,CAAC,UAAoC,SAAS,OAAO,CAAC,MAAM,EAAE,UAAU,KAAK,EAAE;AACzF,SACE,4BAA4B,EAAE,SAAS,CAAC,eAAe,EAAE,YAAY,CAAC,eACxD,EAAE,YAAY,CAAC,cAAc,0BAA0B;AAEzE;AAMO,IAAM,qBAAqB;AA0BlC,eAAsB,uBACpB,MACA,MACA,MACA,KAC+B;AAC/B,MAAI,CAAC,KAAK,QAAS,QAAO,CAAC;AAC3B,QAAM,SAA+B,CAAC;AACtC,aAAW,UAAU,KAAK,iBAAiB;AACzC,UAAM,MAAM,MAAM,KAAK,UAAU,MAAM,MAAM;AAC7C,QAAI,CAAC,IAAK;AACV,UAAM,WAAW,IAAI,QAAQ,IAAI,IAAI,KAAK,IAAI,WAAW,EAAE,QAAQ,KAAK;AACxE,WAAO,KAAK,EAAE,QAAQ,UAAU,IAAI,mBAAmB,UAAU,mBAAmB,CAAC;AAAA,EACvF;AACA,SAAO;AACT;AAkBA,eAAsB,0BACpB,KACA,MACA,MAAY,oBAAI,KAAK,GACrB,WAA0B,eACQ;AAClC,QAAM,OAAgC,CAAC;AACvC,aAAW,KAAK,MAAM,KAAK,aAAa,GAAG,GAAG;AAC5C,UAAM,OAAO,GAAG,GAAG,IAAI,EAAE,IAAI;AAC7B,QAAI,EAAE,YAAY,EAAE,eAAe,UAAU;AAC3C,WAAK,KAAK;AAAA,QACR;AAAA,QACA,QAAQ;AAAA,QACR,QAAQ,EAAE,WAAW,aAAa,GAAG,EAAE,UAAU;AAAA,MACnD,CAAC;AACD;AAAA,IACF;AACA,QAAI;AACF,YAAM,OAAiB,CAAC;AACxB,UAAI,gBAAgB;AACpB,YAAM,WAAW,MAAM,KAAK,iBAAiB,IAAI;AACjD,UAAI,SAAS,WAAW,GAAG;AACzB,aAAK,KAAK,yBAAyB;AAAA,MACrC,OAAO;AACL,cAAM,SAAS,MAAM,QAAQ;AAAA,UAC3B,SAAS,IAAI,OAAO,OAAO;AACzB,kBAAM,OAAO,MAAM,KAAK,WAAW,MAAM,GAAG,EAAE;AAC9C,mBAAO,EAAE,MAAM,GAAG,MAAM,MAAM,MAAM,YAAY,MAAM,IAAI,EAAE;AAAA,UAC9D,CAAC;AAAA,QACH;AACA,cAAM,WAAW,OAAO,KAAK,CAAC,MAAM,EAAE,KAAK,WAAW,CAAC;AACvD,YAAI,UAAU;AACZ,gBAAM,WAAW,SAAS,KAAK,SAAS,CAAC,GAAG;AAAA,YAC1C,CAAC,SAAS,KAAK,SAAS;AAAA,UAC1B;AACA,0BAAgB,IAAI,SAAS,IAAI,IAAI,UAAU,KAAK,sCAAiC;AAAA,QACvF,OAAO;AACL,eAAK,KAAK,OAAO,IAAI,CAAC,MAAM,IAAI,EAAE,IAAI,MAAM,EAAE,KAAK,KAAK,IAAI,CAAC,EAAE,EAAE,KAAK,KAAK,CAAC;AAAA,QAC9E;AAAA,MACF;AAGA,YAAM,aACJ,EAAE,mBAAmB,YAAY,MAAM,KAAK,iBAAiB,IAAI,IAAI;AACvE,WAAK,KAAK,GAAG,mBAAmB,MAAM,GAAG,UAAU,CAAC;AACpD,WAAK,KAAK,GAAG,aAAa,MAAM,KAAK,eAAe,MAAM,sBAAsB,GAAG,GAAG,CAAC;AACvF,YAAM,OAAO,MAAM,KAAK,oBAAoB,IAAI;AAChD,WAAK,KAAK,GAAG,wBAAwB,IAAI,CAAC;AAC1C,WAAK,KAAK,GAAG,oBAAoB,MAAM,uBAAuB,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC;AAGrF,YAAM,UAAU,gBAAgB,MAAM,KAAK,oBAAoB,IAAI,GAAG,GAAG;AACzE,YAAM,EAAE,MAAM,UAAU,MAAM,IAAI,sBAAsB,MAAM,MAAM,KAAK,QAAQ;AACjF,UAAI,KAAK,SAAS,GAAG;AAGnB,aAAK,KAAK;AAAA,UACR;AAAA,UACA,QAAQ;AAAA,UACR,QAAQ,CAAC,GAAG,MAAM,GAAG,KAAK,EAAE,KAAK,KAAK;AAAA,UACtC,iBAAiB;AAAA,QACnB,CAAC;AAAA,MACH,WAAW,MAAM,SAAS,GAAG;AAG3B,aAAK,KAAK;AAAA,UACR;AAAA,UACA,QAAQ;AAAA,UACR,QAAQ,MAAM,KAAK,KAAK;AAAA,UACxB,iBAAiB;AAAA,QACnB,CAAC;AAAA,MACH,OAAO;AACL,aAAK,KAAK;AAAA,UACR;AAAA,UACA,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,iBAAiB;AAAA,QACnB,CAAC;AAAA,MACH;AAAA,IACF,SAAS,GAAG;AACV,WAAK,KAAK;AAAA,QACR;AAAA,QACA,QAAQ;AAAA,QACR,QAAQ,iBAAiB,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACrE,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;;;AC1cO,IAAM,gCAA+C,CAAC;AAWtD,SAAS,yBAAyB,aAA0C;AACjF,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,WAAW;AAAA,EACjC,SAAS,GAAG;AACV,WAAO;AAAA,MACL,OAAO;AAAA,MACP,QAAQ,mCAAmC,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,IACvF;AAAA,EACF;AACA,MAAI,OAAO,WAAW,YAAY,WAAW,MAAM;AACjD,WAAO,EAAE,OAAO,cAAc,QAAQ,oCAAoC;AAAA,EAC5E;AACA,QAAM,QAAS,OAAmC;AAClD,MAAI,UAAU,OAAW,QAAO,EAAE,OAAO,SAAS;AAClD,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,OAAO,cAAc,QAAQ,qBAAqB,OAAO,KAAK,iBAAiB;AAAA,EAC1F;AACA,SAAO,EAAE,OAAO,WAAW,MAAM;AACnC;AAyBO,SAAS,yBAAyB,UAA4B;AACnE,QAAM,QAAQ,SAAS,MAAM,IAAI;AACjC,QAAM,OAAiB,CAAC;AAExB,QAAM,WAAW,CAAC,SAAyB,KAAK,SAAS,KAAK,UAAU,EAAE;AAC1E,QAAM,UAAU,CAAC,SAA0B,KAAK,KAAK,EAAE,WAAW,KAAK,QAAQ,KAAK,IAAI;AAExF,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,OAAO,MAAM,CAAC;AACpB,UAAM,OAAO,4DAA4D,KAAK,IAAI;AAClF,QAAI,CAAC,KAAM;AAIX,UAAM,gBAAgB,KAAK,CAAC,EAAG,UAAU,KAAK,CAAC,IAAI,KAAK,CAAC,EAAE,SAAS;AAEpE,aAAS,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACzC,YAAM,OAAO,MAAM,CAAC;AACpB,UAAI,QAAQ,IAAI,EAAG;AACnB,YAAM,SAAS,SAAS,IAAI;AAG5B,UAAI,SAAS,cAAe;AAC5B,UAAI,WAAW,iBAAiB,UAAU,KAAK,IAAI,EAAG;AACtD,UAAI,WAAW,iBAAiB,CAAC,gBAAgB,KAAK,IAAI,EAAG;AAG7D,eAAS,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACzC,cAAM,QAAQ,MAAM,CAAC;AACrB,YAAI,QAAQ,KAAK,EAAG;AACpB,YAAI,SAAS,KAAK,KAAK,cAAe;AACtC,cAAM,IAAI,kCAAkC,KAAK,KAAK;AACtD,YAAI,EAAG,MAAK,KAAK,EAAE,CAAC,CAAE;AAAA,MACxB;AACA;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAsBO,SAAS,SAAS,MAA0B;AACjD,MAAI,KAAK,WAAW,EAAG,QAAO,EAAE,OAAO,OAAO;AAC9C,QAAM,QAAQ,oBAAI,IAAoB;AACtC,aAAW,KAAK,KAAM,OAAM,IAAI,IAAI,MAAM,IAAI,CAAC,KAAK,KAAK,CAAC;AAC1D,QAAM,SAAS,CAAC,GAAG,MAAM,QAAQ,CAAC,EAC/B,IAAI,CAAC,CAAC,KAAK,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,EACtC,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,cAAc,EAAE,GAAG,CAAC;AACjE,QAAM,MAAM,OAAO,CAAC;AACpB,MAAI,IAAI,QAAQ,IAAI,KAAK,QAAQ;AAC/B,WAAO,EAAE,OAAO,YAAY,KAAK,IAAI,KAAK,OAAO,IAAI,OAAO,OAAO,KAAK,OAAO;AAAA,EACjF;AACA,SAAO,EAAE,OAAO,SAAS,QAAQ,OAAO,KAAK,OAAO;AACtD;AAGO,SAAS,mBAAmB,OAA4B,OAA2B;AACxF,MAAI,MAAM,gBAAgB,KAAM,QAAO,CAAC;AACxC,QAAM,QAAQ,yBAAyB,MAAM,WAAW;AACxD,QAAM,OAAiB,CAAC;AACxB,QAAM,kBAAkB,MAAM,aAC3B,IAAI,CAAC,MAAM,GAAG,EAAE,IAAI,cAAc,EAAE,OAAO,GAAG,EAC9C,KAAK,IAAI;AAEZ,MAAI,MAAM,UAAU,cAAc;AAChC,SAAK,KAAK,oCAA+B,MAAM,MAAM,0BAA0B;AAC/E,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,UAAU,UAAU;AAC5B,UAAM,YACJ,MAAM,UAAU,aAAa,sBAAsB,MAAM,GAAG,MAAM;AAKpE,SAAK;AAAA,MACH,MAAM,aAAa,SAAS,IACxB,0CAA0C,SAAS,SAAS,eAAe,+MAI3E,0CAA0C,SAAS;AAAA,IAGzD;AACA,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,UAAU,cAAc,MAAM,UAAU,MAAM,KAAK;AAC3D,SAAK;AAAA,MACH,qBAAqB,MAAM,KAAK,yBAAyB,MAAM,GAAG,KAC5D,MAAM,KAAK,IAAI,MAAM,KAAK;AAAA,IAElC;AAAA,EACF;AAEA,MAAI,MAAM,aAAa,SAAS,GAAG;AACjC,UAAM,QAAQ,MAAM,aAAa,MAAM,CAAC,MAAM,QAAQ,EAAE,OAAO,OAAO,MAAM,KAAK;AACjF,SAAK;AAAA,MACH,GAAG,eAAe,sDAAsD,MAAM,KAAK,MAChF,QAAQ,6DAAwD,2BACjE;AAAA,IAEJ;AAAA,EACF;AAEA,SAAO;AACT;AAqBA,eAAsB,0BACpB,KACA,MACA,MAAY,oBAAI,KAAK,GACrB,WAA0B,+BACI;AAC9B,QAAM,OAA4B,CAAC;AACnC,QAAM,QAAQ,oBAAI,IAAiC;AAEnD,aAAW,KAAK,MAAM,KAAK,aAAa,GAAG,GAAG;AAC5C,UAAM,OAAO,GAAG,GAAG,IAAI,EAAE,IAAI;AAC7B,QAAI,EAAE,YAAY,EAAE,eAAe,UAAU;AAC3C,WAAK,KAAK;AAAA,QACR;AAAA,QACA,OAAO;AAAA,QACP,MAAM,CAAC;AAAA,QACP,UAAU,CAAC;AAAA,QACX,QAAQ,EAAE,WAAW,aAAa,GAAG,EAAE,UAAU;AAAA,MACnD,CAAC;AACD;AAAA,IACF;AACA,QAAI;AACF,YAAM,cAAc,MAAM,KAAK,aAAa,MAAM,cAAc;AAChE,UAAI,gBAAgB,MAAM;AAExB,aAAK,KAAK;AAAA,UACR;AAAA,UACA,OAAO;AAAA,UACP,MAAM,CAAC;AAAA,UACP,UAAU,CAAC;AAAA,UACX,QAAQ;AAAA,QACV,CAAC;AACD;AAAA,MACF;AACA,YAAM,eAAoD,CAAC;AAC3D,iBAAW,QAAQ,MAAM,KAAK,kBAAkB,IAAI,GAAG;AACrD,cAAM,WAAW,MAAM,KAAK,aAAa,MAAM,IAAI;AACnD,YAAI,aAAa,KAAM;AACvB,mBAAW,WAAW,yBAAyB,QAAQ,GAAG;AACxD,uBAAa,KAAK,EAAE,MAAM,KAAK,QAAQ,0BAA0B,EAAE,GAAG,QAAQ,CAAC;AAAA,QACjF;AAAA,MACF;AACA,YAAM,IAAI,MAAM,EAAE,aAAa,aAAa,CAAC;AAC7C,WAAK,KAAK,EAAE,MAAM,OAAO,UAAU,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,QAAQ,GAAG,CAAC;AAAA,IACzE,SAAS,GAAG;AAGV,WAAK,KAAK;AAAA,QACR;AAAA,QACA,OAAO;AAAA,QACP,MAAM,CAAC,0BAA0B,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC,EAAE;AAAA,QAC7E,UAAU,CAAC;AAAA,QACX,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,SAAmB,CAAC;AAC1B,aAAW,KAAK,MAAM,OAAO,GAAG;AAC9B,UAAM,QAAQ,yBAAyB,EAAE,WAAY;AACrD,QAAI,MAAM,UAAU,UAAW,QAAO,KAAK,MAAM,KAAK;AAAA,EACxD;AACA,QAAM,QAAQ,SAAS,MAAM;AAE7B,SAAO,KAAK,IAAI,CAAC,QAAQ;AACvB,UAAM,IAAI,MAAM,IAAI,IAAI,IAAI;AAC5B,QAAI,CAAC,EAAG,QAAO;AACf,UAAM,MAAM,mBAAmB,GAAG,KAAK;AACvC,UAAM,QAAQ,sBAAsB,IAAI,MAAM,KAAK,KAAK,QAAQ;AAChE,UAAM,QAAQ,yBAAyB,EAAE,WAAY;AACrD,WAAO;AAAA,MACL,GAAG;AAAA,MACH,MAAM,MAAM;AAAA,MACZ,UAAU,MAAM;AAAA,MAChB,QAAQ,MAAM,UAAU,YAAY,MAAM,QAAQ,IAAI;AAAA,IACxD;AAAA,EACF,CAAC;AACH;AAKO,SAAS,yBAAyB,MAAmC;AAC1E,QAAM,SAAS,KAAK,OAAO,CAAC,MAAM,EAAE,UAAU,QAAQ;AACtD,QAAM,OAAO,OAAO,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,WAAW,OAAO,CAAC;AAC5E,QAAM,QAAQ,SAAS,IAAI;AAC3B,QAAM,MACJ,MAAM,UAAU,aACZ,MAAM,MACN,MAAM,UAAU,UACd,SAAS,MAAM,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,GAAG,CAAC,MACjE;AACR,SACE,4BAA4B,OAAO,OAAO,CAAC,MAAM,EAAE,KAAK,SAAS,CAAC,EAAE,MAAM,WAChE,KAAK,MAAM,WAAW,OAAO,MAAM,iBAC7B,KAAK,OAAO,CAAC,MAAM,EAAE,UAAU,cAAc,EAAE,MAAM,YAC1D,KAAK,OAAO,CAAC,MAAM,EAAE,UAAU,SAAS,EAAE,MAAM,aAAa,GAAG;AAE/E;;;ACxXO,SAAS,wBAAwB,MAAsB;AAC5D,SAAO,OAAO,IAAI,IAAI;AACxB;AASA,eAAsB,0BACpB,MAEA,cAC2C;AAC3C,QAAM,MAAM,KAAK,KAAK,KAAK;AAC3B,MAAI,CAAC,KAAK;AACR,WAAO,EAAE,QAAQ,yCAAyC,MAAM,EAAE;AAAA,EACpE;AACA,QAAM,QAAQ,QAAQ,IAAI,UAAU,KAAK;AACzC,MAAI,CAAC,gBAAgB,CAAC,OAAO;AAC3B,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM;AAAA,IACR;AAAA,EACF;AACA,QAAM,OAAO,gBAAgB,WAAW,EAAE,MAAc,CAAC;AAEzD,QAAM,WAAW,MAAM,0BAA0B,KAAK,IAAI;AAC1D,QAAM,UAAU,MAAM,0BAA0B,KAAK,IAAI;AACzD,QAAM,YAAY,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAMzD,QAAM,OAAO,SAAS,IAAI,CAAC,QAAQ;AACjC,UAAM,MAAM,UAAU,IAAI,IAAI,IAAI;AAClC,QAAI,CAAC,OAAO,IAAI,KAAK,WAAW,EAAG,QAAO;AAC1C,WAAO;AAAA,MACL,GAAG;AAAA,MACH,QAAQ;AAAA,MACR,QAAQ,CAAC,IAAI,QAAQ,GAAG,IAAI,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,KAAK,KAAK;AAAA,IAC1E;AAAA,EACF,CAAC;AAED,QAAM,OAAO,KAAK,OAAO,CAAC,MAAM,EAAE,WAAW,KAAK;AAClD,QAAM,UAAU,KAAK,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS;AACzD,QAAM,UAAU,KAAK,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS;AAEzD,QAAM,QAAQ,KAAK,IAAI,CAAC,MAAM,GAAG,EAAE,OAAO,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,IAAI,WAAM,EAAE,MAAM,EAAE;AAO3F,QAAM,WAAW,KAAK,IAAI,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,CAAC,MAAM,MAAM,MAAS;AACjF,aAAW,KAAK,MAAM;AACpB,UAAM,OAAO,EAAE,mBAAmB,oBAAoB,EAAE,eAAe;AACvE,QAAI,KAAM,OAAM,KAAK,WAAW,EAAE,IAAI,WAAM,IAAI,EAAE;AAAA,EACpD;AAGA,aAAW,KAAK,SAAS;AACvB,eAAW,QAAQ,EAAE,SAAU,OAAM,KAAK,WAAW,EAAE,IAAI,WAAM,IAAI,EAAE;AAAA,EACzE;AACA,QAAM,KAAK,uBAAuB,QAAQ,CAAC;AAC3C,QAAM,KAAK,yBAAyB,OAAO,CAAC;AAC5C,QAAM;AAAA,IACJ,yBAAyB,KAAK,MAAM,YAAY,QAAQ,MAAM,YAAY,QAAQ,MAAM,UAAU,KAAK,MAAM;AAAA,EAC/G;AACA,SAAO,EAAE,QAAQ,MAAM,KAAK,IAAI,GAAG,MAAM,wBAAwB,KAAK,MAAM,EAAE;AAChF;","names":[]}
@@ -3,11 +3,11 @@ import {
3
3
  isConfigName,
4
4
  planTemplateDiffs,
5
5
  syncConfigs
6
- } from "../chunk-IMYL67JV.js";
7
- import "../chunk-SQ4FSNYL.js";
6
+ } from "../chunk-3RLDX3Q2.js";
8
7
  import "../chunk-SLFG7Y76.js";
8
+ import "../chunk-LUF52EIY.js";
9
9
  import "../chunk-XXTZBPUY.js";
10
- import "../chunk-EASYCD6F.js";
10
+ import "../chunk-WMF5S2MB.js";
11
11
  export {
12
12
  ALL_CONFIG_NAMES,
13
13
  isConfigName,
@@ -3,8 +3,8 @@ import {
3
3
  PROBES_SKIPPED,
4
4
  domainOf,
5
5
  resolveBusinessName
6
- } from "./chunk-QFSL2L2B.js";
7
- import "./chunk-LAJ3FF54.js";
6
+ } from "./chunk-L5F3WYIY.js";
7
+ import "./chunk-5UI4FERC.js";
8
8
  import {
9
9
  escapeHtml,
10
10
  safeUrl
@@ -328,4 +328,4 @@ function renderProspectReport(result) {
328
328
  export {
329
329
  renderProspectReport
330
330
  };
331
- //# sourceMappingURL=render-MHUUHPLN.js.map
331
+ //# sourceMappingURL=render-NWUAX6RN.js.map
@@ -2,16 +2,18 @@ import {
2
2
  renderReportFromRow,
3
3
  requireLighthouse,
4
4
  windowDays
5
- } from "./chunk-Y5BVR4HW.js";
5
+ } from "./chunk-6THEZQLA.js";
6
6
  import "./chunk-GECESMVS.js";
7
7
  import "./chunk-KJGKZLCT.js";
8
8
  import "./chunk-BGTYPVLT.js";
9
- import "./chunk-MZZDMBDT.js";
10
- import "./chunk-KN52PM63.js";
9
+ import "./chunk-LZMAG73U.js";
10
+ import "./chunk-TC4GANMX.js";
11
+ import "./chunk-ICSRWW5Q.js";
12
+ import "./chunk-RQJFGK3A.js";
11
13
  import "./chunk-RARREDQE.js";
12
14
  export {
13
15
  renderReportFromRow,
14
16
  requireLighthouse,
15
17
  windowDays
16
18
  };
17
- //# sourceMappingURL=render-from-row-65DC6ZP3.js.map
19
+ //# sourceMappingURL=render-from-row-75JTED2Z.js.map
@@ -6,11 +6,13 @@ import {
6
6
  readAirtableConfig
7
7
  } from "./chunk-HN6I2XWI.js";
8
8
  import {
9
- allActionableVulnsTransitive,
10
- isDashboardVisible,
11
- listWebsites,
12
9
  updateAutoFixAttempts
13
- } from "./chunk-KN52PM63.js";
10
+ } from "./chunk-TC4GANMX.js";
11
+ import {
12
+ allActionableVulnsTransitive,
13
+ isDashboardVisible
14
+ } from "./chunk-ICSRWW5Q.js";
15
+ import "./chunk-RQJFGK3A.js";
14
16
  import {
15
17
  makeGitHub
16
18
  } from "./chunk-43SW3FGW.js";
@@ -98,15 +100,18 @@ async function runRenovateDispatchCommand(opts) {
98
100
  if (!opts.fleet) {
99
101
  return { output: "renovate-dispatch currently supports only --fleet", code: 2 };
100
102
  }
101
- const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();
103
+ const token = process.env.GH_TOKEN?.trim();
102
104
  if (!token) {
103
105
  return {
104
- output: "renovate-dispatch skipped: no RENOVATE_TOKEN/GH_TOKEN (fleet dispatch) configured.",
106
+ output: "renovate-dispatch skipped: no GH_TOKEN (fleet dispatch) configured.",
105
107
  code: 0
106
108
  };
107
109
  }
108
110
  const base = opts.base ?? openBase(readAirtableConfig());
109
- const websites = await listWebsites(base);
111
+ const websites = await (opts.roster ?? (async () => {
112
+ const { readFleetRoster } = await import("./roster-EJ3ILR52.js");
113
+ return readFleetRoster();
114
+ }))();
110
115
  const targets = selectRenovateTargets(websites);
111
116
  const lines = [];
112
117
  let result = { dispatched: [], skipped: [], failed: [] };
@@ -141,4 +146,4 @@ async function runRenovateDispatchCommand(opts) {
141
146
  export {
142
147
  runRenovateDispatchCommand
143
148
  };
144
- //# sourceMappingURL=renovate-dispatch-ONJMBUHJ.js.map
149
+ //# sourceMappingURL=renovate-dispatch-ZZQXT7DA.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 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
+ {"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 { updateAutoFixAttempts } from \"../../reports/airtable/websites.js\";\nimport type { FleetRoster } from \"../../fleet/roster.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. Still\n * needed: the auto-fix counter write is an Airtable SHADOW write. */\n base?: AirtableBase;\n /** #646 step 4: the fleet roster targets are selected from. Default: Turso\n * (`readFleetRoster`) — an Airtable roster cannot see a `site_<ULID>` site,\n * so its vulnerabilities would never dispatch Renovate. */\n roster?: FleetRoster;\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.GH_TOKEN?.trim();\n if (!token) {\n return {\n output: \"renovate-dispatch skipped: no GH_TOKEN (fleet dispatch) configured.\",\n code: 0,\n };\n }\n\n const base = opts.base ?? openBase(readAirtableConfig());\n const websites = await (\n opts.roster ??\n (async () => {\n const { readFleetRoster } = await import(\"../../fleet/roster.js\");\n return readFleetRoster();\n })\n )();\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":";;;;;;;;;;;;;;;;;;;;;;AAyBO,IAAM,yBAAyB;AAY/B,SAAS,qBAAqB,KAAoC;AACvE,SAAO,IAAI,KAAK,CAAC,OAAO,aAAa,EAAE,KAAK,GAAG,cAAc,aAAa;AAC5E;AAsBO,SAAS,sBAAsB,OAA+C;AACnF,QAAM,UAAoC,CAAC;AAC3C,aAAW,KAAK,OAAO;AACrB,QAAI,CAAC,mBAAmB,CAAC,EAAG;AAC5B,UAAM,OAAO,EAAE,SAAS,KAAK;AAC7B,QAAI,CAAC,KAAM;AACX,UAAM,WAAW,EAAE,yBAAyB;AAC5C,UAAM,OAAO,EAAE,qBAAqB;AACpC,QAAI,WAAW,QAAQ,EAAG;AAC1B,YAAQ,KAAK,EAAE,MAAM,UAAU,EAAE,MAAM,UAAU,KAAK,CAAC;AAAA,EACzD;AACA,SAAO;AACT;AAgCA,eAAsB,uBACpB,SACA,MACiC;AACjC,QAAM,aAAuB,CAAC;AAC9B,QAAM,UAAoB,CAAC;AAC3B,QAAM,SAA4C,CAAC;AACnD,aAAW,KAAK,SAAS;AACvB,QAAI;AACF,UAAI,MAAM,KAAK,yBAAyB,EAAE,IAAI,GAAG;AAC/C,gBAAQ,KAAK,EAAE,IAAI;AACnB;AAAA,MACF;AACA,YAAM,MAAM,MAAM,KAAK,cAAc,EAAE,IAAI;AAC3C,YAAM,KAAK,SAAS,EAAE,MAAM,wBAAwB,GAAG;AACvD,iBAAW,KAAK,EAAE,IAAI;AAAA,IACxB,SAAS,GAAG;AACV,aAAO,KAAK,EAAE,MAAM,EAAE,MAAM,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,EAAE,CAAC;AAAA,IACjF;AAAA,EACF;AACA,SAAO,EAAE,YAAY,SAAS,OAAO;AACvC;AAGO,SAAS,8BAA8B,QAAwC;AACpF,SAAO,wCAAwC,OAAO,WAAW,MAAM,YAAY,OAAO,QAAQ,MAAM,WAAW,OAAO,OAAO,MAAM;AACzI;AAuBO,SAAS,6BACd,OACA,QACoC;AACpC,QAAM,aAAa,IAAI,IAAI,OAAO,UAAU;AAC5C,QAAM,UAA8C,CAAC;AACrD,aAAW,KAAK,OAAO;AACrB,UAAM,UAAU,EAAE,2BAA2B;AAC7C,UAAM,SAAS,EAAE,yBAAyB,MAAM,EAAE,qBAAqB;AACvE,QAAI,UAAU,GAAG;AAIf,UAAI,YAAY,EAAG,SAAQ,KAAK,EAAE,IAAI,EAAE,IAAI,UAAU,EAAE,CAAC;AACzD;AAAA,IACF;AACA,QAAI,CAAC,mBAAmB,CAAC,EAAG;AAC5B,UAAM,OAAO,EAAE,SAAS,KAAK;AAC7B,QAAI,CAAC,KAAM;AACX,QAAI,CAAC,WAAW,IAAI,IAAI,EAAG;AAG3B,QAAI,6BAA6B,EAAE,kBAAkB,EAAG;AACxD,YAAQ,KAAK,EAAE,IAAI,EAAE,IAAI,UAAU,UAAU,EAAE,CAAC;AAAA,EAClD;AACA,SAAO;AACT;AAQA,eAAsB,2BACpB,SACA,OAC8C;AAC9C,MAAI,UAAU;AACd,MAAI,SAAS;AACb,aAAW,KAAK,SAAS;AACvB,QAAI;AACF,YAAM,MAAM,EAAE,IAAI,EAAE,QAAQ;AAC5B;AAAA,IACF,QAAQ;AACN;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,SAAS,OAAO;AAC3B;AAGO,SAAS,6BAA6B,OAAoD;AAC/F,SAAO,qCAAqC,MAAM,OAAO,WAAW,MAAM,MAAM;AAClF;;;ACpLA,eAAsB,2BAA2B,MAaH;AAC5C,MAAI,CAAC,KAAK,OAAO;AACf,WAAO,EAAE,QAAQ,qDAAqD,MAAM,EAAE;AAAA,EAChF;AACA,QAAM,QAAQ,QAAQ,IAAI,UAAU,KAAK;AACzC,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM;AAAA,IACR;AAAA,EACF;AAEA,QAAM,OAAO,KAAK,QAAQ,SAAS,mBAAmB,CAAC;AACvD,QAAM,WAAW,OACf,KAAK,WACJ,YAAY;AACX,UAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,sBAAuB;AAChE,WAAO,gBAAgB;AAAA,EACzB,IACA;AACF,QAAM,UAAU,sBAAsB,QAAQ;AAE9C,QAAM,QAAkB,CAAC;AACzB,MAAI,SAAiC,EAAE,YAAY,CAAC,GAAG,SAAS,CAAC,GAAG,QAAQ,CAAC,EAAE;AAC/E,MAAI,QAAQ,WAAW,GAAG;AACxB,UAAM;AAAA,MACJ;AAAA,IACF;AAAA,EACF,OAAO;AACL,UAAM,KAAK,WAAW,EAAE,MAAM,CAAC;AAC/B,aAAS,MAAM,uBAAuB,SAAS;AAAA,MAC7C,0BAA0B,OAAO,SAC/B,qBAAqB,MAAM,GAAG,iBAAiB,IAAI,CAAC;AAAA,MACtD,eAAe,CAAC,SAAS,GAAG,cAAc,IAAI;AAAA,MAC9C,UAAU,CAAC,MAAM,UAAU,QAAQ,GAAG,iBAAiB,MAAM,UAAU,GAAG;AAAA,IAC5E,CAAC;AAED,UAAM,cAAc,IAAI,IAAI,OAAO,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AAC5D,UAAM,eAAe,IAAI,IAAI,OAAO,OAAO;AAC3C,eAAW,KAAK,SAAS;AACvB,YAAM,SAAS,YAAY,IAAI,EAAE,IAAI,IACjC,kCACA,aAAa,IAAI,EAAE,IAAI,IACrB,kCACA;AACN,YAAM,KAAK,GAAG,MAAM,IAAI,EAAE,IAAI,cAAc,EAAE,QAAQ,SAAS,EAAE,IAAI,YAAO,EAAE,QAAQ,EAAE;AAAA,IAC1F;AACA,eAAW,KAAK,OAAO,OAAQ,OAAM,KAAK,YAAO,EAAE,IAAI,KAAK,EAAE,KAAK,EAAE;AAAA,EACvE;AAOA,QAAM,iBAAiB,6BAA6B,UAAU,MAAM;AACpE,QAAM,eAAe,MAAM,2BAA2B,gBAAgB,OAAO,IAAI,aAAa;AAC5F,UAAM,SAAS,MAAM,sBAAsB,MAAM,IAAI,QAAQ;AAC7D,UAAM,KAAK,YAAY,OAAO,IAAI,MAAM;AAAA,EAC1C,CAAC;AAED,QAAM,KAAK,8BAA8B,MAAM,CAAC;AAChD,QAAM,KAAK,6BAA6B,YAAY,CAAC;AAErD,SAAO,EAAE,QAAQ,MAAM,KAAK,IAAI,GAAG,MAAM,EAAE;AAC7C;","names":[]}