@reddoorla/maintenance 0.95.0 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -2
- package/dist/airtable-SJV4ZSMS.js +9 -0
- package/dist/{announce-PGT4SZZB.js → announce-45HVP4J4.js} +7 -7
- package/dist/{blux-4KNXUP2K.js → blux-RUFMNOUJ.js} +5 -5
- package/dist/{bump-deps-C5G2EFBB.js → bump-deps-EL3TKMIL.js} +6 -6
- package/dist/{chunk-XN37VTDB.js → chunk-37B4LVB3.js} +2 -2
- package/dist/{chunk-BVEDKMNG.js → chunk-3EA43S3R.js} +28 -10
- package/dist/chunk-3EA43S3R.js.map +1 -0
- package/dist/{chunk-XHHMM5HX.js → chunk-43SW3FGW.js} +63 -1
- package/dist/chunk-43SW3FGW.js.map +1 -0
- package/dist/{chunk-WMBLR5VA.js → chunk-44SDGPGQ.js} +3 -3
- package/dist/chunk-4UTJ4G6P.js +52 -0
- package/dist/chunk-4UTJ4G6P.js.map +1 -0
- package/dist/{chunk-R53VL3RI.js → chunk-5CJVUPJU.js} +1 -1
- package/dist/{chunk-R53VL3RI.js.map → chunk-5CJVUPJU.js.map} +1 -1
- package/dist/{chunk-4AIYWWKV.js → chunk-62YWOFVN.js} +36 -5
- package/dist/chunk-62YWOFVN.js.map +1 -0
- package/dist/{chunk-22GKJO5F.js → chunk-6INOBNRO.js} +2 -2
- package/dist/{chunk-BYGPUUCT.js → chunk-72FJZ6LO.js} +43 -11
- package/dist/chunk-72FJZ6LO.js.map +1 -0
- package/dist/{chunk-5GUJ36TL.js → chunk-74QRWE6G.js} +2 -2
- package/dist/chunk-A5QKNOXW.js +72 -0
- package/dist/chunk-A5QKNOXW.js.map +1 -0
- package/dist/{chunk-2NQUCS7O.js → chunk-BIYQN7FH.js} +3 -1
- package/dist/{chunk-2NQUCS7O.js.map → chunk-BIYQN7FH.js.map} +1 -1
- package/dist/{chunk-S7RYFUIK.js → chunk-CA6ADGOC.js} +2 -2
- package/dist/chunk-CGQZFQAY.js +38 -0
- package/dist/chunk-CGQZFQAY.js.map +1 -0
- package/dist/{chunk-YLYXW5PH.js → chunk-CXICCLNM.js} +34 -4
- package/dist/chunk-CXICCLNM.js.map +1 -0
- package/dist/{chunk-KZRR4NFG.js → chunk-D3CTOCUR.js} +3 -3
- package/dist/{chunk-CWXTNPNZ.js → chunk-DVJNYPFR.js} +3 -3
- package/dist/chunk-DVJNYPFR.js.map +1 -0
- package/dist/{chunk-AVEQUR44.js → chunk-EASYCD6F.js} +60 -17
- package/dist/chunk-EASYCD6F.js.map +1 -0
- package/dist/{chunk-BCKEP5S6.js → chunk-ECDJDHKT.js} +2 -2
- package/dist/{chunk-DMPHP7UP.js → chunk-EQQ4H6L3.js} +2 -2
- package/dist/{chunk-WVNDEYXL.js → chunk-GEUC5VGM.js} +11 -5
- package/dist/chunk-GEUC5VGM.js.map +1 -0
- package/dist/{chunk-Z2K4JPQ3.js → chunk-H445EH33.js} +2 -2
- package/dist/chunk-IHHAYXKY.js +351 -0
- package/dist/chunk-IHHAYXKY.js.map +1 -0
- package/dist/{chunk-FUWFLAWQ.js → chunk-IMYL67JV.js} +3 -3
- package/dist/{chunk-3Z6X3BHU.js → chunk-KN52PM63.js} +7 -2
- package/dist/{chunk-3Z6X3BHU.js.map → chunk-KN52PM63.js.map} +1 -1
- package/dist/{chunk-WJMPCVOR.js → chunk-LAJ3FF54.js} +12 -4
- package/dist/chunk-LAJ3FF54.js.map +1 -0
- package/dist/{chunk-5UINJNAJ.js → chunk-MZZDMBDT.js} +16 -3
- package/dist/chunk-MZZDMBDT.js.map +1 -0
- package/dist/{chunk-DFKBTBO3.js → chunk-NVLREB5R.js} +35 -6
- package/dist/chunk-NVLREB5R.js.map +1 -0
- package/dist/{chunk-AEUABPAJ.js → chunk-OTEVKRAH.js} +3 -3
- package/dist/{chunk-KQB2BX4V.js → chunk-P4A6D6HO.js} +4 -4
- package/dist/chunk-P7323YVW.js +134 -0
- package/dist/chunk-P7323YVW.js.map +1 -0
- package/dist/{chunk-4763KQ6O.js → chunk-PXRN2MER.js} +2 -2
- package/dist/{chunk-TR3HFH6J.js → chunk-QFSL2L2B.js} +67 -10
- package/dist/chunk-QFSL2L2B.js.map +1 -0
- package/dist/{chunk-2LYOAB2E.js → chunk-RRE74VWI.js} +2 -2
- package/dist/{chunk-LM2NHRCJ.js → chunk-SHGIDTYF.js} +4 -4
- package/dist/chunk-SHGIDTYF.js.map +1 -0
- package/dist/{chunk-ZQV7UQNE.js → chunk-SQ4FSNYL.js} +2 -2
- package/dist/{chunk-OCNSIBMP.js → chunk-T76MA2FH.js} +135 -1
- package/dist/{chunk-OCNSIBMP.js.map → chunk-T76MA2FH.js.map} +1 -1
- package/dist/{chunk-6R56OKJZ.js → chunk-UIITN37J.js} +2 -2
- package/dist/{chunk-CJ2RMTT7.js → chunk-VEIIUCYL.js} +2 -2
- package/dist/{chunk-SVAAONYB.js → chunk-WQLNUEZL.js} +12 -2
- package/dist/{chunk-SVAAONYB.js.map → chunk-WQLNUEZL.js.map} +1 -1
- package/dist/{chunk-WSJ7RO54.js → chunk-XED2DNAI.js} +23 -5
- package/dist/chunk-XED2DNAI.js.map +1 -0
- package/dist/{chunk-FSGICFXB.js → chunk-XG2A6GIG.js} +17 -5
- package/dist/chunk-XG2A6GIG.js.map +1 -0
- package/dist/{chunk-TO7DJYLZ.js → chunk-XY7YV2YQ.js} +178 -39
- package/dist/chunk-XY7YV2YQ.js.map +1 -0
- package/dist/{chunk-HTABUCF4.js → chunk-Y5BVR4HW.js} +3 -3
- package/dist/chunk-Y6FCXRIC.js +3972 -0
- package/dist/chunk-Y6FCXRIC.js.map +1 -0
- package/dist/{chunk-2SLED3NB.js → chunk-YDZ7XGPZ.js} +1 -1
- package/dist/{chunk-2SLED3NB.js.map → chunk-YDZ7XGPZ.js.map} +1 -1
- package/dist/{chunk-QF6IQQFP.js → chunk-Z7FDJ35L.js} +18 -12
- package/dist/chunk-Z7FDJ35L.js.map +1 -0
- package/dist/{chunk-5EEMTJMT.js → chunk-ZH5LOQET.js} +4 -4
- package/dist/cli/bin.js +49 -31
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +18 -18
- package/dist/client-L7NAUG6J.js +11 -0
- package/dist/configs/eslint.js +27 -1
- package/dist/configs/eslint.js.map +1 -1
- package/dist/configs/playwright-a11y.d.ts +11 -1
- package/dist/configs/playwright-a11y.js +3 -1
- package/dist/{convert-to-pnpm-YTFREUJK.js → convert-to-pnpm-4DON7IFS.js} +6 -6
- package/dist/{db-BBEM7CBX.js → db-5K4CDEGJ.js} +45 -20
- package/dist/db-5K4CDEGJ.js.map +1 -0
- package/dist/deadletter-G4M3HSVM.js +17 -0
- package/dist/{digest-65BOZ7AT.js → digest-5AJGWS33.js} +45 -22
- package/dist/digest-5AJGWS33.js.map +1 -0
- package/dist/{digest-collectors-EHFY2OQC.js → digest-collectors-MC5NWEUA.js} +12 -10
- package/dist/{digest-state-3KHZF7GD.js → digest-state-ZDPKR7MW.js} +18 -2
- package/dist/digest-state-ZDPKR7MW.js.map +1 -0
- package/dist/{ensure-site-WIDO6LGU.js → ensure-site-XESZHZEB.js} +39 -11
- package/dist/ensure-site-XESZHZEB.js.map +1 -0
- package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-GJWGKTLD.js} +6 -4
- package/dist/{forms-notify-target-2LWF5C5B.js → forms-notify-target-4NPLX2UG.js} +5 -5
- package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
- package/dist/{github-signals-GAHNOAHS.js → github-signals-DMU74SAX.js} +10 -10
- package/dist/{header-image-YN3AQRY3.js → header-image-BRTOLHJF.js} +6 -5
- package/dist/header-image-BRTOLHJF.js.map +1 -0
- package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-UCLMSSCN.js} +8 -7
- package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-UCLMSSCN.js.map} +1 -1
- package/dist/{health-mirror-EB76EERV.js → health-mirror-ONRT4DQ5.js} +7 -7
- package/dist/{import-airtable-TAKSN4NV.js → import-airtable-IIK2QUUF.js} +3 -3
- package/dist/index.d.ts +135 -4
- package/dist/index.js +68 -34
- package/dist/index.js.map +1 -1
- package/dist/{init-FFE4FCO2.js → init-3LMZTCVD.js} +19 -18
- package/dist/{init-FFE4FCO2.js.map → init-3LMZTCVD.js.map} +1 -1
- package/dist/{launch-4RVWPZGI.js → launch-XTDMC6LD.js} +209 -61
- package/dist/launch-XTDMC6LD.js.map +1 -0
- package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
- package/dist/{lighthouse-airtable-YMH4UA7R.js → lighthouse-airtable-3NOFB42B.js} +3 -3
- package/dist/{match-harness-2Q6ISEES.js → match-harness-OW45CB5H.js} +14 -7
- package/dist/match-harness-OW45CB5H.js.map +1 -0
- package/dist/migrate-NE3DUMTM.js +7 -0
- package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +3 -3
- package/dist/{onboard-TM2KESGF.js → onboard-CZYBWVWH.js} +6 -6
- package/dist/{orchestrate-MWXENQOS.js → orchestrate-TGDFGY52.js} +8 -8
- package/dist/{parity-CAJ477PS.js → parity-AEKYSLN6.js} +5 -5
- package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
- package/dist/{preflight-4VQP7JJL.js → preflight-POZZI3GN.js} +9 -9
- package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-AHY5ULJR.js} +38 -8
- package/dist/prismic-ci-AHY5ULJR.js.map +1 -0
- package/dist/{prismic-models-W6CNLTWH.js → prismic-models-URYGODNY.js} +9 -9
- package/dist/prospect/types.d.ts +72 -0
- package/dist/prospect/types.js +1 -1
- package/dist/{prospect-audit-X4Z67LWW.js → prospect-audit-AJEE3Z4L.js} +16 -7
- package/dist/prospect-audit-AJEE3Z4L.js.map +1 -0
- package/dist/{prospect-audits-RVYEA4KH.js → prospect-audits-IZVQY3OQ.js} +12 -4
- package/dist/protection-audit-3J3UEOVT.js +413 -0
- package/dist/protection-audit-3J3UEOVT.js.map +1 -0
- package/dist/recipes/sync-configs.js +3 -3
- package/dist/{render-YGNGTX3W.js → render-MHUUHPLN.js} +36 -9
- package/dist/render-MHUUHPLN.js.map +1 -0
- package/dist/{render-from-row-IFSBHWDH.js → render-from-row-65DC6ZP3.js} +4 -4
- package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-ONJMBUHJ.js} +3 -3
- package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
- package/dist/replay-4FVFOMHH.js.map +1 -0
- package/dist/{report-4GFD4MW2.js → report-RC5LGPU3.js} +22 -20
- package/dist/report-RC5LGPU3.js.map +1 -0
- package/dist/{report-mirror-IP64R7B3.js → report-mirror-KKZZ6RMZ.js} +13 -9
- package/dist/{report-mirror-IP64R7B3.js.map → report-mirror-KKZZ6RMZ.js.map} +1 -1
- package/dist/{schema-2V7BGYVT.js → schema-KWUQEZ5O.js} +1 -1
- package/dist/schema-KWUQEZ5O.js.map +1 -0
- package/dist/{self-updating-MWXIZLL7.js → self-updating-RKKIYNV4.js} +8 -7
- package/dist/{self-updating-MWXIZLL7.js.map → self-updating-RKKIYNV4.js.map} +1 -1
- package/dist/{selftest-MAB2PBXQ.js → selftest-AFLR7YV7.js} +10 -10
- package/dist/site-lookup-DTOLJ4EA.js +27 -0
- package/dist/site-lookup-DTOLJ4EA.js.map +1 -0
- package/dist/{site-mirror-UKHREAD6.js → site-mirror-H2FYMN55.js} +23 -9
- package/dist/site-mirror-H2FYMN55.js.map +1 -0
- package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-BWHR3XX5.js} +8 -7
- package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-BWHR3XX5.js.map} +1 -1
- package/dist/{submissions-5GKJPCIH.js → submissions-II5J35QM.js} +4 -4
- package/dist/{submissions-3I3GAQAX.js → submissions-JDZP35JM.js} +49 -9
- package/dist/submissions-JDZP35JM.js.map +1 -0
- package/dist/{svelte-codemods-WSV2FGNG.js → svelte-codemods-QWGIRNN6.js} +6 -6
- package/dist/{sync-K3PKHVLH.js → sync-NKPDCMYW.js} +5 -5
- package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-HGLQV55H.js} +6 -6
- package/dist/{upgrade-VTD6V4HS.js → upgrade-WZ44IARG.js} +6 -6
- package/dist/util/git.d.ts +48 -9
- package/dist/util/git.js +1 -1
- package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
- package/dist/{websites-TB4TCAWM.js → websites-6DHIHJ2D.js} +4 -2
- package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-BIK6LNVC.js} +5 -5
- package/dist/write-audits-to-airtable-BIK6LNVC.js.map +1 -0
- package/package.json +3 -2
- package/dist/airtable-JGCPS7JH.js +0 -9
- package/dist/chunk-4AIYWWKV.js.map +0 -1
- package/dist/chunk-5UINJNAJ.js.map +0 -1
- package/dist/chunk-6VKYQATC.js +0 -1708
- package/dist/chunk-6VKYQATC.js.map +0 -1
- package/dist/chunk-7MKT4I5T.js +0 -62
- package/dist/chunk-7MKT4I5T.js.map +0 -1
- package/dist/chunk-AVEQUR44.js.map +0 -1
- package/dist/chunk-BVEDKMNG.js.map +0 -1
- package/dist/chunk-BYGPUUCT.js.map +0 -1
- package/dist/chunk-CWXTNPNZ.js.map +0 -1
- package/dist/chunk-DFKBTBO3.js.map +0 -1
- package/dist/chunk-FSGICFXB.js.map +0 -1
- package/dist/chunk-GASBX52O.js +0 -12
- package/dist/chunk-GASBX52O.js.map +0 -1
- package/dist/chunk-LM2NHRCJ.js.map +0 -1
- package/dist/chunk-QF6IQQFP.js.map +0 -1
- package/dist/chunk-TO7DJYLZ.js.map +0 -1
- package/dist/chunk-TR3HFH6J.js.map +0 -1
- package/dist/chunk-WJMPCVOR.js.map +0 -1
- package/dist/chunk-WSJ7RO54.js.map +0 -1
- package/dist/chunk-WVNDEYXL.js.map +0 -1
- package/dist/chunk-XHHMM5HX.js.map +0 -1
- package/dist/chunk-YLYXW5PH.js.map +0 -1
- package/dist/client-M2V6FHH5.js +0 -11
- package/dist/db-BBEM7CBX.js.map +0 -1
- package/dist/digest-65BOZ7AT.js.map +0 -1
- package/dist/digest-state-3KHZF7GD.js.map +0 -1
- package/dist/ensure-site-WIDO6LGU.js.map +0 -1
- package/dist/header-image-YN3AQRY3.js.map +0 -1
- package/dist/launch-4RVWPZGI.js.map +0 -1
- package/dist/match-harness-2Q6ISEES.js.map +0 -1
- package/dist/migrate-K4JETR36.js +0 -7
- package/dist/prismic-ci-6GKKZGB5.js.map +0 -1
- package/dist/prospect-audit-X4Z67LWW.js.map +0 -1
- package/dist/protection-audit-SJO53MEQ.js +0 -168
- package/dist/protection-audit-SJO53MEQ.js.map +0 -1
- package/dist/render-YGNGTX3W.js.map +0 -1
- package/dist/replay-PCSQOGCZ.js.map +0 -1
- package/dist/report-4GFD4MW2.js.map +0 -1
- package/dist/schema-2V7BGYVT.js.map +0 -1
- package/dist/site-mirror-UKHREAD6.js.map +0 -1
- package/dist/submissions-3I3GAQAX.js.map +0 -1
- /package/dist/{airtable-JGCPS7JH.js.map → airtable-SJV4ZSMS.js.map} +0 -0
- /package/dist/{announce-PGT4SZZB.js.map → announce-45HVP4J4.js.map} +0 -0
- /package/dist/{blux-4KNXUP2K.js.map → blux-RUFMNOUJ.js.map} +0 -0
- /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-EL3TKMIL.js.map} +0 -0
- /package/dist/{chunk-XN37VTDB.js.map → chunk-37B4LVB3.js.map} +0 -0
- /package/dist/{chunk-WMBLR5VA.js.map → chunk-44SDGPGQ.js.map} +0 -0
- /package/dist/{chunk-22GKJO5F.js.map → chunk-6INOBNRO.js.map} +0 -0
- /package/dist/{chunk-5GUJ36TL.js.map → chunk-74QRWE6G.js.map} +0 -0
- /package/dist/{chunk-S7RYFUIK.js.map → chunk-CA6ADGOC.js.map} +0 -0
- /package/dist/{chunk-KZRR4NFG.js.map → chunk-D3CTOCUR.js.map} +0 -0
- /package/dist/{chunk-BCKEP5S6.js.map → chunk-ECDJDHKT.js.map} +0 -0
- /package/dist/{chunk-DMPHP7UP.js.map → chunk-EQQ4H6L3.js.map} +0 -0
- /package/dist/{chunk-Z2K4JPQ3.js.map → chunk-H445EH33.js.map} +0 -0
- /package/dist/{chunk-FUWFLAWQ.js.map → chunk-IMYL67JV.js.map} +0 -0
- /package/dist/{chunk-AEUABPAJ.js.map → chunk-OTEVKRAH.js.map} +0 -0
- /package/dist/{chunk-KQB2BX4V.js.map → chunk-P4A6D6HO.js.map} +0 -0
- /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
- /package/dist/{chunk-2LYOAB2E.js.map → chunk-RRE74VWI.js.map} +0 -0
- /package/dist/{chunk-ZQV7UQNE.js.map → chunk-SQ4FSNYL.js.map} +0 -0
- /package/dist/{chunk-6R56OKJZ.js.map → chunk-UIITN37J.js.map} +0 -0
- /package/dist/{chunk-CJ2RMTT7.js.map → chunk-VEIIUCYL.js.map} +0 -0
- /package/dist/{chunk-HTABUCF4.js.map → chunk-Y5BVR4HW.js.map} +0 -0
- /package/dist/{chunk-5EEMTJMT.js.map → chunk-ZH5LOQET.js.map} +0 -0
- /package/dist/{client-M2V6FHH5.js.map → client-L7NAUG6J.js.map} +0 -0
- /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-4DON7IFS.js.map} +0 -0
- /package/dist/{digest-collectors-EHFY2OQC.js.map → deadletter-G4M3HSVM.js.map} +0 -0
- /package/dist/{fleet-state-UEVZ3X3J.js.map → digest-collectors-MC5NWEUA.js.map} +0 -0
- /package/dist/{freeze-A6MUCZU4.js.map → fleet-state-GJWGKTLD.js.map} +0 -0
- /package/dist/{forms-notify-target-2LWF5C5B.js.map → forms-notify-target-4NPLX2UG.js.map} +0 -0
- /package/dist/{import-airtable-TAKSN4NV.js.map → freeze-GUQ43EAU.js.map} +0 -0
- /package/dist/{github-signals-GAHNOAHS.js.map → github-signals-DMU74SAX.js.map} +0 -0
- /package/dist/{health-mirror-EB76EERV.js.map → health-mirror-ONRT4DQ5.js.map} +0 -0
- /package/dist/{lighthouse-VHI77PFB.js.map → import-airtable-IIK2QUUF.js.map} +0 -0
- /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
- /package/dist/{migrate-K4JETR36.js.map → lighthouse-airtable-3NOFB42B.js.map} +0 -0
- /package/dist/{notify-IISTHNBR.js.map → migrate-NE3DUMTM.js.map} +0 -0
- /package/dist/{orchestrate-MWXENQOS.js.map → notify-K3WVH3DM.js.map} +0 -0
- /package/dist/{onboard-TM2KESGF.js.map → onboard-CZYBWVWH.js.map} +0 -0
- /package/dist/{parity-CAJ477PS.js.map → orchestrate-TGDFGY52.js.map} +0 -0
- /package/dist/{pipeline-YROAY6DT.js.map → parity-AEKYSLN6.js.map} +0 -0
- /package/dist/{prospect-audits-RVYEA4KH.js.map → pipeline-UUPXOS2J.js.map} +0 -0
- /package/dist/{preflight-4VQP7JJL.js.map → preflight-POZZI3GN.js.map} +0 -0
- /package/dist/{prismic-models-W6CNLTWH.js.map → prismic-models-URYGODNY.js.map} +0 -0
- /package/dist/{render-from-row-IFSBHWDH.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
- /package/dist/{websites-TB4TCAWM.js.map → render-from-row-65DC6ZP3.js.map} +0 -0
- /package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-ONJMBUHJ.js.map} +0 -0
- /package/dist/{selftest-MAB2PBXQ.js.map → selftest-AFLR7YV7.js.map} +0 -0
- /package/dist/{submissions-5GKJPCIH.js.map → submissions-II5J35QM.js.map} +0 -0
- /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-QWGIRNN6.js.map} +0 -0
- /package/dist/{sync-K3PKHVLH.js.map → sync-NKPDCMYW.js.map} +0 -0
- /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-HGLQV55H.js.map} +0 -0
- /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-WZ44IARG.js.map} +0 -0
- /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → websites-6DHIHJ2D.js.map} +0 -0
|
@@ -0,0 +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":[]}
|
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
isConfigName,
|
|
4
4
|
planTemplateDiffs,
|
|
5
5
|
syncConfigs
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-IMYL67JV.js";
|
|
7
|
+
import "../chunk-SQ4FSNYL.js";
|
|
8
8
|
import "../chunk-SLFG7Y76.js";
|
|
9
9
|
import "../chunk-XXTZBPUY.js";
|
|
10
|
-
import "../chunk-
|
|
10
|
+
import "../chunk-EASYCD6F.js";
|
|
11
11
|
export {
|
|
12
12
|
ALL_CONFIG_NAMES,
|
|
13
13
|
isConfigName,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ANALYZE_SKIPPED,
|
|
3
3
|
PROBES_SKIPPED,
|
|
4
|
+
domainOf,
|
|
4
5
|
resolveBusinessName
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-QFSL2L2B.js";
|
|
7
|
+
import "./chunk-LAJ3FF54.js";
|
|
7
8
|
import {
|
|
8
9
|
escapeHtml,
|
|
9
10
|
safeUrl
|
|
@@ -104,7 +105,7 @@ function formatIsoDate(iso) {
|
|
|
104
105
|
timeZone: "UTC"
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
|
-
function buildProbesSection(p, businessNameUsed) {
|
|
108
|
+
function buildProbesSection(p, businessNameUsed, termsSource) {
|
|
108
109
|
const byKind = /* @__PURE__ */ new Map();
|
|
109
110
|
for (const a of p.answers) {
|
|
110
111
|
const bucket = byKind.get(a.kind) ?? [];
|
|
@@ -113,6 +114,7 @@ function buildProbesSection(p, businessNameUsed) {
|
|
|
113
114
|
}
|
|
114
115
|
const recognition = p.brandedRecognized ? "<p>When the engines were asked about the business by name, they recognized it.</p>" : businessNameUsed ? "<p>When the engines were asked about the business by name, they did not recognize it \u2014 a real citation of the site never showed up, even with the name handed to them.</p>" : "<p>The engines did not recognize the business \u2014 there was no business name available to ask them with, so this reflects a harder starting point than a named query would.</p>";
|
|
115
116
|
const categoryCaveat = byKind.has("category") ? "" : `<p class="muted"><strong>No buyer-question (category) query was tested here</strong> \u2014 only name-recognition. An engine echoing back a name it was just given says nothing about whether a buyer who had never heard of the business would be shown it; treat the line above as a floor, not a visibility signal.</p>`;
|
|
117
|
+
const termsProvenance = termsSource === "chosen" ? `<p class="muted">These are searches we chose with you. They stay fixed between audits, so a later run measures the same thing.</p>` : termsSource === "generated" ? `<p class="muted">These are searches we chose from your site. A later audit may choose differently, so treat a before/after across them with care.</p>` : "";
|
|
116
118
|
const tally = p.categoryProbes;
|
|
117
119
|
const missing = tally ? tally.attempted - tally.answered : 0;
|
|
118
120
|
const degradedNotice = tally && missing > 0 ? `<p class="muted"><strong>${missing} of ${tally.attempted} buyer-question searches did not come back</strong> \u2014 the engine errored on them. They are counted as "not found", so the visibility figure above is a floor: the real number could be higher, not lower. Worth re-running before drawing a conclusion from it.</p>` : "";
|
|
@@ -133,8 +135,29 @@ function buildProbesSection(p, businessNameUsed) {
|
|
|
133
135
|
).join("");
|
|
134
136
|
return `<h3>${escapeHtml(KIND_LABEL[kind])}</h3>${cards}`;
|
|
135
137
|
}).join("");
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
+
const queriesCiting = (domain) => [
|
|
139
|
+
...new Set(
|
|
140
|
+
p.answers.filter((a) => a.citedDomains.some((d) => domainOf(d) === domain)).map((a) => a.query)
|
|
141
|
+
)
|
|
142
|
+
];
|
|
143
|
+
const attribution = (domain) => {
|
|
144
|
+
const queries = queriesCiting(domain);
|
|
145
|
+
if (queries.length === 0) return "";
|
|
146
|
+
return `, came back on: ${queries.map((q) => `\u201C${escapeHtml(q)}\u201D`).join(", ")}`;
|
|
147
|
+
};
|
|
148
|
+
const countOf = (n) => `${n} time${n === 1 ? "" : "s"}`;
|
|
149
|
+
const namesakes = p.namesakes ?? [];
|
|
150
|
+
const namesakeDomains = new Set(namesakes.map((n) => n.domain));
|
|
151
|
+
const namesakeBlock = namesakes.length ? `<h3>A different business is using your name</h3>
|
|
152
|
+
<p>The engines cited these domains, and they carry your own business name. They are not competitors \u2014 this is a brand collision, and on a search for your name it is what your buyer finds instead of you.</p>
|
|
153
|
+
<ul>${namesakes.map(
|
|
154
|
+
(n) => `<li>${escapeHtml(n.domain)} \u2014 cited ${countOf(n.count)}${attribution(n.domain)}</li>`
|
|
155
|
+
).join("")}</ul>` : "";
|
|
156
|
+
const rivals = p.competitorsSeen.filter((c) => !namesakeDomains.has(c.domain));
|
|
157
|
+
const competitors = rivals.length ? `<h3>Who the engines cited instead</h3><ul>${rivals.map(
|
|
158
|
+
(c) => `<li>${escapeHtml(c.domain)} \u2014 ${countOf(c.count)}${attribution(c.domain)}</li>`
|
|
159
|
+
).join("")}</ul>` : "";
|
|
160
|
+
return recognition + categoryCaveat + termsProvenance + degradedNotice + groups + namesakeBlock + competitors;
|
|
138
161
|
}
|
|
139
162
|
function buildFindabilitySection(c) {
|
|
140
163
|
const accessBlock = !c.crawlerAccessMeasured ? `<p class="muted">Crawler access: not measured \u2014 ${STAGE_FAILED_MESSAGE}.</p>` : c.crawlerAccess.blockedAi.length ? `<p><strong>Blocked AI crawlers:</strong> ${escapeHtml(
|
|
@@ -173,7 +196,7 @@ function buildLighthouseBlock(lh) {
|
|
|
173
196
|
}
|
|
174
197
|
function buildReadabilitySection(c) {
|
|
175
198
|
const jsLine = c.jsDependence.avgMissing === null ? `<p class="muted">JavaScript-dependence: not measured \u2014 no page produced a comparable raw/rendered pair.</p>` : `<p><strong>${Math.round(c.jsDependence.avgMissing * 100)}%</strong> of the words a visitor reads only appear after JavaScript runs.
|
|
176
|
-
|
|
199
|
+
We tested this directly rather than assuming it: an assistant reading a page by URL reported text that a script wrote at runtime as not stated, three times out of three, while reading the server-rendered text on the same pages correctly every time. That share of your site is invisible to it.</p>`;
|
|
177
200
|
return `${jsLine}
|
|
178
201
|
<ul>
|
|
179
202
|
<li>Structured data found: ${c.schema.typesFound.length ? escapeHtml(c.schema.typesFound.join(", ")) : "none"}</li>
|
|
@@ -234,7 +257,11 @@ function renderProspectReport(result) {
|
|
|
234
257
|
ANALYZE_SKIPPED,
|
|
235
258
|
"Skipped \u2014 the checks stage failed, so there was nothing to build fixes from."
|
|
236
259
|
);
|
|
237
|
-
const probesSectionFinal = result.probes.ok ? buildProbesSection(
|
|
260
|
+
const probesSectionFinal = result.probes.ok ? buildProbesSection(
|
|
261
|
+
result.probes.data,
|
|
262
|
+
businessNameUsed,
|
|
263
|
+
result.analyze.ok ? result.analyze.data.termsSource : void 0
|
|
264
|
+
) : skippableStageNote(
|
|
238
265
|
result.probes.error,
|
|
239
266
|
PROBES_SKIPPED,
|
|
240
267
|
"This audit did not run the AI-visibility probes."
|
|
@@ -268,7 +295,7 @@ function renderProspectReport(result) {
|
|
|
268
295
|
<p class="muted">Each score below runs 0\u2013100 \u2014 read it as a score, not a percentage.</p>
|
|
269
296
|
<div class="scores">
|
|
270
297
|
${scoreCard("Findability", result.scores.findability, "How easily AI and search crawlers can find and reach your site \u2014 crawl rules, sitemap, key metadata")}
|
|
271
|
-
${scoreCard("Readability", result.scores.readability, "How much of your site's content those crawlers can actually read once they're in \u2014
|
|
298
|
+
${scoreCard("Readability", result.scores.readability, "How much of your site's content those crawlers can actually read once they're in \u2014 text your scripts write at runtime did not reach the assistant we tested")}
|
|
272
299
|
${scoreCard("Answers", result.scores.answers, "How many buyer questions your site's own content answers \u2014 separate from what the AI engines say back")}
|
|
273
300
|
</div>
|
|
274
301
|
|
|
@@ -301,4 +328,4 @@ function renderProspectReport(result) {
|
|
|
301
328
|
export {
|
|
302
329
|
renderProspectReport
|
|
303
330
|
};
|
|
304
|
-
//# sourceMappingURL=render-
|
|
331
|
+
//# sourceMappingURL=render-MHUUHPLN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/prospect/render.ts"],"sourcesContent":["import { escapeHtml, safeUrl } from \"../util/html.js\";\nimport { hostnameOf } from \"../util/url.js\";\nimport { ANALYZE_SKIPPED, PROBES_SKIPPED } from \"./pipeline.js\";\nimport { domainOf, resolveBusinessName } from \"./probes.js\";\nimport type {\n AnalyzeResult,\n ChecksResult,\n Fix,\n LighthouseScores,\n ProbeAnswer,\n ProbesResult,\n ProspectAuditResult,\n StageResult,\n} from \"./types.js\";\n\nconst RED = \"#d71920\";\nconst IMPACT_ORDER: Record<Fix[\"impact\"], number> = { high: 0, medium: 1, low: 2 };\nconst KIND_LABEL: Record<ProbeAnswer[\"kind\"], string> = {\n branded: \"Branded — asked about the business by name\",\n category: \"Category — the questions a buyer would actually type\",\n competitor: \"Competitor — head-to-head comparisons\",\n};\nconst KIND_ORDER: ProbeAnswer[\"kind\"][] = [\"category\", \"branded\", \"competitor\"];\n\n/** Terse product mapping so a recipient who has never seen a crawler's raw\n * user-agent string can place it — factual labelling, not a voice change. */\nconst AI_AGENT_LABELS: Record<string, string> = {\n GPTBot: \"ChatGPT\",\n \"OAI-SearchBot\": \"ChatGPT's search results\",\n ClaudeBot: \"Claude\",\n PerplexityBot: \"Perplexity\",\n \"Google-Extended\": \"Gemini and Google's AI Overviews\",\n CCBot: \"Common Crawl, used to train many AI models\",\n};\n\nfunction describeAgent(agent: string): string {\n const label = AI_AGENT_LABELS[agent];\n return label ? `${agent} (feeds ${label})` : agent;\n}\n\n/** Every genuine stage failure collapses to this one phrase — the real error\n * (status codes, retry counts, timeouts — operator vocabulary) stays in the\n * persisted JSON and CLI output for operators, but never reaches a stranger\n * who would read it as broken software rather than a diagnostic. */\nconst STAGE_FAILED_MESSAGE = \"we could not complete this part of the analysis\";\n\nconst STYLES = `\n :root { color-scheme: light; }\n * { box-sizing: border-box; }\n body { margin: 0; background: #faf8f5; color: #1a1a1a;\n font: 16px/1.6 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif; }\n .wrap { max-width: 860px; margin: 0 auto; padding: 48px 24px 80px; }\n h1, h2, h3 { font-family: Besley, Georgia, \"Times New Roman\", serif; font-weight: 600; line-height: 1.2; }\n h1 { font-size: 40px; margin: 0 0 8px; }\n h2 { font-size: 26px; margin: 48px 0 12px; border-top: 2px solid #e6e1da; padding-top: 24px; }\n h3 { font-size: 18px; margin: 24px 0 8px; }\n .lede { color: #57544f; margin: 0 0 8px; }\n .scores { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin: 32px 0; }\n .score { background: #fff; border: 1px solid #e6e1da; border-radius: 8px; padding: 16px; }\n .score .n { font-family: Besley, Georgia, serif; font-size: 38px; line-height: 1; color: ${RED}; }\n .score .n.na { font-size: 18px; color: #8a857e; }\n .score .l { font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: #57544f; margin-top: 8px; }\n .score .hint { font-size: 12px; color: #8a857e; margin-top: 4px; }\n .card { background: #fff; border: 1px solid #e6e1da; border-radius: 8px; padding: 16px; margin: 12px 0; }\n .q { font-weight: 600; }\n .tag { display: inline-block; font-size: 12px; letter-spacing: .06em; text-transform: uppercase;\n border-radius: 999px; padding: 2px 10px; border: 1px solid currentColor; }\n .yes { color: #14663c; } .partial { color: #8a6d00; } .no { color: ${RED}; }\n .unknown { color: #8a857e; border-style: dashed; }\n ul { padding-left: 20px; } li { margin: 6px 0; }\n .muted { color: #8a857e; }\n .cta { margin-top: 56px; background: ${RED}; color: #fff; border-radius: 8px; padding: 24px 28px; }\n .cta h2 { border: 0; padding: 0; margin: 0 0 8px; color: #fff; }\n .cta a { color: #fff; }\n table { width: 100%; border-collapse: collapse; margin-top: 8px; }\n td, th { text-align: left; padding: 6px 8px; border-bottom: 1px solid #eee; vertical-align: top; }\n @media print {\n body { background: #fff; }\n .card, .score, .cta { break-inside: avoid; }\n /* Browsers default \"print background graphics\" OFF, which drops the red\n .cta background but keeps its white text — the only ask in the report\n would otherwise print white-on-white and vanish. Fall back to dark\n text on no background with a visible border instead of relying on the\n background for contrast. */\n .cta { background: none; color: #1a1a1a; border: 2px solid ${RED}; }\n .cta h2, .cta a { color: #1a1a1a; }\n }\n`;\n\n/** One of the THREE site-score cards. AI Visibility used to be a fourth and\n * was removed on purpose, matching the web report's control split: these\n * three measure the site and move when the site is fixed; visibility is\n * measured out in the world and reported with receipts in \"What the AI\n * engines said about you\" — a fourth card said it was the same kind of claim,\n * and ours to move. It is neither. `scores.aiVisibility` stays computed and\n * stored; it just is not a card, here or in the email or CLI summary. */\nfunction scoreCard(label: string, value: number | null, hint?: string): string {\n const n =\n value === null ? `<div class=\"n na\">Not measured</div>` : `<div class=\"n\">${value}</div>`;\n const hintHtml = hint ? `<div class=\"hint\">${escapeHtml(hint)}</div>` : \"\";\n return `<div class=\"score\">${n}<div class=\"l\">${escapeHtml(label)}</div>${hintHtml}</div>`;\n}\n\n/** Body for a stage that succeeded, or a uniform \"Not measured\" note when it didn't. */\nfunction stageBody<T>(stage: StageResult<T>, body: (data: T) => string): string {\n return stage.ok ? body(stage.data) : notMeasuredNote();\n}\n\n/** The stage's real error is deliberately not a parameter here — see\n * STAGE_FAILED_MESSAGE above. Every genuine failure reads the same\n * client-safe way, regardless of what actually broke. */\nfunction notMeasuredNote(): string {\n return `<p class=\"muted\">Not measured — ${STAGE_FAILED_MESSAGE}.</p>`;\n}\n\n/** A stage a human deliberately turned off (--no-probes, or analyze skipped\n * because checks failed upstream) must read as \"you asked us to skip this\" —\n * never as \"we tried and could not\", which is what a bare \"Not measured —\n * {error}\" would otherwise imply. Compares against the pipeline's exported\n * constants rather than a retyped string literal. */\nfunction skippableStageNote(error: string, skipConstant: string, skipMessage: string): string {\n if (error === skipConstant) {\n return `<p class=\"muted\">${escapeHtml(skipMessage)}</p>`;\n }\n return notMeasuredNote();\n}\n\n/** `new Date(iso).toLocaleDateString()` never throws on a bad timestamp — it\n * silently returns \"Invalid Date\" — so a try/catch here is dead code. Guard\n * on getTime() instead, and fall back to the raw ISO string so a corrupt\n * timestamp is at least visibly a timestamp rather than the literal words\n * \"Invalid Date\". */\nfunction formatIsoDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return iso;\n return d.toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n timeZone: \"UTC\",\n });\n}\n\n/** The probes section leads the report, so a branded-only run (analyze\n * failed upstream and supplied no buyer questions, or every category query\n * itself failed) must not let the branded recognition line read as a\n * positive discoverability finding on its own — an engine echoing a name\n * it was just handed proves nothing about whether a buyer who never named\n * the business would find it. */\n/** `businessNameUsed` is whether the engines were actually queried with the\n * business NAME — not merely whether one existed. `resolveBusinessName` falls\n * back to the bare domain for anything it cannot use, so a name can be present\n * and still never reach a query. Without this gate, \"even with the name handed\n * to them\" is a false claim: whenever businessName was empty, and (until the\n * caller was fixed) whenever the name was rejected and the probes searched for\n * the domain instead. Never derived from the raw ProbesResult, which has no\n * way to say \"no name was available\". */\nfunction buildProbesSection(\n p: ProbesResult,\n businessNameUsed: boolean,\n /** #676. Where the searches below came from. Undefined on every report stored\n * before the choice existed — those say nothing rather than claim either. */\n termsSource?: \"chosen\" | \"generated\",\n): string {\n const byKind = new Map<ProbeAnswer[\"kind\"], ProbeAnswer[]>();\n for (const a of p.answers) {\n const bucket = byKind.get(a.kind) ?? [];\n bucket.push(a);\n byKind.set(a.kind, bucket);\n }\n\n const recognition = p.brandedRecognized\n ? \"<p>When the engines were asked about the business by name, they recognized it.</p>\"\n : businessNameUsed\n ? \"<p>When the engines were asked about the business by name, they did not recognize it — a real citation of the site never showed up, even with the name handed to them.</p>\"\n : \"<p>The engines did not recognize the business — there was no business name available to ask them with, so this reflects a harder starting point than a named query would.</p>\";\n\n const categoryCaveat = byKind.has(\"category\")\n ? \"\"\n : `<p class=\"muted\"><strong>No buyer-question (category) query was tested here</strong> — only name-recognition. An engine echoing back a name it was just given says nothing about whether a buyer who had never heard of the business would be shown it; treat the line above as a floor, not a visibility signal.</p>`;\n\n // #676. Which searches these are is a claim about how far the number travels.\n // Terms chosen with the client stay fixed between audits, so a later run\n // measures the same thing; terms the model read off the site may differ next\n // time, and a before/after built on them is comparing two instruments. The\n // report says which, rather than leaving the reader to assume the stronger one.\n const termsProvenance =\n termsSource === \"chosen\"\n ? `<p class=\"muted\">These are searches we chose with you. They stay fixed between audits, so a later run measures the same thing.</p>`\n : termsSource === \"generated\"\n ? `<p class=\"muted\">These are searches we chose from your site. A later audit may choose differently, so treat a before/after across them with care.</p>`\n : \"\";\n\n // A degraded run says so. The score now divides by what was ASKED, so failed\n // probes push it DOWN rather than silently inflating it — which is the safe\n // direction, but only if the reader is told the run was incomplete. Silence\n // here would trade one misleading number for another.\n // Bound to a local so the narrowing survives into the template — `missing > 0`\n // says nothing to the compiler about `p.categoryProbes` itself, and a stored\n // report from before the field existed genuinely does not have it.\n const tally = p.categoryProbes;\n const missing = tally ? tally.attempted - tally.answered : 0;\n const degradedNotice =\n tally && missing > 0\n ? `<p class=\"muted\"><strong>${missing} of ${tally.attempted} buyer-question searches did not come back</strong> — the engine errored on them. They are counted as \"not found\", so the visibility figure above is a floor: the real number could be higher, not lower. Worth re-running before drawing a conclusion from it.</p>`\n : \"\";\n\n const groups = KIND_ORDER.filter((k) => byKind.has(k))\n .map((kind) => {\n const answers = byKind.get(kind) ?? [];\n const cards = answers\n .map(\n (a) => `<div class=\"card\">\n <div class=\"q\">${escapeHtml(a.engine)} · “${escapeHtml(a.query)}”</div>\n <p class=\"muted\">Asked ${escapeHtml(formatIsoDate(a.askedAt))}</p>\n <p>${escapeHtml(a.snippet)}${a.truncated ? \"…\" : \"\"}</p>\n <p class=\"muted\">${\n // The scorer's own decision, recorded on the answer — NOT\n // `domainCited || brandMentioned`, which is a looser rule. Using the\n // loose one printed \"You were named in this answer\" above a card\n // contributing zero to the score beside it. Reports persisted before\n // the field existed fall back to what they already said.\n (a.countedAsVisible ?? (a.domainCited || a.brandMentioned))\n ? \"You were named in this answer.\"\n : \"You were not named in this answer.\"\n }${a.citedDomains.length ? ` Sources the engine retrieved: ${escapeHtml(a.citedDomains.join(\", \"))}` : \"\"}</p>\n </div>`,\n )\n .join(\"\");\n return `<h3>${escapeHtml(KIND_LABEL[kind])}</h3>${cards}`;\n })\n .join(\"\");\n\n // Which searches a domain actually came back on. Derived from the answers we\n // already store — no new measurement — so the report can say \"for this search,\n // these came back\" instead of one merged list disconnected from the queries\n // that produced it. Stored reports may hold full URLs rather than bare hosts,\n // so both sides go through domainOf.\n const queriesCiting = (domain: string): string[] => [\n ...new Set(\n p.answers\n .filter((a) => a.citedDomains.some((d) => domainOf(d) === domain))\n .map((a) => a.query),\n ),\n ];\n\n const attribution = (domain: string): string => {\n const queries = queriesCiting(domain);\n if (queries.length === 0) return \"\";\n return `, came back on: ${queries.map((q) => `“${escapeHtml(q)}”`).join(\", \")}`;\n };\n\n const countOf = (n: number): string => `${n} time${n === 1 ? \"\" : \"s\"}`;\n\n // Absent (a report stored before the field existed) means \"not measured\", so\n // nothing is claimed and every cited domain stays an ordinary competitor —\n // exactly what those documents already said.\n const namesakes = p.namesakes ?? [];\n const namesakeDomains = new Set(namesakes.map((n) => n.domain));\n\n const namesakeBlock = namesakes.length\n ? `<h3>A different business is using your name</h3>\n <p>The engines cited these domains, and they carry your own business name. They are not competitors — this is a brand collision, and on a search for your name it is what your buyer finds instead of you.</p>\n <ul>${namesakes\n .map(\n (n) =>\n `<li>${escapeHtml(n.domain)} — cited ${countOf(n.count)}${attribution(n.domain)}</li>`,\n )\n .join(\"\")}</ul>`\n : \"\";\n\n // A namesake already has its own section above; printing it again here as a\n // rival would file the single most valuable finding of the run under the one\n // heading that misdescribes it.\n const rivals = p.competitorsSeen.filter((c) => !namesakeDomains.has(c.domain));\n const competitors = rivals.length\n ? `<h3>Who the engines cited instead</h3><ul>${rivals\n .map(\n (c) => `<li>${escapeHtml(c.domain)} — ${countOf(c.count)}${attribution(c.domain)}</li>`,\n )\n .join(\"\")}</ul>`\n : \"\";\n\n return (\n recognition +\n categoryCaveat +\n termsProvenance +\n degradedNotice +\n groups +\n namesakeBlock +\n competitors\n );\n}\n\n/** crawlerAccessMeasured is false only when the robots.txt fetch itself\n * failed, so the blocked/allowed lists are empty out of ignorance, not\n * because we confirmed access. Saying \"every crawler can reach the site\" in\n * that case would manufacture a finding from our own missing data.\n * sitemapMeasured/llmsTxtMeasured get the same treatment as\n * crawlerAccessMeasured: a failed fetch reads as \"not measured\", never as a\n * confirmed \"missing\" — that's a claim about the prospect's site we haven't\n * earned. Item 1: none of the three \"not measured\" lines below take the raw\n * fetch error as input anymore — status codes and transport vocabulary\n * (\"503 Service Unavailable\", \"ETIMEDOUT\") are operator vocabulary, same as\n * a stage failure (see STAGE_FAILED_MESSAGE); the real error still lives in\n * the persisted JSON and CLI output, never in this client-facing page. */\nfunction buildFindabilitySection(c: ChecksResult): string {\n const accessBlock = !c.crawlerAccessMeasured\n ? `<p class=\"muted\">Crawler access: not measured — ${STAGE_FAILED_MESSAGE}.</p>`\n : c.crawlerAccess.blockedAi.length\n ? `<p><strong>Blocked AI crawlers:</strong> ${escapeHtml(\n c.crawlerAccess.blockedAi.map(describeAgent).join(\", \"),\n )}</p>`\n : // Says what was measured, which is robots.txt, and no more. \"Every AI\n // crawler can reach the site\" was a claim robots.txt cannot support:\n // a CDN's bot management enforces its own answer and can contradict the\n // file without the owner knowing. Confirmed on a live prospect: its\n // robots.txt blocked nothing relevant, and its CDN returned 403 to one\n // named AI crawler on every request while serving a browser and two\n // other crawlers normally. The report said they were fine.\n `<p>Nothing in your robots.txt blocks the AI crawlers we checked.</p>`;\n const classical =\n c.crawlerAccessMeasured && c.crawlerAccess.blockedClassical.length\n ? `<p><strong>Blocked search crawlers:</strong> ${escapeHtml(\n c.crawlerAccess.blockedClassical.join(\", \"),\n )}</p>`\n : \"\";\n const sitemapLine = c.sitemapMeasured\n ? `<li>sitemap.xml: ${c.sitemapPresent ? \"present\" : \"missing\"}</li>`\n : `<li>sitemap.xml: not measured — ${STAGE_FAILED_MESSAGE}.</li>`;\n // llms.txt is no longer listed as a finding. It used to sit here as\n // \"present\"/\"missing\" beside sitemap.xml, which put the two on equal footing\n // — and \"missing\" beside a checklist reads as a job to do. Search crawlers\n // demonstrably read a sitemap; no answer engine has documented reading\n // llms.txt to build an answer. The footnote below says that outright instead\n // of leaving a prospect to infer it, or to go and build one on our say-so.\n const llmsFootnote = `<p class=\"muted\"><strong>A note on llms.txt.</strong> You may have been told to\n add one. We check for it — this site ${\n !c.llmsTxtMeasured ? \"could not be checked\" : c.llmsTxtPresent ? \"has one\" : \"does not\"\n } — but we do not score it and we will not recommend it. It is a 2024 proposal that no\n answer engine has committed to reading, and adding one has no measured effect on whether you\n are cited. If that changes we will say so and score it then.</p>`;\n return `${accessBlock}${classical}\n <ul>\n ${sitemapLine}\n <li>Pages missing a meta description: ${c.meta.missingDescription} of ${c.meta.pageCount}</li>\n <li>Pages missing a canonical URL: ${c.meta.missingCanonical} of ${c.meta.pageCount}</li>\n <li>Pages missing share images/titles: ${c.meta.missingSocial} of ${c.meta.pageCount}</li>\n <li>Security headers missing: ${\n c.securityHeaders.missing.length ? escapeHtml(c.securityHeaders.missing.join(\", \")) : \"none\"\n }</li>\n ${\n c.meta.pagesWithoutExtract > 0\n ? `<li><strong>${c.meta.pagesWithoutExtract}</strong> additional page${\n c.meta.pagesWithoutExtract === 1 ? \"\" : \"s\"\n } produced no readable content at all and ${\n c.meta.pagesWithoutExtract === 1 ? \"is\" : \"are\"\n } not counted in the ${c.meta.pageCount} pages above.</li>`\n : \"\"\n }\n </ul>${llmsFootnote}`;\n}\n\n/** Lighthouse runs as its own independent pipeline stage (see pipeline.ts) —\n * it does not depend on `checks` succeeding. Rendering it off `result.checks`\n * would throw away real, measured performance/SEO/accessibility numbers\n * whenever an unrelated stage failed, so this reads `result.lighthouse`\n * directly and is rendered unconditionally, not nested inside the checks\n * degrade path. */\nfunction buildLighthouseBlock(lh: StageResult<LighthouseScores>): string {\n return lh.ok\n ? `<p class=\"muted\">Lighthouse — performance ${lh.data.performance ?? \"not measured\"},\n SEO ${lh.data.seo ?? \"not measured\"}, accessibility ${lh.data.accessibility ?? \"not measured\"}.</p>`\n : `<p class=\"muted\">Lighthouse not measured — ${STAGE_FAILED_MESSAGE}.</p>`;\n}\n\nfunction buildReadabilitySection(c: ChecksResult): string {\n const jsLine =\n c.jsDependence.avgMissing === null\n ? `<p class=\"muted\">JavaScript-dependence: not measured — no page produced a comparable raw/rendered pair.</p>`\n : `<p><strong>${Math.round(c.jsDependence.avgMissing * 100)}%</strong> of the words a visitor reads only appear after JavaScript runs.\n We tested this directly rather than assuming it: an assistant reading a page by URL reported text that a script wrote at runtime as not stated, three times out of three, while reading the server-rendered text on the same pages correctly every time. That share of your site is invisible to it.</p>`;\n return `${jsLine}\n <ul>\n <li>Structured data found: ${c.schema.typesFound.length ? escapeHtml(c.schema.typesFound.join(\", \")) : \"none\"}</li>\n <li>Expected structured data missing: ${\n c.schema.missingExpected.length ? escapeHtml(c.schema.missingExpected.join(\", \")) : \"none\"\n }</li>\n <li>Pages without a top-level heading: ${c.headings.pagesWithoutH1} of ${c.meta.pageCount}</li>\n ${\n c.meta.pagesWithoutExtract > 0\n ? `<li><strong>${c.meta.pagesWithoutExtract}</strong> page${\n c.meta.pagesWithoutExtract === 1 ? \"\" : \"s\"\n } produced no extract at all — excluded from every figure above, not counted as readable or unreadable.</li>`\n : \"\"\n }\n </ul>`;\n}\n\n/** The report's whole premise is claims the recipient can go check — so the\n * one quoted passage of evidence links to the page it came from (via\n * safeUrl; a null/non-http page renders as plain escaped text, no dead\n * \"#\" anchor). `answered`/`impact`/`effort` are escaped even though a zod\n * enum constrains them upstream — the renderer should not depend on\n * another file staying in sync to stay safe. */\n/** What the verdict column says. \"unknown\" is OUR gap — the model returned no\n * verdict for that question — so it must not be printed as a finding about the\n * site, and must not look like one either. Matches the web report's labels. */\nconst ANSWERED_LABEL: Record<string, string> = {\n yes: \"yes\",\n partial: \"partial\",\n no: \"no\",\n unknown: \"not measured\",\n};\n\nfunction buildAnswersSection(a: AnalyzeResult): string {\n const rows = a.buyerQuestions\n .map((q) => {\n const answered = escapeHtml(q.answered);\n const label = escapeHtml(ANSWERED_LABEL[q.answered] ?? q.answered);\n const evidenceText = q.evidence ? escapeHtml(q.evidence) : null;\n const evidenceHref = q.evidence && q.page ? safeUrl(q.page) : null;\n // An unmeasured question has no evidence for the same reason it has no\n // verdict: we never got one back. Printing \"no passage on the site\" there\n // reports our own missing measurement as a defect of theirs.\n const evidenceCell =\n q.answered === \"unknown\"\n ? '<span class=\"muted\">we did not get a verdict on this question</span>'\n : evidenceText === null\n ? '<span class=\"muted\">no passage on the site</span>'\n : evidenceHref && evidenceHref !== \"#\"\n ? `<a href=\"${escapeHtml(evidenceHref)}\" target=\"_blank\" rel=\"noopener noreferrer\">${evidenceText}</a>`\n : evidenceText;\n return `<tr>\n <td>${escapeHtml(q.question)}</td>\n <td><span class=\"tag ${answered}\">${label}</span></td>\n <td>${evidenceCell}</td>\n </tr>`;\n })\n .join(\"\");\n return `<p>${escapeHtml(a.narrative.answers)}</p>\n <table><tr><th>What buyers ask</th><th>Answered</th><th>Evidence</th></tr>${rows}</table>`;\n}\n\nexport function renderProspectReport(result: ProspectAuditResult): string {\n const host = hostnameOf(result.url);\n // `businessName` is the searchable NAME (possibly \"\"), never the\n // description. An empty/missing name is itself a finding — fall back to\n // the hostname rather than printing nothing, or worse, printing \"\" as if\n // it were a verified fact about the business.\n const name = result.businessName && result.businessName.trim() ? result.businessName : host;\n const date = formatIsoDate(result.generatedAt);\n\n const findabilitySection = stageBody(result.checks, buildFindabilitySection);\n const lighthouseBlock = buildLighthouseBlock(result.lighthouse);\n const readabilitySection = stageBody(result.checks, buildReadabilitySection);\n\n // A real business name was resolved and handed to the engines only when\n // `result.businessName` is non-empty — the same value the pipeline\n // actually queried with (see pipeline.ts). Used to gate the probes\n // section's \"even with the name handed to them\" phrasing (Fix 6).\n // Whether the engines were actually queried with the NAME, which is not the\n // same as whether a name existed. resolveBusinessName falls back to the bare\n // domain for anything it cannot use, and this line used to read the\n // un-resolved value — so a report could claim the engines were given the name\n // \"even with the name handed to them\" while the probes had in fact searched\n // for \"stlouisroofing.com\". Resolve it the same way probes.ts does.\n const businessNameUsed = Boolean(\n result.businessName &&\n result.businessName.trim() &&\n resolveBusinessName(result.businessName, result.url) === result.businessName.trim(),\n );\n\n const fixes = result.analyze.ok\n ? (() => {\n const sorted = [...result.analyze.data.fixes].sort(\n (x, y) => IMPACT_ORDER[x.impact] - IMPACT_ORDER[y.impact],\n );\n if (sorted.length === 0) {\n // A genuinely well-optimized site: a bare empty list reads as a\n // rendering bug, not a compliment. Say so directly, and keep this\n // section as a pitch — it's the one the salesperson is sending\n // the report to make.\n return `<p>Nothing here reads as urgent — the essentials are already in place.\n Reddoor can still help push this further: tightening structured data, monitoring\n how the answer engines describe you, and keeping pace as they change.</p>`;\n }\n return `<ol>${sorted\n .map((f) => {\n const impact = escapeHtml(f.impact);\n const effort = escapeHtml(f.effort);\n return `<li><strong>${escapeHtml(f.title)}</strong> — ${escapeHtml(f.why)}\n <span class=\"muted\">(${impact} impact, ${effort} effort)</span></li>`;\n })\n .join(\"\")}</ol>`;\n })()\n : skippableStageNote(\n result.analyze.error,\n ANALYZE_SKIPPED,\n \"Skipped — the checks stage failed, so there was nothing to build fixes from.\",\n );\n\n const probesSectionFinal = result.probes.ok\n ? buildProbesSection(\n result.probes.data,\n businessNameUsed,\n result.analyze.ok ? result.analyze.data.termsSource : undefined,\n )\n : skippableStageNote(\n result.probes.error,\n PROBES_SKIPPED,\n \"This audit did not run the AI-visibility probes.\",\n );\n\n const answersSectionFinal = result.analyze.ok\n ? buildAnswersSection(result.analyze.data)\n : skippableStageNote(\n result.analyze.error,\n ANALYZE_SKIPPED,\n \"Skipped — the checks stage failed, so there were no buyer questions to check.\",\n );\n\n const description = result.analyze.ok ? result.analyze.data.business : null;\n const narrative = result.analyze.ok\n ? `<p class=\"lede\">${escapeHtml(description ?? \"\")}</p>\n <p class=\"lede\">${escapeHtml(result.analyze.data.narrative.findability)}\n ${escapeHtml(result.analyze.data.narrative.readability)}</p>`\n : \"\";\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<meta name=\"robots\" content=\"noindex\" />\n<title>Can AI find ${escapeHtml(name)}? — Reddoor audit</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n<link href=\"https://fonts.googleapis.com/css2?family=Besley:wght@400;600&display=swap\" rel=\"stylesheet\" />\n<style>${STYLES}</style>\n</head>\n<body>\n<div class=\"wrap\">\n <h1>Can AI and Google actually find ${escapeHtml(name)}?</h1>\n <p class=\"lede\"><a href=\"${escapeHtml(safeUrl(result.url))}\" target=\"_blank\" rel=\"noopener noreferrer\">${escapeHtml(result.url)}</a> · audited ${escapeHtml(date)} by Reddoor Creative</p>\n ${narrative}\n\n <p class=\"muted\">Each score below runs 0–100 — read it as a score, not a percentage.</p>\n <div class=\"scores\">\n ${scoreCard(\"Findability\", result.scores.findability, \"How easily AI and search crawlers can find and reach your site — crawl rules, sitemap, key metadata\")}\n ${scoreCard(\"Readability\", result.scores.readability, \"How much of your site's content those crawlers can actually read once they're in — text your scripts write at runtime did not reach the assistant we tested\")}\n ${scoreCard(\"Answers\", result.scores.answers, \"How many buyer questions your site's own content answers — separate from what the AI engines say back\")}\n </div>\n\n <h2>What the AI engines said about you</h2>\n ${probesSectionFinal}\n\n <h2>What the crawlers can reach</h2>\n ${findabilitySection}\n ${lighthouseBlock}\n\n <h2>What the crawlers can read</h2>\n ${readabilitySection}\n\n <h2>The questions your buyers ask</h2>\n ${answersSectionFinal}\n\n <h2>What to fix first</h2>\n ${fixes}\n\n <div class=\"cta\">\n <h2>Want this fixed?</h2>\n <p>Reddoor Creative rebuilds sites so answer engines can read, quote and recommend them.\n Reply to the email this link came from, or start at\n <a href=\"https://reddoorla.com/\">reddoorla.com</a>.</p>\n </div>\n</div>\n</body>\n</html>`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,16 +2,16 @@ import {
|
|
|
2
2
|
renderReportFromRow,
|
|
3
3
|
requireLighthouse,
|
|
4
4
|
windowDays
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-Y5BVR4HW.js";
|
|
6
6
|
import "./chunk-GECESMVS.js";
|
|
7
7
|
import "./chunk-KJGKZLCT.js";
|
|
8
8
|
import "./chunk-BGTYPVLT.js";
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-MZZDMBDT.js";
|
|
10
|
+
import "./chunk-KN52PM63.js";
|
|
11
11
|
import "./chunk-RARREDQE.js";
|
|
12
12
|
export {
|
|
13
13
|
renderReportFromRow,
|
|
14
14
|
requireLighthouse,
|
|
15
15
|
windowDays
|
|
16
16
|
};
|
|
17
|
-
//# sourceMappingURL=render-from-row-
|
|
17
|
+
//# sourceMappingURL=render-from-row-65DC6ZP3.js.map
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
isDashboardVisible,
|
|
11
11
|
listWebsites,
|
|
12
12
|
updateAutoFixAttempts
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-KN52PM63.js";
|
|
14
14
|
import {
|
|
15
15
|
makeGitHub
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-43SW3FGW.js";
|
|
17
17
|
import "./chunk-LBYOLBW7.js";
|
|
18
18
|
import "./chunk-6VAL7XJT.js";
|
|
19
19
|
|
|
@@ -141,4 +141,4 @@ async function runRenovateDispatchCommand(opts) {
|
|
|
141
141
|
export {
|
|
142
142
|
runRenovateDispatchCommand
|
|
143
143
|
};
|
|
144
|
-
//# sourceMappingURL=renovate-dispatch-
|
|
144
|
+
//# sourceMappingURL=renovate-dispatch-ONJMBUHJ.js.map
|
|
@@ -1,30 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listUnreplayedDeadLetters,
|
|
3
|
+
markDeadLetterReplayed
|
|
4
|
+
} from "./chunk-A5QKNOXW.js";
|
|
1
5
|
import {
|
|
2
6
|
ingestSubmission
|
|
3
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-Z7FDJ35L.js";
|
|
4
8
|
import "./chunk-NO74DXDP.js";
|
|
5
9
|
import "./chunk-I7NHLVVI.js";
|
|
6
10
|
import "./chunk-2OP4JKIQ.js";
|
|
7
11
|
|
|
8
|
-
// src/db/deadletter.ts
|
|
9
|
-
async function listUnreplayedDeadLetters(db) {
|
|
10
|
-
const rows = await db.selectFrom("submission_deadletter").select(["id", "site_slug", "payload", "turnstile", "error", "received_at"]).where("replayed_at", "is", null).orderBy("received_at", "asc").execute();
|
|
11
|
-
return rows.map((r) => ({
|
|
12
|
-
id: r.id,
|
|
13
|
-
siteSlug: r.site_slug,
|
|
14
|
-
payload: JSON.parse(r.payload),
|
|
15
|
-
turnstile: JSON.parse(r.turnstile),
|
|
16
|
-
error: r.error,
|
|
17
|
-
receivedAt: r.received_at
|
|
18
|
-
}));
|
|
19
|
-
}
|
|
20
|
-
async function markDeadLetterReplayed(db, id, outcome, submissionId, now) {
|
|
21
|
-
await db.updateTable("submission_deadletter").set({
|
|
22
|
-
replayed_at: now.toISOString(),
|
|
23
|
-
replay_outcome: outcome,
|
|
24
|
-
replay_submission_id: submissionId
|
|
25
|
-
}).where("id", "=", id).execute();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
12
|
// src/forms/replay.ts
|
|
29
13
|
async function replayDeadLetters(db, deps) {
|
|
30
14
|
const rows = await listUnreplayedDeadLetters(db);
|
|
@@ -57,11 +41,14 @@ async function replayOne(db, deps, row) {
|
|
|
57
41
|
await markDeadLetterReplayed(db, row.id, "accepted", res.submissionId, now);
|
|
58
42
|
return { terminal: true, outcome: "accepted", submissionId: res.submissionId };
|
|
59
43
|
}
|
|
60
|
-
|
|
44
|
+
if (res.status === "unknown-site") {
|
|
45
|
+
return { terminal: false, error: `unknown-site: no fleet row for '${row.siteSlug}'` };
|
|
46
|
+
}
|
|
47
|
+
const outcome = `rejected:${res.reason}`;
|
|
61
48
|
await markDeadLetterReplayed(db, row.id, outcome, null, now);
|
|
62
49
|
return { terminal: true, outcome, submissionId: null };
|
|
63
50
|
}
|
|
64
51
|
export {
|
|
65
52
|
replayDeadLetters
|
|
66
53
|
};
|
|
67
|
-
//# sourceMappingURL=replay-
|
|
54
|
+
//# sourceMappingURL=replay-4FVFOMHH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/forms/replay.ts"],"sourcesContent":["import type { Db } from \"../db/client.js\";\nimport {\n listUnreplayedDeadLetters,\n markDeadLetterReplayed,\n type DeadLetterRow,\n} from \"../db/deadletter.js\";\nimport { ingestSubmission, type IngestDeps } from \"./ingest.js\";\n\nexport type ReplayResult = {\n /** Terminal outcomes this run: the row will never be picked up again. */\n replayed: Array<{ id: string; outcome: string; submissionId: string | null }>;\n /** Rows whose lookup threw AGAIN — left unreplayed for the next run. */\n stillFailing: Array<{ id: string; error: string }>;\n};\n\n/**\n * Run every unreplayed dead-letter back through `ingestSubmission` — the SAME\n * pipeline a live submission takes, so a replayed lead gets real spam\n * classification, notify, and fan-out, not a bespoke re-implementation.\n *\n * Each row replays with the Turnstile verification computed at receipt (tokens\n * expire in 300s; re-verifying is impossible by design, see deadletter.ts).\n *\n * `deps` must NOT wire `deadLetter`: a lookup that throws during replay would\n * otherwise dead-letter the SAME lead a second time while the original row stays\n * unreplayed — every retry would mint a duplicate. Enforced here by stripping the\n * key rather than trusting the caller. A thrown lookup leaves the row untouched\n * for the next run; `accepted` and `rejected` are terminal — re-running cannot\n * improve on an answer the store actually gave. `unknown-site` USED to be\n * terminal too and no longer is: see replayOne below (#645).\n *\n * Oldest-first (listUnreplayedDeadLetters orders by received_at) so the\n * duplicate/velocity spam signals see submissions in arrival order.\n */\nexport async function replayDeadLetters(\n db: Db,\n deps: Omit<IngestDeps, \"deadLetter\">,\n): Promise<ReplayResult> {\n const rows = await listUnreplayedDeadLetters(db);\n const result: ReplayResult = { replayed: [], stillFailing: [] };\n\n for (const row of rows) {\n const outcome = await replayOne(db, deps, row);\n if (outcome.terminal) {\n result.replayed.push({\n id: row.id,\n outcome: outcome.outcome,\n submissionId: outcome.submissionId,\n });\n } else {\n result.stillFailing.push({ id: row.id, error: outcome.error });\n }\n }\n return result;\n}\n\nasync function replayOne(\n db: Db,\n deps: Omit<IngestDeps, \"deadLetter\">,\n row: DeadLetterRow,\n): Promise<\n | { terminal: true; outcome: string; submissionId: string | null }\n | { terminal: false; error: string }\n> {\n // Strip deadLetter defensively even though the type forbids it — a structurally\n // wider object passed through `as` must not re-arm the duplicate loop.\n const { deadLetter: _stripped, ...safeDeps } = deps as IngestDeps;\n void _stripped;\n let res;\n try {\n res = await ingestSubmission(safeDeps, row.siteSlug, row.payload, row.turnstile);\n } catch (err) {\n return { terminal: false, error: String(err) };\n }\n const now = deps.now();\n if (res.status === \"accepted\") {\n await markDeadLetterReplayed(db, row.id, \"accepted\", res.submissionId, now);\n return { terminal: true, outcome: \"accepted\", submissionId: res.submissionId };\n }\n // #645. `unknown-site` is NO LONGER terminal. It used to be, on the reasoning\n // that a slug the store rejects \"can never improve\" — true while nothing could\n // create the missing row. `ensure-site` can now heal one (#645 item 1), and\n // `ingestSubmission` now dead-letters `unknown-site` leads instead of dropping\n // them, so this queue holds real client leads whose site is merely absent.\n //\n // Terminality would then be an ORDERING TRAP: replay-before-heal marks every\n // such lead replayed-and-lost, and that is the order a person reaches for\n // first. Leaving the row queued makes both orders safe.\n //\n // The cost is a slug that is genuinely gone piling up rows and holding this\n // command at exit 1. Deliberate: loud and recoverable beats silent and not.\n if (res.status === \"unknown-site\") {\n return { terminal: false, error: `unknown-site: no fleet row for '${row.siteSlug}'` };\n }\n const outcome = `rejected:${res.reason}`;\n await markDeadLetterReplayed(db, row.id, outcome, null, now);\n return { terminal: true, outcome, submissionId: null };\n}\n"],"mappings":";;;;;;;;;;;;AAkCA,eAAsB,kBACpB,IACA,MACuB;AACvB,QAAM,OAAO,MAAM,0BAA0B,EAAE;AAC/C,QAAM,SAAuB,EAAE,UAAU,CAAC,GAAG,cAAc,CAAC,EAAE;AAE9D,aAAW,OAAO,MAAM;AACtB,UAAM,UAAU,MAAM,UAAU,IAAI,MAAM,GAAG;AAC7C,QAAI,QAAQ,UAAU;AACpB,aAAO,SAAS,KAAK;AAAA,QACnB,IAAI,IAAI;AAAA,QACR,SAAS,QAAQ;AAAA,QACjB,cAAc,QAAQ;AAAA,MACxB,CAAC;AAAA,IACH,OAAO;AACL,aAAO,aAAa,KAAK,EAAE,IAAI,IAAI,IAAI,OAAO,QAAQ,MAAM,CAAC;AAAA,IAC/D;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,UACb,IACA,MACA,KAIA;AAGA,QAAM,EAAE,YAAY,WAAW,GAAG,SAAS,IAAI;AAC/C,OAAK;AACL,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,iBAAiB,UAAU,IAAI,UAAU,IAAI,SAAS,IAAI,SAAS;AAAA,EACjF,SAAS,KAAK;AACZ,WAAO,EAAE,UAAU,OAAO,OAAO,OAAO,GAAG,EAAE;AAAA,EAC/C;AACA,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,IAAI,WAAW,YAAY;AAC7B,UAAM,uBAAuB,IAAI,IAAI,IAAI,YAAY,IAAI,cAAc,GAAG;AAC1E,WAAO,EAAE,UAAU,MAAM,SAAS,YAAY,cAAc,IAAI,aAAa;AAAA,EAC/E;AAaA,MAAI,IAAI,WAAW,gBAAgB;AACjC,WAAO,EAAE,UAAU,OAAO,OAAO,mCAAmC,IAAI,QAAQ,IAAI;AAAA,EACtF;AACA,QAAM,UAAU,YAAY,IAAI,MAAM;AACtC,QAAM,uBAAuB,IAAI,IAAI,IAAI,SAAS,MAAM,GAAG;AAC3D,SAAO,EAAE,UAAU,MAAM,SAAS,cAAc,KAAK;AACvD;","names":[]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
draftReportForSite,
|
|
6
6
|
readGaConfig
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-72FJZ6LO.js";
|
|
8
8
|
import {
|
|
9
9
|
reportTier
|
|
10
10
|
} from "./chunk-YFAXOD7Z.js";
|
|
@@ -26,12 +26,12 @@ import "./chunk-KJGKZLCT.js";
|
|
|
26
26
|
import {
|
|
27
27
|
escapeHtml
|
|
28
28
|
} from "./chunk-BGTYPVLT.js";
|
|
29
|
-
import "./chunk-
|
|
29
|
+
import "./chunk-MZZDMBDT.js";
|
|
30
30
|
import {
|
|
31
31
|
listWebsites,
|
|
32
32
|
siteSlug,
|
|
33
33
|
updateNextDueDates
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-KN52PM63.js";
|
|
35
35
|
import "./chunk-RARREDQE.js";
|
|
36
36
|
import "./chunk-6VAL7XJT.js";
|
|
37
37
|
|
|
@@ -78,24 +78,26 @@ function dashboardBaseUrl() {
|
|
|
78
78
|
}
|
|
79
79
|
async function runReportCommand(slug, opts) {
|
|
80
80
|
if (opts.digest) {
|
|
81
|
-
const { runDigest } = await import("./digest-
|
|
81
|
+
const { runDigest } = await import("./digest-5AJGWS33.js");
|
|
82
82
|
return runDigest({ baseUrl: dashboardBaseUrl() });
|
|
83
83
|
}
|
|
84
84
|
if (opts.sendReady) {
|
|
85
|
-
const { sendApprovedReports } = await import("./orchestrate-
|
|
86
|
-
const { makeSiteMirror } = await import("./site-mirror-
|
|
87
|
-
const { mirrorWrite } = await import("./freeze-
|
|
88
|
-
const { openDb, readDbConfig } = await import("./client-
|
|
89
|
-
const { mirrorReportPatch } = await import("./fleet-state-
|
|
85
|
+
const { sendApprovedReports } = await import("./orchestrate-TGDFGY52.js");
|
|
86
|
+
const { makeSiteMirror } = await import("./site-mirror-H2FYMN55.js");
|
|
87
|
+
const { mirrorWrite } = await import("./freeze-GUQ43EAU.js");
|
|
88
|
+
const { openDb, readDbConfig } = await import("./client-L7NAUG6J.js");
|
|
89
|
+
const { mirrorReportPatch } = await import("./fleet-state-GJWGKTLD.js");
|
|
90
90
|
return sendApprovedReports({
|
|
91
91
|
siteMirror: await makeSiteMirror(),
|
|
92
92
|
// stampSent's Turso shadow, routed through mirrorWrite so the freeze
|
|
93
93
|
// switch owns the semantics: strict rethrows and the send loop reds the
|
|
94
94
|
// run. Mirrors stampSent exactly — the 409 replay path leaves Airtable's
|
|
95
|
-
// `Resend message ID` untouched, so the shadow omits it there too.
|
|
95
|
+
// `Resend message ID` untouched, so the shadow omits it there too. The
|
|
96
|
+
// row count is handed through (#647): a stamp for a report row Turso
|
|
97
|
+
// never held is `missed`, not a green no-op.
|
|
96
98
|
reportSentMirror: (reportId, sentAt, messageId) => mirrorWrite(`stamp-sent ${reportId}`, async () => {
|
|
97
99
|
const db = await openDb(readDbConfig());
|
|
98
|
-
|
|
100
|
+
return mirrorReportPatch(db, reportId, {
|
|
99
101
|
sent_at: sentAt.toISOString(),
|
|
100
102
|
...messageId !== null ? { resend_message_id: messageId } : {}
|
|
101
103
|
});
|
|
@@ -104,9 +106,9 @@ async function runReportCommand(slug, opts) {
|
|
|
104
106
|
}
|
|
105
107
|
if (opts.rerender) {
|
|
106
108
|
const { rerenderReport, formatRerenderResult } = await import("./rerender-3FM6DNRH.js");
|
|
107
|
-
const { renderReportFromRow } = await import("./render-from-row-
|
|
108
|
-
const { openDb, readDbConfig } = await import("./client-
|
|
109
|
-
const { getReportById, getSiteById, storeRenderedHtml } = await import("./fleet-state-
|
|
109
|
+
const { renderReportFromRow } = await import("./render-from-row-65DC6ZP3.js");
|
|
110
|
+
const { openDb, readDbConfig } = await import("./client-L7NAUG6J.js");
|
|
111
|
+
const { getReportById, getSiteById, storeRenderedHtml } = await import("./fleet-state-GJWGKTLD.js");
|
|
110
112
|
const { loadHeaderImage } = await import("./header-images-SQBCE5OD.js");
|
|
111
113
|
const { fetchAttachmentBytes } = await import("./attachments-NLZIJ5NU.js");
|
|
112
114
|
const db = await openDb(readDbConfig());
|
|
@@ -145,9 +147,9 @@ async function runReportCommand(slug, opts) {
|
|
|
145
147
|
}
|
|
146
148
|
async function runDueDraft() {
|
|
147
149
|
const base = openBase(readAirtableConfig());
|
|
148
|
-
const { makeScheduleMirrorBestEffort } = await import("./health-mirror-
|
|
149
|
-
const { makeReportMirror } = await import("./report-mirror-
|
|
150
|
-
const { makeSiteMirror } = await import("./site-mirror-
|
|
150
|
+
const { makeScheduleMirrorBestEffort } = await import("./health-mirror-ONRT4DQ5.js");
|
|
151
|
+
const { makeReportMirror } = await import("./report-mirror-KKZZ6RMZ.js");
|
|
152
|
+
const { makeSiteMirror } = await import("./site-mirror-H2FYMN55.js");
|
|
151
153
|
const result = await draftDueReports(
|
|
152
154
|
base,
|
|
153
155
|
/* @__PURE__ */ new Date(),
|
|
@@ -337,8 +339,8 @@ async function runSingleSiteDraft(slug, opts) {
|
|
|
337
339
|
if (!site) {
|
|
338
340
|
throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
|
|
339
341
|
}
|
|
340
|
-
const reportMirror = opts.previewOnly ? null : await (await import("./report-mirror-
|
|
341
|
-
const siteMirror = opts.previewOnly ? null : await (await import("./site-mirror-
|
|
342
|
+
const reportMirror = opts.previewOnly ? null : await (await import("./report-mirror-KKZZ6RMZ.js")).makeReportMirror();
|
|
343
|
+
const siteMirror = opts.previewOnly ? null : await (await import("./site-mirror-H2FYMN55.js")).makeSiteMirror();
|
|
342
344
|
const result = await draftReportForSite(opts.previewOnly ? null : base, site, opts.reportType, {
|
|
343
345
|
previewOnly: opts.previewOnly,
|
|
344
346
|
// Only forced on. Left undefined, draftReportForSite keeps its default
|
|
@@ -366,4 +368,4 @@ export {
|
|
|
366
368
|
runReportCommand,
|
|
367
369
|
writeNextDueDates
|
|
368
370
|
};
|
|
369
|
-
//# sourceMappingURL=report-
|
|
371
|
+
//# sourceMappingURL=report-RC5LGPU3.js.map
|