@reddoorla/maintenance 0.95.1 → 0.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -7
- package/dist/{announce-CLQYX5I4.js → announce-5ZD2SBGD.js} +39 -31
- package/dist/announce-5ZD2SBGD.js.map +1 -0
- package/dist/attachments-JFGFQRXQ.js +10 -0
- package/dist/{blux-4KNXUP2K.js → blux-NXQLMS4W.js} +1 -1
- package/dist/{bump-deps-C5G2EFBB.js → bump-deps-775VLDYB.js} +6 -6
- package/dist/{chunk-FUWFLAWQ.js → chunk-3RLDX3Q2.js} +5 -5
- package/dist/{chunk-XHHMM5HX.js → chunk-43SW3FGW.js} +63 -1
- package/dist/chunk-43SW3FGW.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-S7RYFUIK.js → chunk-6PGXKN6F.js} +2 -2
- package/dist/{chunk-HTABUCF4.js → chunk-6THEZQLA.js} +3 -3
- package/dist/chunk-A5QKNOXW.js +72 -0
- package/dist/chunk-A5QKNOXW.js.map +1 -0
- package/dist/chunk-ASLDCWBF.js +41 -0
- package/dist/chunk-ASLDCWBF.js.map +1 -0
- package/dist/{chunk-CWXTNPNZ.js → chunk-AYDQQBNG.js} +3 -3
- package/dist/chunk-AYDQQBNG.js.map +1 -0
- package/dist/{chunk-WVNDEYXL.js → chunk-BINFZQ2T.js} +57 -6
- package/dist/chunk-BINFZQ2T.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-YLYXW5PH.js → chunk-CXICCLNM.js} +34 -4
- package/dist/chunk-CXICCLNM.js.map +1 -0
- package/dist/{chunk-BYGPUUCT.js → chunk-DRNZVORX.js} +66 -30
- package/dist/chunk-DRNZVORX.js.map +1 -0
- package/dist/{chunk-LM2NHRCJ.js → chunk-DYTYPKFN.js} +4 -4
- package/dist/chunk-DYTYPKFN.js.map +1 -0
- package/dist/{chunk-DFKBTBO3.js → chunk-EN6CTBUN.js} +35 -6
- package/dist/chunk-EN6CTBUN.js.map +1 -0
- package/dist/{chunk-5EEMTJMT.js → chunk-FHFXJ3EM.js} +8 -6
- package/dist/chunk-FHFXJ3EM.js.map +1 -0
- package/dist/{chunk-Z2K4JPQ3.js → chunk-FP6HOW6J.js} +10 -7
- package/dist/chunk-FP6HOW6J.js.map +1 -0
- package/dist/{chunk-2LYOAB2E.js → chunk-HSMN35LU.js} +2 -2
- package/dist/chunk-ICSRWW5Q.js +148 -0
- package/dist/chunk-ICSRWW5Q.js.map +1 -0
- package/dist/{chunk-FSGICFXB.js → chunk-IDBPII6G.js} +17 -5
- package/dist/chunk-IDBPII6G.js.map +1 -0
- package/dist/{chunk-C3IQQKXL.js → chunk-IOTOOTII.js} +121 -1
- package/dist/{chunk-C3IQQKXL.js.map → chunk-IOTOOTII.js.map} +1 -1
- package/dist/{chunk-BCKEP5S6.js → chunk-JHJNNJLG.js} +2 -2
- package/dist/{chunk-TO7DJYLZ.js → chunk-L3S3QZXL.js} +178 -39
- package/dist/chunk-L3S3QZXL.js.map +1 -0
- package/dist/{chunk-WSJ7RO54.js → chunk-LAG6IP7Y.js} +23 -5
- package/dist/chunk-LAG6IP7Y.js.map +1 -0
- package/dist/{chunk-WJMPCVOR.js → chunk-LAJ3FF54.js} +12 -4
- package/dist/chunk-LAJ3FF54.js.map +1 -0
- package/dist/{chunk-2ZAZ7ETT.js → chunk-LDHRIQUW.js} +32 -18
- package/dist/chunk-LDHRIQUW.js.map +1 -0
- package/dist/{chunk-6R56OKJZ.js → chunk-LF2TJN4X.js} +2 -2
- package/dist/chunk-LF2TJN4X.js.map +1 -0
- package/dist/{chunk-ZQV7UQNE.js → chunk-LUF52EIY.js} +2 -2
- package/dist/{chunk-5UINJNAJ.js → chunk-LZMAG73U.js} +34 -21
- package/dist/chunk-LZMAG73U.js.map +1 -0
- package/dist/{chunk-VSGMCCGQ.js → chunk-MMSMZC7S.js} +2 -2
- package/dist/chunk-MVKNU4QD.js +37 -0
- package/dist/chunk-MVKNU4QD.js.map +1 -0
- package/dist/{chunk-XPJQXSC4.js → chunk-P7323YVW.js} +27 -5
- package/dist/chunk-P7323YVW.js.map +1 -0
- package/dist/{chunk-KJRVMMM3.js → chunk-PARKXDXI.js} +36 -5
- package/dist/chunk-PARKXDXI.js.map +1 -0
- package/dist/{chunk-4763KQ6O.js → chunk-PXRN2MER.js} +2 -2
- package/dist/{chunk-WMBLR5VA.js → chunk-PZZ6HM5T.js} +3 -3
- package/dist/{chunk-TR3HFH6J.js → chunk-QFSL2L2B.js} +67 -10
- package/dist/chunk-QFSL2L2B.js.map +1 -0
- package/dist/chunk-RQJFGK3A.js +51 -0
- package/dist/chunk-RQJFGK3A.js.map +1 -0
- package/dist/{chunk-3Z6X3BHU.js → chunk-TC4GANMX.js} +53 -150
- package/dist/chunk-TC4GANMX.js.map +1 -0
- package/dist/{chunk-F3JWOPTG.js → chunk-TEMQPSCM.js} +45 -105
- package/dist/chunk-TEMQPSCM.js.map +1 -0
- package/dist/chunk-TJLKWQOX.js +52 -0
- package/dist/chunk-TJLKWQOX.js.map +1 -0
- package/dist/{chunk-CJ2RMTT7.js → chunk-TV76A6GN.js} +2 -2
- package/dist/{chunk-KZRR4NFG.js → chunk-TVZ6QCOE.js} +3 -3
- package/dist/{chunk-BVEDKMNG.js → chunk-U2ZI6G7H.js} +29 -11
- package/dist/chunk-U2ZI6G7H.js.map +1 -0
- package/dist/chunk-UPE6D4AT.js +69 -0
- package/dist/chunk-UPE6D4AT.js.map +1 -0
- package/dist/{chunk-R5IMFAFR.js → chunk-VMGHQUTV.js} +7 -16
- package/dist/chunk-VMGHQUTV.js.map +1 -0
- package/dist/chunk-VQMAAQ5M.js +53 -0
- package/dist/chunk-VQMAAQ5M.js.map +1 -0
- package/dist/{chunk-AVEQUR44.js → chunk-WMF5S2MB.js} +60 -17
- package/dist/chunk-WMF5S2MB.js.map +1 -0
- 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-XN37VTDB.js → chunk-X56LAKPR.js} +3 -3
- package/dist/chunk-X56LAKPR.js.map +1 -0
- package/dist/{chunk-ZCL7C5LH.js → chunk-Y6FCXRIC.js} +1661 -387
- 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-IY6256GK.js → chunk-YJBSB4LP.js} +6 -1
- package/dist/chunk-YJBSB4LP.js.map +1 -0
- package/dist/{chunk-BI2VI6EM.js → chunk-YXWBM26P.js} +2 -2
- package/dist/{chunk-QF6IQQFP.js → chunk-Z7FDJ35L.js} +18 -12
- package/dist/chunk-Z7FDJ35L.js.map +1 -0
- package/dist/cli/bin.js +111 -51
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +33 -3
- package/dist/cli/commands/audit.js +28 -25
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-DGTO65WB.js +11 -0
- 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-KLWA53N5.js} +6 -6
- package/dist/{db-XKZYDCKM.js → db-II5WAK4D.js} +45 -20
- package/dist/db-II5WAK4D.js.map +1 -0
- package/dist/deadletter-G4M3HSVM.js +17 -0
- package/dist/{digest-7UBGUBRK.js → digest-CWKK2DMR.js} +56 -32
- package/dist/digest-CWKK2DMR.js.map +1 -0
- package/dist/{digest-collectors-DJFN5ZDK.js → digest-collectors-CYTN2EUX.js} +17 -12
- 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-2QZHFVQ3.js +70 -0
- package/dist/ensure-site-2QZHFVQ3.js.map +1 -0
- package/dist/ensure-site-JEKV2ODE.js +105 -0
- package/dist/ensure-site-JEKV2ODE.js.map +1 -0
- package/dist/{ensure-site-OAAE5YE2.js → ensure-site-OO4UPWNQ.js} +49 -44
- package/dist/ensure-site-OO4UPWNQ.js.map +1 -0
- package/dist/fleet-events-CkOZpMVC.d.ts +435 -0
- package/dist/{fleet-state-UEVZ3X3J.js → fleet-state-RJROYXKO.js} +19 -7
- package/dist/{forms-notify-target-UC6PF3AE.js → forms-notify-target-X6UEOI2M.js} +19 -15
- package/dist/forms-notify-target-X6UEOI2M.js.map +1 -0
- package/dist/{freeze-A6MUCZU4.js → freeze-GUQ43EAU.js} +2 -2
- package/dist/{github-signals-TX5FPYP6.js → github-signals-QIRTHHET.js} +22 -17
- package/dist/github-signals-QIRTHHET.js.map +1 -0
- package/dist/{header-image-PSPT7CUM.js → header-image-H7ZJAV26.js} +15 -18
- package/dist/header-image-H7ZJAV26.js.map +1 -0
- package/dist/{health-endpoint-KVCJYWET.js → health-endpoint-BJ6BKRHF.js} +8 -7
- package/dist/{health-endpoint-KVCJYWET.js.map → health-endpoint-BJ6BKRHF.js.map} +1 -1
- package/dist/{health-mirror-XJAKA3EY.js → health-mirror-KU3G7V2Y.js} +10 -10
- package/dist/{import-airtable-TAKSN4NV.js → import-airtable-UBF5GYAL.js} +3 -3
- package/dist/index.d.ts +158 -419
- package/dist/index.js +91 -46
- package/dist/index.js.map +1 -1
- package/dist/{init-FFE4FCO2.js → init-GOWDA5QS.js} +19 -18
- package/dist/{init-FFE4FCO2.js.map → init-GOWDA5QS.js.map} +1 -1
- package/dist/{launch-UD36AHUN.js → launch-7N3X2MXB.js} +229 -79
- package/dist/launch-7N3X2MXB.js.map +1 -0
- package/dist/{lighthouse-VHI77PFB.js → lighthouse-52WSPZ5D.js} +2 -2
- package/dist/lighthouse-airtable-M45OPVRV.js +14 -0
- package/dist/match-harness-GBCDSVPS.js +417 -0
- package/dist/match-harness-GBCDSVPS.js.map +1 -0
- package/dist/migrate-XZ7U5DFC.js +7 -0
- package/dist/{notify-IISTHNBR.js → notify-K3WVH3DM.js} +4 -4
- package/dist/{onboard-TM2KESGF.js → onboard-JQQIMQW7.js} +6 -6
- package/dist/{orchestrate-HYVQPRCE.js → orchestrate-L2A5BZFY.js} +13 -10
- package/dist/parity-2JGVUCZD.js +14 -0
- package/dist/{pipeline-YROAY6DT.js → pipeline-UUPXOS2J.js} +3 -3
- package/dist/{preflight-RYIP54NY.js → preflight-O7Q6FWHU.js} +35 -11
- package/dist/preflight-O7Q6FWHU.js.map +1 -0
- package/dist/{prismic-ci-6GKKZGB5.js → prismic-ci-NAPWOXXD.js} +39 -9
- package/dist/prismic-ci-NAPWOXXD.js.map +1 -0
- package/dist/{prismic-models-P6FWWVRS.js → prismic-models-LL6ORTQ2.js} +20 -17
- package/dist/prismic-models-LL6ORTQ2.js.map +1 -0
- package/dist/prospect/types.d.ts +72 -0
- package/dist/prospect/types.js +1 -1
- package/dist/{prospect-audit-PRHU53HS.js → prospect-audit-FMPGQIRQ.js} +16 -7
- package/dist/prospect-audit-FMPGQIRQ.js.map +1 -0
- package/dist/{prospect-audits-R3LWF6U7.js → prospect-audits-IZVQY3OQ.js} +4 -2
- package/dist/protection-audit-YXJKFU6O.js +413 -0
- package/dist/protection-audit-YXJKFU6O.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-75JTED2Z.js} +6 -4
- package/dist/{renovate-dispatch-U5Q52SKT.js → renovate-dispatch-2IHAZDVA.js} +14 -9
- package/dist/{renovate-dispatch-U5Q52SKT.js.map → renovate-dispatch-2IHAZDVA.js.map} +1 -1
- package/dist/{replay-PCSQOGCZ.js → replay-4FVFOMHH.js} +10 -23
- package/dist/replay-4FVFOMHH.js.map +1 -0
- package/dist/{report-LQS4ECTM.js → report-IAHXN5CW.js} +103 -57
- package/dist/report-IAHXN5CW.js.map +1 -0
- package/dist/{report-mirror-HDTOJE64.js → report-mirror-4QM57LMS.js} +34 -12
- package/dist/report-mirror-4QM57LMS.js.map +1 -0
- package/dist/roster-GDNSIRAK.js +19 -0
- package/dist/roster-GDNSIRAK.js.map +1 -0
- package/dist/{schema-A4WCPIF4.js → schema-KWUQEZ5O.js} +1 -1
- package/dist/schema-KWUQEZ5O.js.map +1 -0
- package/dist/{self-updating-MWXIZLL7.js → self-updating-ZUWPGYUP.js} +8 -7
- package/dist/{self-updating-MWXIZLL7.js.map → self-updating-ZUWPGYUP.js.map} +1 -1
- package/dist/{selftest-VXTHRPRN.js → selftest-G4W3LIBQ.js} +21 -17
- package/dist/{selftest-VXTHRPRN.js.map → selftest-G4W3LIBQ.js.map} +1 -1
- package/dist/site-create-ZU3ACBTO.js +45 -0
- package/dist/site-create-ZU3ACBTO.js.map +1 -0
- package/dist/site-lookup-DTOLJ4EA.js +27 -0
- package/dist/site-lookup-DTOLJ4EA.js.map +1 -0
- package/dist/{site-mirror-WON4FTJ2.js → site-mirror-WR7ACXOV.js} +26 -12
- package/dist/site-mirror-WR7ACXOV.js.map +1 -0
- package/dist/{smoke-suite-7DGITCTZ.js → smoke-suite-WAUPI6EJ.js} +8 -7
- package/dist/{smoke-suite-7DGITCTZ.js.map → smoke-suite-WAUPI6EJ.js.map} +1 -1
- package/dist/{submissions-OVZPVBYG.js → submissions-2SHCFOKD.js} +12 -7
- package/dist/submissions-2SHCFOKD.js.map +1 -0
- 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-VOLOKKFI.js} +6 -6
- package/dist/{sync-K3PKHVLH.js → sync-F4USL4K5.js} +6 -6
- package/dist/{sync-configs-RRIHNLIJ.js → sync-configs-OXSWKVZ2.js} +6 -6
- package/dist/turso-KQKBBQCK.js +24 -0
- package/dist/turso-KQKBBQCK.js.map +1 -0
- package/dist/{upgrade-VTD6V4HS.js → upgrade-EVAL3JTO.js} +6 -6
- package/dist/util/git.d.ts +49 -10
- package/dist/util/git.js +1 -1
- package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
- package/dist/{websites-TB4TCAWM.js → websites-Y6FN2XFD.js} +27 -20
- package/dist/{write-audits-to-airtable-53EVXKMU.js → write-audits-to-airtable-TI3JMWSY.js} +7 -5
- package/package.json +3 -2
- package/dist/airtable-JGCPS7JH.js +0 -9
- package/dist/announce-CLQYX5I4.js.map +0 -1
- package/dist/attachments-NLZIJ5NU.js +0 -9
- package/dist/chunk-2ZAZ7ETT.js.map +0 -1
- package/dist/chunk-3Z6X3BHU.js.map +0 -1
- package/dist/chunk-5EEMTJMT.js.map +0 -1
- package/dist/chunk-5GUJ36TL.js +0 -53
- package/dist/chunk-5GUJ36TL.js.map +0 -1
- package/dist/chunk-5UINJNAJ.js.map +0 -1
- package/dist/chunk-6R56OKJZ.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-F3JWOPTG.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-IY6256GK.js.map +0 -1
- package/dist/chunk-KJRVMMM3.js.map +0 -1
- package/dist/chunk-LM2NHRCJ.js.map +0 -1
- package/dist/chunk-QF6IQQFP.js.map +0 -1
- package/dist/chunk-R5IMFAFR.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-XN37VTDB.js.map +0 -1
- package/dist/chunk-XPJQXSC4.js.map +0 -1
- package/dist/chunk-YFAXOD7Z.js +0 -42
- package/dist/chunk-YFAXOD7Z.js.map +0 -1
- package/dist/chunk-YLYXW5PH.js.map +0 -1
- package/dist/chunk-Z2K4JPQ3.js.map +0 -1
- package/dist/chunk-ZCL7C5LH.js.map +0 -1
- package/dist/client-G2WGWPCA.js +0 -11
- package/dist/db-XKZYDCKM.js.map +0 -1
- package/dist/digest-7UBGUBRK.js.map +0 -1
- package/dist/digest-state-3KHZF7GD.js.map +0 -1
- package/dist/ensure-site-OAAE5YE2.js.map +0 -1
- package/dist/fleet-events-cF2Om88w.d.ts +0 -24
- package/dist/forms-notify-target-UC6PF3AE.js.map +0 -1
- package/dist/github-signals-TX5FPYP6.js.map +0 -1
- package/dist/header-image-PSPT7CUM.js.map +0 -1
- package/dist/launch-UD36AHUN.js.map +0 -1
- package/dist/lighthouse-airtable-YMH4UA7R.js +0 -12
- package/dist/match-harness-QRG6XZAR.js +0 -70
- package/dist/match-harness-QRG6XZAR.js.map +0 -1
- package/dist/migrate-Y7R4CESL.js +0 -7
- package/dist/parity-CAJ477PS.js +0 -14
- package/dist/preflight-RYIP54NY.js.map +0 -1
- package/dist/prismic-ci-6GKKZGB5.js.map +0 -1
- package/dist/prismic-models-P6FWWVRS.js.map +0 -1
- package/dist/prospect-audit-PRHU53HS.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-LQS4ECTM.js.map +0 -1
- package/dist/report-mirror-HDTOJE64.js.map +0 -1
- package/dist/schema-A4WCPIF4.js.map +0 -1
- package/dist/site-mirror-WON4FTJ2.js.map +0 -1
- package/dist/submissions-3I3GAQAX.js.map +0 -1
- package/dist/submissions-OVZPVBYG.js.map +0 -1
- /package/dist/{airtable-JGCPS7JH.js.map → attachments-JFGFQRXQ.js.map} +0 -0
- /package/dist/{blux-4KNXUP2K.js.map → blux-NXQLMS4W.js.map} +0 -0
- /package/dist/{bump-deps-C5G2EFBB.js.map → bump-deps-775VLDYB.js.map} +0 -0
- /package/dist/{chunk-FUWFLAWQ.js.map → chunk-3RLDX3Q2.js.map} +0 -0
- /package/dist/{chunk-S7RYFUIK.js.map → chunk-6PGXKN6F.js.map} +0 -0
- /package/dist/{chunk-HTABUCF4.js.map → chunk-6THEZQLA.js.map} +0 -0
- /package/dist/{chunk-2LYOAB2E.js.map → chunk-HSMN35LU.js.map} +0 -0
- /package/dist/{chunk-BCKEP5S6.js.map → chunk-JHJNNJLG.js.map} +0 -0
- /package/dist/{chunk-ZQV7UQNE.js.map → chunk-LUF52EIY.js.map} +0 -0
- /package/dist/{chunk-VSGMCCGQ.js.map → chunk-MMSMZC7S.js.map} +0 -0
- /package/dist/{chunk-4763KQ6O.js.map → chunk-PXRN2MER.js.map} +0 -0
- /package/dist/{chunk-WMBLR5VA.js.map → chunk-PZZ6HM5T.js.map} +0 -0
- /package/dist/{chunk-CJ2RMTT7.js.map → chunk-TV76A6GN.js.map} +0 -0
- /package/dist/{chunk-KZRR4NFG.js.map → chunk-TVZ6QCOE.js.map} +0 -0
- /package/dist/{chunk-BI2VI6EM.js.map → chunk-YXWBM26P.js.map} +0 -0
- /package/dist/{attachments-NLZIJ5NU.js.map → client-DGTO65WB.js.map} +0 -0
- /package/dist/{convert-to-pnpm-YTFREUJK.js.map → convert-to-pnpm-KLWA53N5.js.map} +0 -0
- /package/dist/{client-G2WGWPCA.js.map → deadletter-G4M3HSVM.js.map} +0 -0
- /package/dist/{digest-collectors-DJFN5ZDK.js.map → digest-collectors-CYTN2EUX.js.map} +0 -0
- /package/dist/{fleet-state-UEVZ3X3J.js.map → fleet-state-RJROYXKO.js.map} +0 -0
- /package/dist/{freeze-A6MUCZU4.js.map → freeze-GUQ43EAU.js.map} +0 -0
- /package/dist/{health-mirror-XJAKA3EY.js.map → health-mirror-KU3G7V2Y.js.map} +0 -0
- /package/dist/{import-airtable-TAKSN4NV.js.map → import-airtable-UBF5GYAL.js.map} +0 -0
- /package/dist/{lighthouse-VHI77PFB.js.map → lighthouse-52WSPZ5D.js.map} +0 -0
- /package/dist/{lighthouse-airtable-YMH4UA7R.js.map → lighthouse-airtable-M45OPVRV.js.map} +0 -0
- /package/dist/{migrate-Y7R4CESL.js.map → migrate-XZ7U5DFC.js.map} +0 -0
- /package/dist/{notify-IISTHNBR.js.map → notify-K3WVH3DM.js.map} +0 -0
- /package/dist/{onboard-TM2KESGF.js.map → onboard-JQQIMQW7.js.map} +0 -0
- /package/dist/{orchestrate-HYVQPRCE.js.map → orchestrate-L2A5BZFY.js.map} +0 -0
- /package/dist/{parity-CAJ477PS.js.map → parity-2JGVUCZD.js.map} +0 -0
- /package/dist/{pipeline-YROAY6DT.js.map → pipeline-UUPXOS2J.js.map} +0 -0
- /package/dist/{prospect-audits-R3LWF6U7.js.map → prospect-audits-IZVQY3OQ.js.map} +0 -0
- /package/dist/{render-from-row-IFSBHWDH.js.map → render-from-row-75JTED2Z.js.map} +0 -0
- /package/dist/{svelte-codemods-WSV2FGNG.js.map → svelte-codemods-VOLOKKFI.js.map} +0 -0
- /package/dist/{sync-K3PKHVLH.js.map → sync-F4USL4K5.js.map} +0 -0
- /package/dist/{sync-configs-RRIHNLIJ.js.map → sync-configs-OXSWKVZ2.js.map} +0 -0
- /package/dist/{upgrade-VTD6V4HS.js.map → upgrade-EVAL3JTO.js.map} +0 -0
- /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
- /package/dist/{websites-TB4TCAWM.js.map → websites-Y6FN2XFD.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-53EVXKMU.js.map → write-audits-to-airtable-TI3JMWSY.js.map} +0 -0
|
@@ -21,6 +21,10 @@ function siteKey(url) {
|
|
|
21
21
|
return raw;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
function jsonListOrNull(xs) {
|
|
25
|
+
const cleaned = (xs ?? []).map((x) => x.trim()).filter((x) => x !== "");
|
|
26
|
+
return cleaned.length === 0 ? null : JSON.stringify(cleaned);
|
|
27
|
+
}
|
|
24
28
|
async function createProspectAudit(db, audit) {
|
|
25
29
|
const id = newProspectAuditId();
|
|
26
30
|
const token = generateToken();
|
|
@@ -32,7 +36,9 @@ async function createProspectAudit(db, audit) {
|
|
|
32
36
|
business: audit.business,
|
|
33
37
|
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
34
38
|
status: audit.status ?? "complete",
|
|
35
|
-
result_json: audit.resultJson
|
|
39
|
+
result_json: audit.resultJson,
|
|
40
|
+
chosen_terms: jsonListOrNull(audit.chosenTerms),
|
|
41
|
+
chosen_questions: jsonListOrNull(audit.chosenQuestions)
|
|
36
42
|
}).execute();
|
|
37
43
|
return { id, token };
|
|
38
44
|
}
|
|
@@ -46,7 +52,9 @@ async function getProspectAuditByToken(db, token) {
|
|
|
46
52
|
"result_json",
|
|
47
53
|
"overrides_json",
|
|
48
54
|
"edited_at",
|
|
49
|
-
"opened_at"
|
|
55
|
+
"opened_at",
|
|
56
|
+
"chosen_terms",
|
|
57
|
+
"chosen_questions"
|
|
50
58
|
]).where("token", "=", token).executeTakeFirst();
|
|
51
59
|
return row ?? null;
|
|
52
60
|
}
|
|
@@ -56,7 +64,17 @@ function clampLimit(limit) {
|
|
|
56
64
|
return Math.min(Math.max(n, 1), MAX_RECENT_PROSPECT_AUDITS);
|
|
57
65
|
}
|
|
58
66
|
async function listRecentProspectAudits(db, limit) {
|
|
59
|
-
return db.selectFrom("prospect_audits").select([
|
|
67
|
+
return db.selectFrom("prospect_audits").select([
|
|
68
|
+
"id",
|
|
69
|
+
"token",
|
|
70
|
+
"url",
|
|
71
|
+
"business",
|
|
72
|
+
"status",
|
|
73
|
+
"created_at",
|
|
74
|
+
"edited_at",
|
|
75
|
+
"opened_at",
|
|
76
|
+
"chosen_terms"
|
|
77
|
+
]).orderBy("created_at", "desc").limit(clampLimit(limit)).execute();
|
|
60
78
|
}
|
|
61
79
|
var OVERRIDES_MAX_LEN = 512e3;
|
|
62
80
|
function buildOverrideMap(v) {
|
|
@@ -92,8 +110,11 @@ async function setProspectAuditOverrides(db, token, overrides) {
|
|
|
92
110
|
await db.updateTable("prospect_audits").set({ overrides_json: serialised, edited_at: (/* @__PURE__ */ new Date()).toISOString() }).where("token", "=", token).execute();
|
|
93
111
|
return { status: "updated", token };
|
|
94
112
|
}
|
|
113
|
+
var OPENED_AT_COALESCE_MS = 5 * 60 * 1e3;
|
|
95
114
|
async function touchProspectAuditOpened(db, token) {
|
|
96
|
-
|
|
115
|
+
const now = /* @__PURE__ */ new Date();
|
|
116
|
+
const staleBefore = new Date(now.getTime() - OPENED_AT_COALESCE_MS).toISOString();
|
|
117
|
+
await db.updateTable("prospect_audits").set({ opened_at: now.toISOString() }).where("token", "=", token).where((eb) => eb.or([eb("opened_at", "is", null), eb("opened_at", "<", staleBefore)])).execute();
|
|
97
118
|
}
|
|
98
119
|
|
|
99
120
|
export {
|
|
@@ -107,6 +128,7 @@ export {
|
|
|
107
128
|
listRecentProspectAudits,
|
|
108
129
|
OVERRIDES_MAX_LEN,
|
|
109
130
|
setProspectAuditOverrides,
|
|
131
|
+
OPENED_AT_COALESCE_MS,
|
|
110
132
|
touchProspectAuditOpened
|
|
111
133
|
};
|
|
112
|
-
//# sourceMappingURL=chunk-
|
|
134
|
+
//# sourceMappingURL=chunk-P7323YVW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/db/prospect-audits.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport type { Db } from \"./client.js\";\n\n/** 16 random bytes → 22-char base64url string: unguessable, URL-safe. */\nexport function generateToken(): string {\n return randomBytes(16).toString(\"base64url\");\n}\n\n/** Anything not exactly generateToken()-shaped is a probe, not a report. */\nexport function isValidToken(s: string): boolean {\n return /^[A-Za-z0-9_-]{22}$/.test(s);\n}\n\n/** Opaque, collision-free id (mirrors newSubmissionId / newDeadLetterId).\n * crypto is a Node 20 global — no new dep. Nothing reads this format; `token`\n * is the public handle. */\nexport function newProspectAuditId(): string {\n return `pa_${crypto.randomUUID()}`;\n}\n\n/** \"complete\" when every pipeline stage succeeded; \"partial\" when any stage\n * failed or was deliberately skipped (StageResult's `ok: false` covers\n * both — the pipeline doesn't distinguish them at this layer). The report\n * itself already degrades each failed section to \"not measured\"; this is\n * what lets a future dashboard tell complete from partial without\n * deserializing the whole result_json. */\nexport type ProspectAuditStatus = \"complete\" | \"partial\";\n\n/**\n * The lineage handle for a site: one key for every way of writing its address.\n *\n * `url` is kept exactly as it was given, because what we audited is a fact\n * about the run. It is the wrong thing to group by, though, and the corpus\n * already proves it — `beachfrontdentistry.com` and `beachfrontdentistry.com/`\n * are two separate histories in the table today, as are the two spellings of\n * ludlowkingsley.com. Nothing has broken only because nothing yet compares two\n * audits of one site. The moment the report offers a before and an after, a\n * trailing slash turns a returning client into a stranger.\n *\n * Scheme, a leading \"www.\", a trailing slash, a query string and a fragment all\n * go: none of them distinguish one site from another. The path stays, because a\n * site living in a subdirectory genuinely is a different site.\n *\n * Never throws. A key we cannot compute must not take an audit down with it —\n * an unparseable input falls back to its trimmed self, which groups nothing and\n * so is no worse than having no key at all.\n */\nexport function siteKey(url: string): string {\n const raw = url.trim();\n if (raw === \"\") return \"\";\n try {\n const u = new URL(/^[a-z][a-z0-9+.-]*:\\/\\//i.test(raw) ? raw : `https://${raw}`);\n const host = u.hostname.toLowerCase().replace(/^www\\./, \"\");\n const path = u.pathname.replace(/\\/+$/, \"\");\n return `${host}${path}`;\n } catch {\n return raw;\n }\n}\n\nexport type NewProspectAudit = {\n url: string;\n business: string | null;\n resultJson: string;\n /** #676. What the operator chose by hand, if anything. Absent or empty means\n * the audit generated its own — stored as NULL, which is deliberately\n * distinct from an empty operator list. */\n chosenTerms?: string[] | null;\n chosenQuestions?: string[] | null;\n /** Defaults to \"complete\" — the column's own SQL default — for callers\n * (tests, ad-hoc scripts) that don't track per-stage outcomes. The\n * prospect-audit CLI, the only real writer, always computes and passes\n * this explicitly. */\n status?: ProspectAuditStatus;\n};\n\n/** A chosen list as stored: JSON, or NULL when nothing usable was chosen.\n * Blank entries are dropped first, so a textarea holding only whitespace\n * stores NULL rather than an array of empty strings. */\nfunction jsonListOrNull(xs: string[] | null | undefined): string | null {\n const cleaned = (xs ?? []).map((x) => x.trim()).filter((x) => x !== \"\");\n return cleaned.length === 0 ? null : JSON.stringify(cleaned);\n}\n\nexport async function createProspectAudit(\n db: Db,\n audit: NewProspectAudit,\n): Promise<{ id: string; token: string }> {\n const id = newProspectAuditId();\n const token = generateToken();\n await db\n .insertInto(\"prospect_audits\")\n .values({\n id,\n token,\n url: audit.url,\n site_key: siteKey(audit.url),\n business: audit.business,\n created_at: new Date().toISOString(),\n status: audit.status ?? \"complete\",\n result_json: audit.resultJson,\n chosen_terms: jsonListOrNull(audit.chosenTerms),\n chosen_questions: jsonListOrNull(audit.chosenQuestions),\n })\n .execute();\n return { id, token };\n}\n\nexport type ProspectAuditRow = {\n id: string;\n url: string;\n business: string | null;\n created_at: string;\n status: string;\n result_json: string;\n /** The operator's edits as stored JSON, or null when this report has never\n * been edited. Deliberately NOT parsed here: the JSON route will place this\n * string into its response body as-is, exactly as it already does with\n * `result_json`, so the row hands back what was validated on the way in. */\n overrides_json: string | null;\n edited_at: string | null;\n opened_at: string | null;\n /** #676. JSON arrays, or null when the audit chose its own. */\n chosen_terms: string | null;\n chosen_questions: string | null;\n};\n\nexport async function getProspectAuditByToken(\n db: Db,\n token: string,\n): Promise<ProspectAuditRow | null> {\n const row = await db\n .selectFrom(\"prospect_audits\")\n .select([\n \"id\",\n \"url\",\n \"business\",\n \"created_at\",\n \"status\",\n \"result_json\",\n \"overrides_json\",\n \"edited_at\",\n \"opened_at\",\n \"chosen_terms\",\n \"chosen_questions\",\n ])\n .where(\"token\", \"=\", token)\n .executeTakeFirst();\n return row ?? null;\n}\n\n/** The columns the cockpit's /audits listing page shows. Deliberately NOT\n * `result_json` — large, and useless to a list — but `token` IS selected:\n * the listing needs it to build each row's `/r/{token}` link, so it's\n * rendered only into an href, never displayed as a raw value. */\nexport type ProspectAuditListItem = {\n id: string;\n token: string;\n url: string;\n business: string | null;\n status: string;\n created_at: string;\n /** Whether an operator has edited this report, and when someone last opened\n * it — both cheap TEXT columns, and the two facts the listing needs to show\n * an \"edited\" marker without reading `overrides_json`. */\n edited_at: string | null;\n opened_at: string | null;\n /** #676. Present so the listing can mark which audits used chosen terms\n * WITHOUT reading `result_json`, which it deliberately never selects. */\n chosen_terms: string | null;\n};\n\n/** Ceiling on `listRecentProspectAudits`' `limit`, enforced defensively (a\n * caller passing 10_000 must not get 10_000 rows) — well past anything a\n * listing page would ever render on one screen. */\nexport const MAX_RECENT_PROSPECT_AUDITS = 100;\n\n/** Clamp a caller-supplied limit into [1, MAX_RECENT_PROSPECT_AUDITS], with a\n * safe fallback of 1 for anything non-finite (NaN, ±Infinity) rather than\n * letting a bad input reach the query unclamped. */\nfunction clampLimit(limit: number): number {\n const n = Number.isFinite(limit) ? Math.trunc(limit) : 1;\n return Math.min(Math.max(n, 1), MAX_RECENT_PROSPECT_AUDITS);\n}\n\n/** Newest-first audits for the cockpit's /audits listing page, capped both\n * defensively (see clampLimit) and by an index (migration 0010) so the\n * ORDER BY never falls back to a raw scan + a temp b-tree sort — see the\n * EXPLAIN-query-plan gate, tests/db/query-plans.test.ts. */\nexport async function listRecentProspectAudits(\n db: Db,\n limit: number,\n): Promise<ProspectAuditListItem[]> {\n return db\n .selectFrom(\"prospect_audits\")\n .select([\n \"id\",\n \"token\",\n \"url\",\n \"business\",\n \"status\",\n \"created_at\",\n \"edited_at\",\n \"opened_at\",\n \"chosen_terms\",\n ])\n .orderBy(\"created_at\", \"desc\")\n .limit(clampLimit(limit))\n .execute();\n}\n\n/** One replaced string and the generated text it replaced. */\nexport type Override = { original: string; text: string };\nexport type OverrideMap = Record<string, Override>;\n\n/**\n * Ceiling on one report's override map, measured on the SERIALISED JSON rather\n * than on the entry count — one entry holding a megabyte is the case that\n * matters, and the string is what actually gets stored and re-served.\n *\n * The arithmetic, not a round number: the report carries roughly 227 editable\n * strings (the check battery alone, before fixes, claims and goal copy). An\n * operator who replaced every one of them, at around 300 characters of\n * `original` plus 300 of `text` each, writes on the order of 136 KB. This is\n * about four times that worst realistic case, which leaves the honest editor\n * unbounded in practice while still bounding a hand-crafted POST.\n *\n * The bound matters more here than it does for the sibling `setReportCommentary`\n * (2 000 characters): commentary is read when a report is composed, whereas this\n * value is served back inline on every fetch of the report, out of a metered\n * store. 2 288 891 characters were accepted in a single write before this cap.\n */\nexport const OVERRIDES_MAX_LEN = 512_000;\n\nexport type SetOverridesResult =\n | { status: \"updated\"; token: string }\n | { status: \"invalid\"; token: string }\n | { status: \"not-found\"; token: string };\n\n/**\n * The map that will actually be stored — a fresh `{ original, text }` pair per\n * entry, built out of the validated strings themselves — or null for anything\n * that is not a flat map of `{ original, text }` string pairs.\n *\n * Validating and BUILDING are one step deliberately, and this function returns\n * the map rather than a type guard for that reason. Every defect this code has\n * had was the same shape: one object was checked and a different one was handed\n * to `JSON.stringify`. `stringify` honours a `toJSON` method, so the entry\n * `{ original: \"a\", text: \"b\", toJSON: () => 5 }` validated as a pair and stored\n * `{\"k\":5}` — bytes this function never saw, in the one place that claims to be\n * the only gate they can be caught at. Reading each field once and keeping what\n * was read closes the gap by construction: what is stored IS what was approved,\n * and a getter that would answer differently on a second read has no second\n * read. Key order comes straight from `Object.entries`, which is the order\n * `JSON.stringify` would have used, so a round trip is stable and diffs stay\n * legible.\n *\n * EXTRA KEYS ON AN ENTRY ARE DROPPED, and that is a POLICY, not something\n * building the map here forces. A third design satisfies both halves at once:\n * build from the validated fields AND reject any entry carrying a key other than\n * `original` and `text`. Under it the `toJSON` gap, the double-read gap and the\n * extra keys all close by construction exactly as they do now, and a cycle or a\n * BigInt in a third key comes back `invalid` — because the third key is itself\n * the rejection reason, not because `JSON.stringify` threw on it. Dropping and\n * rejecting are therefore independent of building; the choice is DROP, and this\n * is the argument for it rather than a consequence of the rewrite.\n *\n * The consumer reads `original` and `text` and nothing else — the website\n * matches `original` against the generated string before it substitutes `text`.\n * An entry key nobody reads is dead weight re-served on every fetch of the\n * report out of a metered store, which is the same cost `OVERRIDES_MAX_LEN`\n * exists to bound; so the answer to a third key is to stop storing it, not to\n * refuse the operator's words over it. Dropping also keeps the cheap\n * forward-compatible case working: an editor that starts sending a field this\n * schema has not learned yet still saves what was typed.\n *\n * The line that drop stops at is the one this file already draws: it is the\n * ENTRY's own keys that are dropped. An unknown TOP-LEVEL key is a whole\n * override — operator content — and is validated like any other, never silently\n * discarded. The `__proto__` rule below is that same distinction reaching the\n * opposite verdict, for the same reason.\n *\n * The drop IS silent, and that is the cost being accepted: a map is stored that\n * is not byte-identical to the one sent, and the caller is told `updated` all\n * the same. It is acceptable only because what differs is always fields nobody\n * reads — it would not be acceptable for anything a reader could miss.\n *\n * This runs BEFORE the read, the same order `setReportCommentary` uses, so a\n * malformed body costs no round trip.\n *\n * It is also the ONLY place a malformed value can be caught. The JSON route\n * never parses what it serves — today it returns `result_json` untouched, and\n * once it carries overrides too it will place this string into the body the\n * same way, for the reason its own comment gives: parsing and re-serialising\n * there would add a failure mode between the database and the consumer for no\n * gain. Nothing downstream will notice a non-JSON value here; it would simply\n * break every subsequent fetch of that report. So: validate on the way in,\n * once, and never store anything this function has not approved.\n *\n * \"Only place\" is about MALFORMEDNESS, and is not a claim that no other limit is\n * needed. `OVERRIDES_MAX_LEN` bounds the string that gets STORED, never the\n * request body: a POST carrying megabytes of junk in keys nobody reads is parsed\n * in full and walked in full, and then stores 33 bytes and answers `updated`.\n * Bounding the body is the HTTP route's job, with a body limit there — not a\n * second check here.\n *\n * Both null guards are load-bearing, because `typeof null === \"object\"`: the\n * outer one keeps `Object.entries(null)` from throwing, and the inner one keeps\n * a null ENTRY from being read for `.original` — the exact input that crashed\n * the website's own override layer during its review.\n *\n * The PROTOTYPE check is the one that stops this function passing vacuously.\n * `Object.entries()` on a Map, a Set or a Date returns `[]`, so an entry walk\n * never runs its body and approved all three: a Map stored `{}` and silently\n * discarded every override in it, and a Date stored a bare JSON string where a\n * map belongs — each reporting \"updated\" and stamping `edited_at`.\n * Arrays fall out of the same check (`Array.prototype !== Object.prototype`), so\n * there is no separate `Array.isArray` guard on the outer value.\n *\n * `Object.create(null)` IS accepted: a null-prototype object serialises exactly\n * like `{}`, which is the only property this validator's consumer cares about,\n * and refusing it would reject a legitimate map for a reason invisible in JSON.\n *\n * A CROSS-REALM plain object is rejected, though, because its prototype is the\n * OTHER realm's `Object.prototype` and an identity comparison sees a stranger.\n * Nothing produces one today — a route handler's `JSON.parse` builds its objects\n * in this realm — and rejecting is the safe direction, but it would be a mystery\n * to a future worker-thread or `vm` consumer handed a map across the boundary.\n *\n * A `__proto__` KEY is rejected rather than stripped. `JSON.parse` makes it an\n * ordinary own property, and stored verbatim a consumer doing\n * `Object.assign({}, parsed)` does get its prototype replaced — the consumer\n * here is the website, a separate codebase, and this function's whole premise is\n * that nothing downstream will check.\n *\n * It is rejected rather than stripped for the distinction above, not for any\n * general rule against silent drops — such a rule would condemn the entry-key\n * drop in the same breath. `__proto__` at the TOP level is a whole override, so\n * stripping it would discard operator content and still report `updated`, on a\n * report that quietly lost an edit. An entry key is read by nobody, so dropping\n * one loses nothing. One rule, applied consistently, with opposite outcomes for\n * a stated reason.\n */\nfunction buildOverrideMap(v: unknown): OverrideMap | null {\n if (v === null || typeof v !== \"object\") return null;\n const proto = Object.getPrototypeOf(v) as unknown;\n if (proto !== Object.prototype && proto !== null) return null;\n if (Object.hasOwn(v, \"__proto__\")) return null;\n const built: OverrideMap = {};\n for (const [key, entry] of Object.entries(v as Record<string, unknown>)) {\n if (entry === null || typeof entry !== \"object\" || Array.isArray(entry)) return null;\n // One read each, and the values that were read are the values that are kept.\n const { original, text } = entry as Partial<Override>;\n if (typeof original !== \"string\" || typeof text !== \"string\") return null;\n built[key] = { original, text };\n }\n return built;\n}\n\n/**\n * The exact string that will be stored, or null when the input is malformed.\n *\n * Building and serialising are one step, inside one try, because READING the\n * caller's object can throw on an input that is otherwise well-formed. There are\n * three such paths through `buildOverrideMap`, and the try covers all of them:\n *\n * 1. `Object.entries(v)` — an enumerable getter on the map itself, or a\n * Proxy's `ownKeys` / `getOwnPropertyDescriptor` trap.\n * 2. `const { original, text } = entry` — a getter on the ENTRY, which throws\n * after the map has been walked and before either field is validated.\n * 3. `built[key] = { original, text }` — `built` starts as `{}`, so the\n * assignment walks `Object.prototype`, and a setter poisoned there under\n * that key name throws on the WRITE rather than on any read.\n *\n * `Object.getPrototypeOf(v)` and `Object.hasOwn(v, \"__proto__\")` are two more,\n * both trappable on a Proxy and both ahead of the walk.\n *\n * `setProspectAuditOverrides` is typed to return one of three statuses, and an\n * escaping TypeError is a fourth outcome its callers have no branch for. A value\n * that cannot be read is malformed input, not an exceptional condition — and the\n * caller already has a branch for malformed input.\n *\n * `JSON.stringify` itself can no longer throw here: it is handed a plain object\n * of plain `{ original, text }` string pairs, which has no cycle, no BigInt and\n * no `toJSON` to honour. That used to be the other half of this try — a circular\n * reference or a BigInt in a valid-looking entry threw — and both are now simply\n * dropped with the rest of the entry's extra keys. The try stays wrapped around\n * the whole thing anyway: the day a field that is not a string is added, this\n * still returns `invalid` rather than throwing past the return type.\n *\n * The size cap is measured HERE, on the CONSTRUCTED string: that is the string\n * that gets stored and re-served, an entry count says nothing about it, and the\n * caller's object may be arbitrarily larger than the pairs taken out of it.\n * Which is also what it is NOT: a request-body limit. An oversized body is\n * parsed and walked in full before 33 bytes are stored and `updated` returned,\n * so whoever writes the HTTP route still owes it a body limit of its own.\n */\nfunction serialiseOverrides(overrides: OverrideMap): string | null {\n let json: string;\n try {\n const built = buildOverrideMap(overrides);\n if (built === null) return null;\n json = JSON.stringify(built);\n } catch {\n return null;\n }\n return json.length > OVERRIDES_MAX_LEN ? null : json;\n}\n\n/**\n * Replace one report's override map wholesale and stamp `edited_at`.\n *\n * Whole-map rather than per-key: the caller always holds the complete map, and\n * a partial write has no way to express a deletion. An empty map is legitimate\n * and clears every override — an editor that can set but not unset traps the\n * operator in whatever they first typed.\n *\n * What is stored is REBUILT from the validated strings, not the object passed\n * in, so an entry keeps its `original` and its `text` and nothing else: any\n * other key on it is dropped rather than written through. See\n * `buildOverrideMap` for why that is the contract.\n *\n * LAST WRITE WINS, and two writers silently clobber each other: the second\n * write replaces the first's whole map, with nothing to tell either of them it\n * happened. That is accepted rather than overlooked — there is one operator\n * team, and an editor is open for minutes, so the window is small and the cost\n * of losing it is a paragraph retyped. If a second editor ever exists, the\n * column an optimistic check would key on is `edited_at`: read it with the\n * report, send it back with the write, refuse the write if it moved.\n *\n * `result_json` stays untouched. There is still no way to write it.\n */\nexport async function setProspectAuditOverrides(\n db: Db,\n token: string,\n overrides: OverrideMap,\n): Promise<SetOverridesResult> {\n const serialised = serialiseOverrides(overrides);\n if (serialised === null) return { status: \"invalid\", token };\n\n const existing = await getProspectAuditByToken(db, token);\n if (!existing) return { status: \"not-found\", token };\n\n await db\n .updateTable(\"prospect_audits\")\n .set({ overrides_json: serialised, edited_at: new Date().toISOString() })\n .where(\"token\", \"=\", token)\n .execute();\n return { status: \"updated\", token };\n}\n\n/**\n * How stale `opened_at` must be before an open rewrites it.\n *\n * The operator signal is \"has the prospect read this, and roughly when\" — a\n * question five-minute resolution answers exactly as well as per-request\n * resolution does. Everything below the window is refresh noise.\n */\nexport const OPENED_AT_COALESCE_MS = 5 * 60 * 1000;\n\n/**\n * Record that a report was opened by someone who is not editing it.\n *\n * Best effort by contract: the caller must not let a failure here fail the\n * response. Knowing when a prospect last looked is useful; it is not worth\n * turning a read route into one that can 500.\n *\n * COALESCED, and that is a safety property rather than an optimisation. The\n * route that calls this is unauthenticated and rate-limited at 120 req/min per\n * IP, so without the window one token holder can drive ~172 000 writes a day\n * from a single address — into a Turso project shared by the entire fleet,\n * configured `overages: false`, where crossing quota BLOCKS READS AND WRITES\n * for every site at once and where capacity is not alarmed. A read route that\n * amplifies into unbounded writes against that is an outage vector, not a\n * billing detail. With the window, a refresh costs nothing and the amplifier\n * is gone.\n *\n * The window lives in the WHERE clause, not in a read-then-write in the caller:\n * one round trip, and two concurrent opens cannot both decide to write.\n */\nexport async function touchProspectAuditOpened(db: Db, token: string): Promise<void> {\n const now = new Date();\n const staleBefore = new Date(now.getTime() - OPENED_AT_COALESCE_MS).toISOString();\n await db\n .updateTable(\"prospect_audits\")\n .set({ opened_at: now.toISOString() })\n .where(\"token\", \"=\", token)\n .where((eb) => eb.or([eb(\"opened_at\", \"is\", null), eb(\"opened_at\", \"<\", staleBefore)]))\n .execute();\n}\n"],"mappings":";AAAA,SAAS,mBAAmB;AAIrB,SAAS,gBAAwB;AACtC,SAAO,YAAY,EAAE,EAAE,SAAS,WAAW;AAC7C;AAGO,SAAS,aAAa,GAAoB;AAC/C,SAAO,sBAAsB,KAAK,CAAC;AACrC;AAKO,SAAS,qBAA6B;AAC3C,SAAO,MAAM,OAAO,WAAW,CAAC;AAClC;AA6BO,SAAS,QAAQ,KAAqB;AAC3C,QAAM,MAAM,IAAI,KAAK;AACrB,MAAI,QAAQ,GAAI,QAAO;AACvB,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,2BAA2B,KAAK,GAAG,IAAI,MAAM,WAAW,GAAG,EAAE;AAC/E,UAAM,OAAO,EAAE,SAAS,YAAY,EAAE,QAAQ,UAAU,EAAE;AAC1D,UAAM,OAAO,EAAE,SAAS,QAAQ,QAAQ,EAAE;AAC1C,WAAO,GAAG,IAAI,GAAG,IAAI;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAqBA,SAAS,eAAe,IAAgD;AACtE,QAAM,WAAW,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,MAAM,EAAE;AACtE,SAAO,QAAQ,WAAW,IAAI,OAAO,KAAK,UAAU,OAAO;AAC7D;AAEA,eAAsB,oBACpB,IACA,OACwC;AACxC,QAAM,KAAK,mBAAmB;AAC9B,QAAM,QAAQ,cAAc;AAC5B,QAAM,GACH,WAAW,iBAAiB,EAC5B,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA,KAAK,MAAM;AAAA,IACX,UAAU,QAAQ,MAAM,GAAG;AAAA,IAC3B,UAAU,MAAM;AAAA,IAChB,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,IACnC,QAAQ,MAAM,UAAU;AAAA,IACxB,aAAa,MAAM;AAAA,IACnB,cAAc,eAAe,MAAM,WAAW;AAAA,IAC9C,kBAAkB,eAAe,MAAM,eAAe;AAAA,EACxD,CAAC,EACA,QAAQ;AACX,SAAO,EAAE,IAAI,MAAM;AACrB;AAqBA,eAAsB,wBACpB,IACA,OACkC;AAClC,QAAM,MAAM,MAAM,GACf,WAAW,iBAAiB,EAC5B,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC,EACA,MAAM,SAAS,KAAK,KAAK,EACzB,iBAAiB;AACpB,SAAO,OAAO;AAChB;AA0BO,IAAM,6BAA6B;AAK1C,SAAS,WAAW,OAAuB;AACzC,QAAM,IAAI,OAAO,SAAS,KAAK,IAAI,KAAK,MAAM,KAAK,IAAI;AACvD,SAAO,KAAK,IAAI,KAAK,IAAI,GAAG,CAAC,GAAG,0BAA0B;AAC5D;AAMA,eAAsB,yBACpB,IACA,OACkC;AAClC,SAAO,GACJ,WAAW,iBAAiB,EAC5B,OAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC,EACA,QAAQ,cAAc,MAAM,EAC5B,MAAM,WAAW,KAAK,CAAC,EACvB,QAAQ;AACb;AAuBO,IAAM,oBAAoB;AA+GjC,SAAS,iBAAiB,GAAgC;AACxD,MAAI,MAAM,QAAQ,OAAO,MAAM,SAAU,QAAO;AAChD,QAAM,QAAQ,OAAO,eAAe,CAAC;AACrC,MAAI,UAAU,OAAO,aAAa,UAAU,KAAM,QAAO;AACzD,MAAI,OAAO,OAAO,GAAG,WAAW,EAAG,QAAO;AAC1C,QAAM,QAAqB,CAAC;AAC5B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,CAA4B,GAAG;AACvE,QAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,EAAG,QAAO;AAEhF,UAAM,EAAE,UAAU,KAAK,IAAI;AAC3B,QAAI,OAAO,aAAa,YAAY,OAAO,SAAS,SAAU,QAAO;AACrE,UAAM,GAAG,IAAI,EAAE,UAAU,KAAK;AAAA,EAChC;AACA,SAAO;AACT;AAwCA,SAAS,mBAAmB,WAAuC;AACjE,MAAI;AACJ,MAAI;AACF,UAAM,QAAQ,iBAAiB,SAAS;AACxC,QAAI,UAAU,KAAM,QAAO;AAC3B,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,SAAO,KAAK,SAAS,oBAAoB,OAAO;AAClD;AAyBA,eAAsB,0BACpB,IACA,OACA,WAC6B;AAC7B,QAAM,aAAa,mBAAmB,SAAS;AAC/C,MAAI,eAAe,KAAM,QAAO,EAAE,QAAQ,WAAW,MAAM;AAE3D,QAAM,WAAW,MAAM,wBAAwB,IAAI,KAAK;AACxD,MAAI,CAAC,SAAU,QAAO,EAAE,QAAQ,aAAa,MAAM;AAEnD,QAAM,GACH,YAAY,iBAAiB,EAC7B,IAAI,EAAE,gBAAgB,YAAY,YAAW,oBAAI,KAAK,GAAE,YAAY,EAAE,CAAC,EACvE,MAAM,SAAS,KAAK,KAAK,EACzB,QAAQ;AACX,SAAO,EAAE,QAAQ,WAAW,MAAM;AACpC;AASO,IAAM,wBAAwB,IAAI,KAAK;AAsB9C,eAAsB,yBAAyB,IAAQ,OAA8B;AACnF,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,cAAc,IAAI,KAAK,IAAI,QAAQ,IAAI,qBAAqB,EAAE,YAAY;AAChF,QAAM,GACH,YAAY,iBAAiB,EAC7B,IAAI,EAAE,WAAW,IAAI,YAAY,EAAE,CAAC,EACpC,MAAM,SAAS,KAAK,KAAK,EACzB,MAAM,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,aAAa,MAAM,IAAI,GAAG,GAAG,aAAa,KAAK,WAAW,CAAC,CAAC,CAAC,EACrF,QAAQ;AACb;","names":[]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
approveBlockers
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-VMGHQUTV.js";
|
|
4
4
|
import {
|
|
5
5
|
ACTIVE_STATUSES,
|
|
6
6
|
allActionableVulnsTransitive,
|
|
7
7
|
isPreLaunch,
|
|
8
8
|
siteSlug
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-ICSRWW5Q.js";
|
|
10
10
|
|
|
11
11
|
// src/alerts/digest-collectors.ts
|
|
12
12
|
function dashboardUrl(baseUrl, siteName) {
|
|
@@ -196,14 +196,44 @@ var NOTIFY_BOUNCE_WINDOW_DAYS = 14;
|
|
|
196
196
|
function collectNotifyBounceAlerts(sites, bouncedBySite, baseUrl) {
|
|
197
197
|
const items = [];
|
|
198
198
|
for (const s of sites) {
|
|
199
|
-
const
|
|
199
|
+
const counts = bouncedBySite.get(s.id);
|
|
200
|
+
const n = counts?.total ?? 0;
|
|
200
201
|
if (n < NOTIFY_BOUNCE_THRESHOLD) continue;
|
|
202
|
+
const addressIsSuspect = (counts?.permanent ?? 0) > 0;
|
|
201
203
|
items.push({
|
|
202
204
|
key: `notify-bounce:${s.id}`,
|
|
203
205
|
kind: "notify-bounce",
|
|
204
206
|
siteName: s.name,
|
|
205
|
-
title: `${n} lead notifications bounced (${NOTIFY_BOUNCE_WINDOW_DAYS}d) \u2014 check the point-of-contact address`,
|
|
207
|
+
title: addressIsSuspect ? `${n} lead notifications bounced (${NOTIFY_BOUNCE_WINDOW_DAYS}d) \u2014 check the point-of-contact address` : `${n} lead notifications bounced (${NOTIFY_BOUNCE_WINDOW_DAYS}d) \u2014 the client's mail filter is rejecting them; acknowledge them on the site page if the address is fine`,
|
|
206
208
|
url: dashboardUrl(baseUrl, s.name),
|
|
209
|
+
// Critical either way: severity is about the LEAD, which reached nobody in
|
|
210
|
+
// both cases. Only the instruction changes.
|
|
211
|
+
severity: "critical",
|
|
212
|
+
metric: n
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return items;
|
|
216
|
+
}
|
|
217
|
+
function collectDeadLetterAlerts(sites, unreplayedBySlug, baseUrl) {
|
|
218
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
219
|
+
for (const s of sites) {
|
|
220
|
+
const slug = siteSlug(s.name);
|
|
221
|
+
if (slug) bySlug.set(slug, s);
|
|
222
|
+
}
|
|
223
|
+
const items = [];
|
|
224
|
+
for (const slug of [...unreplayedBySlug.keys()].sort()) {
|
|
225
|
+
const n = unreplayedBySlug.get(slug) ?? 0;
|
|
226
|
+
if (n < 1) continue;
|
|
227
|
+
const site = bySlug.get(slug);
|
|
228
|
+
const noun = n === 1 ? "lead" : "leads";
|
|
229
|
+
items.push({
|
|
230
|
+
key: `deadletter:${slug}`,
|
|
231
|
+
kind: "deadletter",
|
|
232
|
+
siteName: site ? site.name : `(unknown site: ${slug})`,
|
|
233
|
+
title: site ? `${n} ${noun} dead-lettered and not yet replayed \u2014 run \`db replay-deadletters\`` : `${n} ${noun} dead-lettered for '${slug}', which resolves to NO fleet site \u2014 leads are being dropped; run \`ensure-site ${slug}\` then \`db replay-deadletters\``,
|
|
234
|
+
// No link for an unresolvable slug: /s/<slug> would 404, and the
|
|
235
|
+
// collectors' own rule is never to render a broken link.
|
|
236
|
+
...site ? { url: dashboardUrl(baseUrl, site.name) } : {},
|
|
207
237
|
severity: "critical",
|
|
208
238
|
metric: n
|
|
209
239
|
});
|
|
@@ -302,8 +332,9 @@ export {
|
|
|
302
332
|
NOTIFY_BOUNCE_THRESHOLD,
|
|
303
333
|
NOTIFY_BOUNCE_WINDOW_DAYS,
|
|
304
334
|
collectNotifyBounceAlerts,
|
|
335
|
+
collectDeadLetterAlerts,
|
|
305
336
|
collectAnalyticsFailures,
|
|
306
337
|
prismicAckIsLive,
|
|
307
338
|
collectPrismicDriftAlerts
|
|
308
339
|
};
|
|
309
|
-
//# sourceMappingURL=chunk-
|
|
340
|
+
//# sourceMappingURL=chunk-PARKXDXI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/alerts/digest-collectors.ts"],"sourcesContent":["// src/alerts/digest-collectors.ts\nimport type { AttentionItem } from \"./attention.js\";\nimport {\n allActionableVulnsTransitive,\n siteSlug,\n ACTIVE_STATUSES,\n isPreLaunch,\n type WebsiteRow,\n} from \"../reports/airtable/websites.js\";\nimport type { ReportRow } from \"../reports/airtable/reports.js\";\nimport { approveBlockers } from \"../reports/preflight.js\";\nimport type { NotifyBounceCounts } from \"../db/submissions.js\";\n\n/** Build the same `/s/<slug>` dashboard link the M3 ready-section uses, trailing-slash-safe.\n * An empty Name slugs to \"\" and `/s/` is a dead link — fall back to the fleet homepage,\n * exactly like the ready-section does. */\nfunction dashboardUrl(baseUrl: string, siteName: string): string {\n const root = baseUrl.replace(/\\/$/, \"\");\n const slug = siteSlug(siteName);\n return slug ? `${root}/s/${slug}` : root;\n}\n\n/**\n * A GitHub-signals sweep older than this (or never run) is no longer trustworthy:\n * a repo whose nightly probe THREW stops being re-swept, so its persisted\n * `Default Branch CI` / `Renovate Failing CIs` freeze at their last value forever\n * — a phantom 🔴 that can never clear. 3 days ≈ 3× the daily sweep interval, so a\n * single missed/flaky run doesn't drop a real signal. The CI/Renovate collectors\n * (only) skip a site whose `githubSignalsAt` is staler than this. Vuln/Lighthouse/\n * delivery signals come from other sweeps and are unaffected.\n */\nconst GITHUB_SIGNALS_STALE_DAYS = 3;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\n/** True when a site's GitHub-signals sweep is too old (or never ran) to trust the\n * persisted CI/Renovate fields. A null `githubSignalsAt` (never swept) is stale —\n * there's no sweep to vouch for the values. A future timestamp (now - swept < 0)\n * is fresh. `now` is injected so the gate is testable. */\nfunction gitHubSignalsStale(swept: string | null, now: Date): boolean {\n if (swept === null) return true;\n const ageMs = now.getTime() - Date.parse(swept);\n if (!Number.isFinite(ageMs)) return true; // unparseable timestamp → don't trust it\n return ageMs > GITHUB_SIGNALS_STALE_DAYS * MS_PER_DAY;\n}\n\n/** Renovate auto-fix dispatches for one vuln episode before it's \"exhausted\" (manual fix needed). */\nconst AUTO_FIX_EXHAUSTED_CYCLES = 3;\n\n/**\n * One attention item per site carrying current critical+high vulns (medium/low omitted\n * per the locked threshold). PURE: takes already-fetched Websites rows. `metric` is the\n * critical+high count (so a rising count diffs as WORSE); `severity` is `critical` when\n * any critical exists, else `warning`. Null counts (never audited) read as 0 → skipped.\n * Once `securityAutoFixAttempts` reaches AUTO_FIX_EXHAUSTED_CYCLES the item is flagged\n * `autoFixExhausted` (forced-critical, escalated title) — Renovate tried and couldn't fix it.\n * EXCEPT when the persisted advisories prove every critical/high vuln is TRANSITIVE: then\n * nightly dispatches were green no-ops (no direct dep to bump — the fix rides the weekly\n * lockfile window), \"auto-fix failed\" would be a lie, and a stale pre-fix counter must not\n * escalate (Sonder said \"auto-fix failed (5×)\" on exactly this, 2026-08-10). Those sites get\n * an honest transitive-only title, keep count-based severity, and stay out of the digest\n * email (amber cockpit Watch still shows them). Unknown relationship data never mutes.\n *\n * Emission is UNCONDITIONAL — every surface applies its own policy on top:\n * - digest email: includes a vuln only when `autoFixExhausted` (the operator hears\n * about a vuln only after Renovate tried and failed; before that the fleet is\n * still self-patching) — runDigest filters\n * - cockpit: non-exhausted vuln → amber Watch; exhausted → hard break (assignTier),\n * and only an exhausted vuln pierces the pre-launch mute (piercesPreLaunchMute)\n */\nexport function collectVulnAlerts(sites: WebsiteRow[], baseUrl: string): AttentionItem[] {\n const items: AttentionItem[] = [];\n for (const s of sites) {\n const critical = s.securityVulnsCritical ?? 0;\n const high = s.securityVulnsHigh ?? 0;\n const metric = critical + high;\n if (metric <= 0) continue;\n const attempts = s.securityAutoFixAttempts ?? 0;\n const transitiveOnly = allActionableVulnsTransitive(s.securityAdvisories);\n const exhausted = !transitiveOnly && attempts >= AUTO_FIX_EXHAUSTED_CYCLES;\n const noun = metric === 1 ? \"vuln\" : \"vulns\";\n items.push({\n key: `vuln:${s.id}`,\n kind: \"vuln\",\n siteName: s.name,\n title: exhausted\n ? `${metric} critical/high ${noun} — auto-fix failed (${attempts}×)`\n : transitiveOnly\n ? `${metric} critical/high ${noun} — transitive-only, fix rides the weekly lockfile window`\n : `${metric} critical/high ${noun}`,\n url: dashboardUrl(baseUrl, s.name),\n severity: exhausted || critical > 0 ? \"critical\" : \"warning\",\n metric,\n ...(exhausted ? { autoFixExhausted: true } : {}),\n });\n }\n return items;\n}\n\n/** Absolute floor below which a Lighthouse category is \"Needs attention\" (Tucker's call). */\nconst LIGHTHOUSE_FLOOR = 75;\n\n/** The four Lighthouse categories, each mapped to its WebsiteRow score field, URL slug,\n * and the human label rendered in the digest title. Order is the operator's reading order. */\nconst LIGHTHOUSE_CATEGORIES: ReadonlyArray<{\n field: \"pScore\" | \"rScore\" | \"bpScore\" | \"seoScore\";\n slug: string;\n label: string;\n}> = [\n { field: \"pScore\", slug: \"performance\", label: \"Performance\" },\n { field: \"rScore\", slug: \"accessibility\", label: \"Accessibility\" },\n { field: \"bpScore\", slug: \"best-practices\", label: \"Best Practices\" },\n { field: \"seoScore\", slug: \"seo\", label: \"SEO\" },\n];\n\n/**\n * One attention item per Lighthouse category below the absolute floor (75) for each site.\n * PURE: takes already-fetched Websites rows. Categories are Performance/Accessibility/\n * Best-Practices/SEO. A null score (never audited) or a score >= 75 is skipped. The\n * `metric` is the DEFICIT (`100 - score`): a lower score → higher metric, so a category\n * that drops further diffs as WORSE and one that first crosses below 75 diffs as NEW —\n * which is how `diffAttention`'s \"WORSE on increase\" rule reads an inverted score. `key`\n * is `lighthouse:<siteId>:<categorySlug>`, so the four categories stay distinct per site.\n */\nexport function collectLighthouseAlerts(sites: WebsiteRow[], baseUrl: string): AttentionItem[] {\n const items: AttentionItem[] = [];\n for (const s of sites) {\n for (const cat of LIGHTHOUSE_CATEGORIES) {\n const score = s[cat.field];\n if (score === null || score >= LIGHTHOUSE_FLOOR) continue;\n items.push({\n key: `lighthouse:${s.id}:${cat.slug}`,\n kind: \"lighthouse\",\n siteName: s.name,\n title: `Lighthouse ${cat.label} ${score} (below ${LIGHTHOUSE_FLOOR})`,\n url: dashboardUrl(baseUrl, s.name),\n severity: \"warning\",\n metric: 100 - score,\n });\n }\n }\n return items;\n}\n\n/**\n * One attention item per report whose `deliveryStatus` is a failure (`bounced` or\n * `complained` — `delivered`/`pending` are ignored). PURE: takes already-fetched\n * Reports rows + a record-id→site map. A complaint ranks above a bounce (locked\n * threshold), so `severity` is `critical` for complained / `warning` for bounced.\n * `metric` is 1 (a binary event). Orphans (siteId not in the map) are skipped, as\n * the M3 ready-section does, so the digest never renders a broken link. The diff\n * key is the report RECORD id, so two failures on one site stay distinct.\n */\n/**\n * One attention item per unsent draft whose send is ALREADY known to fail\n * (approveBlockers: recipients / header image / report scores). An APPROVED one\n * is critical — the next 09:23 UTC run will go red on it; a pending one is a\n * warning — approving it just schedules that failure. Keyed `preflight:<reportId>`.\n * PURE; same predicate the approve gate and the dashboard chip use.\n */\nexport function collectPreflightBlocked(\n reports: ReportRow[],\n sitesById: Map<string, WebsiteRow>,\n baseUrl: string,\n): AttentionItem[] {\n const items: AttentionItem[] = [];\n for (const r of reports) {\n if (!r.draftReady || r.sentAt !== null) continue;\n // The approved state rides the KEY so a pending→approved escalation with\n // unchanged blockers re-news as a fresh critical instead of diffing\n // \"standing\" against its old warning self.\n const state = r.approvedToSend ? \"approved\" : \"pending\";\n const site = sitesById.get(r.siteId);\n if (!site) {\n // A dangling/empty Site link is itself a send blocker (sendApprovedReports\n // fails with \"Site row not found\") — surface it; the empty-slug\n // dashboardUrl fallback yields the fleet root, not a broken /s/ link.\n items.push({\n key: `preflight:${r.id}:${state}`,\n kind: \"preflight\",\n siteName: \"(unlinked site)\",\n title: r.approvedToSend\n ? `Approved ${r.reportType} will fail at send — site-not-found`\n : `${r.reportType} draft can't be approved — site-not-found`,\n url: dashboardUrl(baseUrl, \"\"),\n severity: r.approvedToSend ? \"critical\" : \"warning\",\n metric: 1,\n });\n continue;\n }\n const fails = approveBlockers(site, r).filter((f) => f.level === \"fail\");\n if (fails.length === 0) continue;\n const first = fails[0]!;\n const more = fails.length > 1 ? ` (+${fails.length - 1} more)` : \"\";\n items.push({\n key: `preflight:${r.id}:${state}`,\n kind: \"preflight\",\n siteName: site.name,\n title: r.approvedToSend\n ? `Approved ${r.reportType} will fail at send — ${first.check}${more}`\n : `${r.reportType} draft can't be approved — ${first.check}${more}`,\n url: dashboardUrl(baseUrl, site.name),\n severity: r.approvedToSend ? \"critical\" : \"warning\",\n metric: fails.length,\n });\n }\n return items;\n}\n\nexport function collectDeliveryFailures(\n reports: ReportRow[],\n sitesById: Map<string, WebsiteRow>,\n baseUrl: string,\n): AttentionItem[] {\n const items: AttentionItem[] = [];\n for (const r of reports) {\n if (r.deliveryStatus !== \"bounced\" && r.deliveryStatus !== \"complained\") continue;\n const site = sitesById.get(r.siteId);\n if (!site) continue; // orphan → skip rather than render a broken link\n const complained = r.deliveryStatus === \"complained\";\n items.push({\n key: `delivery:${r.id}`,\n kind: \"delivery\",\n siteName: site.name,\n title: complained ? \"Spam complaint on a sent report\" : \"A sent report bounced\",\n url: dashboardUrl(baseUrl, site.name),\n severity: complained ? \"critical\" : \"warning\",\n metric: 1,\n });\n }\n return items;\n}\n\n/**\n * One attention item per site carrying failing Renovate PRs, read from the\n * slice-2a-persisted `renovateFailingCis` field (the nightly github-signals sweep\n * populates it). PURE. Keyed `renovate:<siteId>` so the digest and the cockpit\n * share one diff key. `metric` is the count (a rising count diffs WORSE); severity\n * `warning`. Null/0 → skipped. A site whose `githubSignalsAt` is >3 days stale (or\n * null) is ALSO skipped — a repo that stopped being swept must not show a phantom\n * count forever (`now` injected, defaults to wall-clock).\n */\nexport function collectRenovateAlerts(\n sites: WebsiteRow[],\n baseUrl: string,\n now: Date = new Date(),\n): AttentionItem[] {\n const items: AttentionItem[] = [];\n for (const s of sites) {\n if (gitHubSignalsStale(s.githubSignalsAt, now)) continue;\n const n = s.renovateFailingCis ?? 0;\n if (n <= 0) continue;\n items.push({\n key: `renovate:${s.id}`,\n kind: \"renovate\",\n siteName: s.name,\n title: `${n} Renovate ${n === 1 ? \"PR\" : \"PRs\"} failing CI`,\n url: dashboardUrl(baseUrl, s.name),\n severity: \"warning\",\n metric: n,\n });\n }\n return items;\n}\n\n/**\n * One attention item per site whose persisted default-branch CI rollup is\n * `failing` (slice 2a). PURE. `metric` 1 (binary); severity `warning`. Any other\n * state (passing/pending/none) or null is skipped. A site whose `githubSignalsAt`\n * is >3 days stale (or null) is ALSO skipped — a repo that stopped being swept must\n * not show a phantom 🔴 forever (`now` injected, defaults to wall-clock).\n */\nexport function collectCiAlerts(\n sites: WebsiteRow[],\n baseUrl: string,\n now: Date = new Date(),\n): AttentionItem[] {\n const items: AttentionItem[] = [];\n for (const s of sites) {\n if (gitHubSignalsStale(s.githubSignalsAt, now)) continue;\n if (s.defaultBranchCi !== \"failing\") continue;\n items.push({\n key: `ci:${s.id}`,\n kind: \"ci\",\n siteName: s.name,\n title: \"Default-branch CI failing\",\n url: dashboardUrl(baseUrl, s.name),\n severity: \"warning\",\n metric: 1,\n });\n }\n return items;\n}\n\n/**\n * A soft-fail older than this is no longer a trustworthy CURRENT signal. Drafting\n * self-clears `analyticsSoftFailAt` on the next clean enrichment, so a non-null\n * value normally means \"errored on the most recent draft and hasn't recovered\" —\n * but a site that stopped being drafted (freq→None, deprecated) would otherwise\n * show a phantom forever. 45 days covers a monthly cadence + margin; older drops.\n */\nconst ANALYTICS_SOFT_FAIL_STALE_DAYS = 45;\n\n/**\n * One attention item per site whose last draft's GA/Search enrichment ERRORED, read\n * from the `analyticsSoftFailAt` timestamp (drafting sets it on a soft-fail, clears\n * it on a clean enrichment). PURE. Keyed `analytics:<siteId>` so the digest and the\n * cockpit share one diff key; `metric` 1 (binary), severity `warning`. A null\n * timestamp (clean, or the operator-added `Analytics soft-fail at` column absent) is\n * skipped, as is one staler than {@link ANALYTICS_SOFT_FAIL_STALE_DAYS}. On a\n * FLEET-WIDE subject outage many sites surface this at once — that breadth IS the\n * signal here; the report cron additionally emails a single concise fleet-wide alert\n * (see `assessAnalyticsAlert`). `now` injected, defaults to wall-clock.\n */\n/** A form-e2e run older than this can't confirm the CURRENT widget state. The run\n * is nightly, so 3 days (mirrors GITHUB_SIGNALS_STALE_DAYS) tolerates a weekend of\n * runner flakes without letting a months-old verdict drive an alarm. */\nconst TURNSTILE_WIDGET_STALE_DAYS = 3;\n\n/**\n * One CRITICAL attention item per site with `Require Turnstile` ON whose Turnstile\n * widget is known BROKEN (`turnstileWidget === \"fail\"`, fresh per\n * {@link TURNSTILE_WIDGET_STALE_DAYS}). That combination silently buckets 100% of the\n * site's real leads (token-less submissions escalate to spam_auto with notify skipped),\n * so the operator's inbox just goes quiet. PURE. Keyed `turnstile:<siteId>`. Because\n * this is an AttentionItem it rides assignTier's items short-circuit, which sits ABOVE\n * the accepted-watch mute loop — an accept key can never silence it. A null verdict or\n * a stale sweep is NOT alarmed here; assignTier raises those as an acceptable\n * \"can't verify\" watch instead. `now` injected, defaults to wall-clock.\n */\nexport function collectTurnstileGuardrailAlerts(\n sites: WebsiteRow[],\n baseUrl: string,\n now: Date = new Date(),\n): AttentionItem[] {\n const items: AttentionItem[] = [];\n for (const s of sites) {\n if (!s.requireTurnstile || s.turnstileWidget !== \"fail\") continue;\n // The stamp of the audit that OWNS the verdict, not function-health's.\n // `form-e2e` took ownership of `turnstileWidget` (#689) because only it opens\n // a browser on the real widget; function-health merely re-stamped its own\n // clock at 08:00 whether or not anything had looked at Turnstile, so gating on\n // it would age a verdict against a timer that never stops — freezing a stale\n // \"fail\" into a red that pages every morning and can never be muted. Verdict\n // and stamp are now written in the SAME FieldSet (`formE2eFields`), so they\n // always move together; `turnstile-freshness.test.ts` pins that coupling.\n const at = s.formE2eCheckedAt;\n if (at !== null) {\n const ageMs = now.getTime() - Date.parse(at);\n // Parseable and beyond the window → stale, downgraded to assignTier's watch.\n // Unparseable (NaN) keeps the item — never silently drop a real failure.\n if (Number.isFinite(ageMs) && ageMs > TURNSTILE_WIDGET_STALE_DAYS * MS_PER_DAY) continue;\n }\n items.push({\n key: `turnstile:${s.id}`,\n kind: \"turnstile\",\n siteName: s.name,\n // Sensor-neutral, and it has to be: the verdict now has TWO fail arms and the\n // row carries no discriminator. Naming /health sent the operator to Netlify to\n // check a `PUBLIC_TURNSTILE_SITE_KEY` that is correctly set — because on the\n // only site that can raise this alarm (Require Turnstile presupposes the key)\n // the reachable arm is the OTHER one: a browser seeing 110200, i.e. the\n // hostname missing from the Cloudflare widget's allowlist. Both causes are\n // named so the runbook fix is the obvious next step.\n title:\n \"Require Turnstile is ON but no working widget was found (no sitekey, or the \" +\n \"hostname is not on the Cloudflare widget's allowlist) — real leads are being auto-bucketed\",\n url: dashboardUrl(baseUrl, s.name),\n severity: \"critical\",\n metric: 1,\n });\n }\n return items;\n}\n\n/** Bounced lead notifications inside the window before a site alarms. One bounce can\n * be a transient greylist/full-mailbox blip; a repeat says the point-of-contact\n * address itself is bad (the Espada mode: apm@ bounced 4 of 8 with nothing alarming). */\nexport const NOTIFY_BOUNCE_THRESHOLD = 2;\n\n/** Window (days) the bounce count covers. Two weeks: long enough that a low-volume\n * site's second bounce still lands in the same window as its first, short enough\n * that a long-fixed address doesn't alarm forever (bounced rows never self-clear —\n * the alarm ages out as bounced submissions leave the window). */\nexport const NOTIFY_BOUNCE_WINDOW_DAYS = 14;\n\n/**\n * One CRITICAL attention item per site with >= {@link NOTIFY_BOUNCE_THRESHOLD} bounced\n * lead notifications in the last {@link NOTIFY_BOUNCE_WINDOW_DAYS} days — leads are\n * being captured but silently never reach the client, which is worse than no form at\n * all (notifyStatus \"sent\" only means Resend accepted the email; the resend-webhook\n * flips it to \"bounced\" when Resend later reports the failure, 2026-07-16). PURE:\n * takes the pre-fetched per-site counts (`countNotifyBouncedBySite`, keyed by the\n * Websites record id). Keyed `notify-bounce:<siteId>` so the digest and the cockpit\n * share one diff key; `metric` is the bounce count (a rising count diffs WORSE).\n * A site absent from the map (or below threshold) is skipped, as is a count for a\n * site id not in `sites` (orphan → never render a broken link).\n */\nexport function collectNotifyBounceAlerts(\n sites: WebsiteRow[],\n bouncedBySite: ReadonlyMap<string, NotifyBounceCounts>,\n baseUrl: string,\n): AttentionItem[] {\n const items: AttentionItem[] = [];\n for (const s of sites) {\n const counts = bouncedBySite.get(s.id);\n const n = counts?.total ?? 0;\n if (n < NOTIFY_BOUNCE_THRESHOLD) continue;\n // #783. ONE permanent bounce is enough to keep accusing the address: Resend\n // said the mailbox itself is bad, and that outranks any number of transient\n // refusals alongside it. With none, the address is not the suspect — the\n // receiving server refused our CONTENT, which is what happened to Espada,\n // and \"check the point-of-contact address\" sent the operator to inspect the\n // one thing that was working.\n //\n // An UNCLASSIFIED count (every row written before migration 0018, and every\n // complaint) takes the filter wording too. That is the safe direction: the\n // old title asserted a dead address on no evidence at all.\n const addressIsSuspect = (counts?.permanent ?? 0) > 0;\n items.push({\n key: `notify-bounce:${s.id}`,\n kind: \"notify-bounce\",\n siteName: s.name,\n title: addressIsSuspect\n ? `${n} lead notifications bounced (${NOTIFY_BOUNCE_WINDOW_DAYS}d) — check the point-of-contact address`\n : `${n} lead notifications bounced (${NOTIFY_BOUNCE_WINDOW_DAYS}d) — the client's mail filter is rejecting them; acknowledge them on the site page if the address is fine`,\n url: dashboardUrl(baseUrl, s.name),\n // Critical either way: severity is about the LEAD, which reached nobody in\n // both cases. Only the instruction changes.\n severity: \"critical\",\n metric: n,\n });\n }\n return items;\n}\n\n/**\n * One CRITICAL attention item per slug holding unreplayed dead-letter rows —\n * leads that were captured but never placed (#645).\n *\n * CRITICAL at a single row, unlike notify-bounce's threshold of 2. A bounce may\n * be a greylist blip; a dead letter is by construction a lead that reached\n * nobody, and post-flip the commonest way to get one is a site whose Turso row\n * is missing — the fleet's only silent, permanent lead-loser.\n *\n * Keyed `deadletter:<slug>` because the queue is keyed by slug and the worst case\n * is precisely a slug that resolves to no site. Such a slug is NOT dropped: it\n * gets the `(unknown site: …)` pseudo-name (the `(unlinked site)` precedent above)\n * and no `url`, since `/s/<slug>` would 404. `metric` is the row count, so a\n * growing queue diffs WORSE. PURE: takes pre-fetched counts\n * (`countUnreplayedDeadLettersBySlug`); slug-ordered for a stable digest.\n */\nexport function collectDeadLetterAlerts(\n sites: WebsiteRow[],\n unreplayedBySlug: ReadonlyMap<string, number>,\n baseUrl: string,\n): AttentionItem[] {\n const bySlug = new Map<string, WebsiteRow>();\n for (const s of sites) {\n const slug = siteSlug(s.name);\n if (slug) bySlug.set(slug, s);\n }\n const items: AttentionItem[] = [];\n for (const slug of [...unreplayedBySlug.keys()].sort()) {\n const n = unreplayedBySlug.get(slug) ?? 0;\n if (n < 1) continue;\n const site = bySlug.get(slug);\n const noun = n === 1 ? \"lead\" : \"leads\";\n items.push({\n key: `deadletter:${slug}`,\n kind: \"deadletter\",\n siteName: site ? site.name : `(unknown site: ${slug})`,\n title: site\n ? `${n} ${noun} dead-lettered and not yet replayed — run \\`db replay-deadletters\\``\n : `${n} ${noun} dead-lettered for '${slug}', which resolves to NO fleet site — leads are being dropped; run \\`ensure-site ${slug}\\` then \\`db replay-deadletters\\``,\n // No link for an unresolvable slug: /s/<slug> would 404, and the\n // collectors' own rule is never to render a broken link.\n ...(site ? { url: dashboardUrl(baseUrl, site.name) } : {}),\n severity: \"critical\",\n metric: n,\n });\n }\n return items;\n}\n\nexport function collectAnalyticsFailures(\n sites: WebsiteRow[],\n baseUrl: string,\n now: Date = new Date(),\n): AttentionItem[] {\n const items: AttentionItem[] = [];\n for (const s of sites) {\n const at = s.analyticsSoftFailAt;\n if (at === null) continue;\n const ageMs = now.getTime() - Date.parse(at);\n // Unparseable timestamp (NaN) → keep the item (don't silently drop a real failure\n // on a parse glitch); only a parseable, beyond-window age is skipped as stale.\n if (Number.isFinite(ageMs) && ageMs > ANALYTICS_SOFT_FAIL_STALE_DAYS * MS_PER_DAY) continue;\n items.push({\n key: `analytics:${s.id}`,\n kind: \"analytics\",\n siteName: s.name,\n title: \"GA/Search enrichment failing (analytics blank)\",\n url: dashboardUrl(baseUrl, s.name),\n severity: \"warning\",\n metric: 1,\n });\n }\n return items;\n}\n\n/** How current a `fail` / `unknown` must be to still CLAIM the fleet's present\n * state. The sweep is nightly, so 3 days (mirrors GITHUB_SIGNALS_STALE_DAYS)\n * tolerates a weekend of runner flakes without letting a months-old finding drive\n * a \"models diverge right now\" alarm. Past it the finding does not vanish — it\n * converts to the staleness item below, which claims nothing about the models. */\nconst PRISMIC_DRIFT_STALE_DAYS = 3;\n\n/** How long a `pass` may go unrefreshed before it is treated as unverified.\n *\n * Deliberately LONGER than the currency window, and the asymmetry is the point.\n * An old `fail` stops being reported as CURRENT drift at 3 days because it may\n * already be fixed. An old `pass` is a green claim nobody has re-established, and\n * escalating that at 3 days would light up the WHOLE fleet after a long weekend of\n * runner flakes — the noise that gets a real alarm muted. A week is the interval\n * over which a silently-dead nightly actually matters. */\nconst PRISMIC_STALE_PASS_DAYS = 7;\n\n/**\n * True when the nightly `prismic-models --fleet turso` sweep is EXPECTED to\n * cover this site. Mirrors the fleet inventory's own filter (src/inventory/\n * select.ts): live `maintained` sites (active, not pre-launch) that carry a\n * `url` AND a Name that yields a slug. Deliberately duplicated rather than\n * imported — the inventory builds `Site` objects and needs a workdir; this is a\n * pure predicate over a row — but the two must stay in step: widen the inventory\n * and widen this. That is no longer left to memory: every shape of row is put\n * through BOTH implementations by tests/alerts/prismic-sweep-scope.test.ts, which\n * goes red the moment they diverge.\n *\n * THE SLUG CLAUSE IS NOT COSMETIC, and it was the first drift this pair produced.\n * The inventory drops an empty-slug row (`siteSlug(name) === \"\"`) with a warning,\n * because such a row can neither form a checkout path nor be matched back to its\n * Websites row on write-back — and `writeSweepToAirtable` joins by that same slug,\n * so even a verdict computed for it could never land. Without this clause a live\n * site named in a script with no `[a-z0-9]` (or with an empty Name) was covered\n * here and swept nowhere: the permanent, un-ackable morning email this predicate\n * exists to prevent, produced by the predicate itself.\n *\n * Only the STALENESS escalation consults it. A `fail`/`unknown` is a verdict some\n * run actually established and is reported wherever it came from; the staleness\n * item is an alarm invented FROM AN ABSENCE, and an absence is only wrong where a\n * sweep was owed. A deprecated site that left the inventory would otherwise carry\n * a frozen `pass` into the digest every morning forever — un-ackable (attention\n * items sit above the accepted-watch mute) and unfixable except by hand-clearing\n * an Airtable cell.\n */\nfunction prismicSweepCovers(s: WebsiteRow): boolean {\n return (\n s.status !== null &&\n ACTIVE_STATUSES.has(s.status) &&\n !isPreLaunch(s.status) &&\n s.url.length > 0 &&\n siteSlug(s.name).length > 0\n );\n}\n\n/**\n * One item per site whose nightly Prismic model verdict needs a human, from the\n * `prismic-models --fleet --write-back` sweep. PURE (`now` injected).\n *\n * The verdict is THREE-valued plus blank, and each state gets its own key so the\n * digest's snapshot diff (keyed on `key`) can never let one condition stand in for\n * another — a site sliding from drift to \"couldn't check\" re-news as NEW rather\n * than diffing \"standing\" against the drift it replaced (both carry `metric` 1):\n *\n * - `fail`, fresh → `prismic-drift:<siteId>` — repo and Prismic diverge.\n * - `unknown`, fresh → `prismic-unknown:<siteId>` — the check RAN AND COULD NOT\n * ANSWER (unreadable checkout, dead write token, unreachable Prismic). Its own\n * wording, because reporting a dead token as \"models diverge\" sends the\n * operator to fix a model when the job is to fix a secret.\n * - any verdict nobody has re-established → `prismic-stale:<siteId>` — a `pass`\n * older than {@link PRISMIC_STALE_PASS_DAYS} (or undateable), or a `fail` /\n * `unknown` older than {@link PRISMIC_DRIFT_STALE_DAYS}. A stale verdict is\n * never silence: dropping it would make \"nobody could evaluate this\" read\n * exactly like \"this is fine\", which is the failure this whole column exists\n * to close. Gated by {@link prismicSweepCovers}.\n * - blank (never swept, or not a Prismic site) → nothing, at any age. There is\n * no claim to un-verify.\n *\n * A `pass` inside its window is the ONLY silent verdict. At most one item per site\n * — the Airtable cell holds one state at a time.\n *\n * `warning`, not `critical`: on the cockpit ANY item already tiers the site 🔴, so\n * severity buys only (a) piercing the pre-launch mute and (b) sorting first in the\n * needs-you feed. `critical` is reserved for the guardrails that are actively\n * losing leads or breaking a live page (Turnstile, notify-bounce, delivery); a\n * model that drifted — or a check that could not run — is neither, and piercing\n * the pre-launch mute would alarm on exactly the sites the sweep excludes by\n * design.\n */\n/**\n * Is an operator's acceptance of a known divergence still in force?\n *\n * Every uncertain answer is `false` — an absent, blank or unparseable cell leaves\n * the alarm ringing. The asymmetry is the point: a mistyped date costing one noisy\n * item is recoverable, and a mistyped date silently muting a live finding is the\n * exact failure this column was built to close. The boundary belongs to the alarm\n * too: an ack \"until 09:00\" has nothing left to say at 09:00.\n */\nexport function prismicAckIsLive(ackUntil: string | null, now: Date): boolean {\n if (ackUntil === null || ackUntil.trim() === \"\") return false;\n const until = Date.parse(ackUntil);\n if (!Number.isFinite(until)) return false;\n return until > now.getTime();\n}\n\nexport function collectPrismicDriftAlerts(\n sites: WebsiteRow[],\n baseUrl: string,\n now: Date = new Date(),\n): AttentionItem[] {\n const items: AttentionItem[] = [];\n for (const s of sites) {\n const verdict = s.prismicModels;\n if (verdict === null) continue;\n\n const at = s.prismicModelsCheckedAt;\n // +Infinity when there is no timestamp, NaN when it won't parse — both mean\n // \"the age of this answer is unknowable\", which the two verdict families read\n // in OPPOSITE directions on purpose (see `unverified`).\n const ageMs = at === null ? Number.POSITIVE_INFINITY : now.getTime() - Date.parse(at);\n const ageKnown = Number.isFinite(ageMs);\n const windowDays = verdict === \"pass\" ? PRISMIC_STALE_PASS_DAYS : PRISMIC_DRIFT_STALE_DAYS;\n // An undateable `pass` is unverified — a green claim nobody can date is not a\n // green claim. An undateable `fail`/`unknown` is NOT downgraded: never silently\n // drop a real finding over a parse glitch.\n const unverified = ageKnown ? ageMs > windowDays * MS_PER_DAY : verdict === \"pass\";\n\n if (unverified) {\n if (!prismicSweepCovers(s)) continue;\n items.push({\n key: `prismic-stale:${s.id}`,\n kind: \"prismic-drift\",\n siteName: s.name,\n title: `Prismic model check has not run recently — the last verdict (\"${verdict}\") is unverified`,\n url: dashboardUrl(baseUrl, s.name),\n severity: \"warning\",\n metric: 1,\n });\n continue;\n }\n\n if (verdict === \"pass\") continue;\n\n const first = s.prismicModelsDrift\n ?.split(\"\\n\")\n .find((l) => l.trim() !== \"\")\n ?.trim();\n if (verdict === \"unknown\") {\n items.push({\n key: `prismic-unknown:${s.id}`,\n kind: \"prismic-drift\",\n siteName: s.name,\n title: first\n ? `Prismic model check could not run — ${first}`\n : \"Prismic model check could not run\",\n url: dashboardUrl(baseUrl, s.name),\n severity: \"warning\",\n metric: 1,\n });\n continue;\n }\n\n // The operator has reviewed THIS divergence and accepted it until a date they\n // set — see `WebsiteRow.prismicAckUntil`. Reached only on `fail`: the `unknown`\n // and staleness branches above have already returned, so an ack can never mute\n // \"the check could not run\" or \"nobody has re-established this\".\n if (prismicAckIsLive(s.prismicAckUntil, now)) continue;\n\n items.push({\n key: `prismic-drift:${s.id}`,\n kind: \"prismic-drift\",\n siteName: s.name,\n title: first\n ? `Prismic models diverge from the repo — ${first}`\n : \"Prismic models diverge from the repo\",\n url: dashboardUrl(baseUrl, s.name),\n severity: \"warning\",\n metric: 1,\n });\n }\n return items;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
hostsMatch
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-Z7FDJ35L.js";
|
|
4
4
|
import {
|
|
5
5
|
operatorEmail
|
|
6
6
|
} from "./chunk-5ML7FMBD.js";
|
|
@@ -401,4 +401,4 @@ export {
|
|
|
401
401
|
notifySubmission,
|
|
402
402
|
makeNotify
|
|
403
403
|
};
|
|
404
|
-
//# sourceMappingURL=chunk-
|
|
404
|
+
//# sourceMappingURL=chunk-PXRN2MER.js.map
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fetchGaUsers,
|
|
3
3
|
fetchSearch
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DRNZVORX.js";
|
|
5
5
|
import {
|
|
6
6
|
announcementSiteExtras,
|
|
7
7
|
resolveCopy
|
|
8
8
|
} from "./chunk-KJGKZLCT.js";
|
|
9
9
|
import {
|
|
10
10
|
siteSlug
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-ICSRWW5Q.js";
|
|
12
12
|
|
|
13
13
|
// src/reports/report-data.ts
|
|
14
14
|
var PREVIEW_WINDOW_DAYS = 30;
|
|
@@ -60,4 +60,4 @@ export {
|
|
|
60
60
|
scoresFromRow,
|
|
61
61
|
buildReportDataForSite
|
|
62
62
|
};
|
|
63
|
-
//# sourceMappingURL=chunk-
|
|
63
|
+
//# sourceMappingURL=chunk-PZZ6HM5T.js.map
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
sleep,
|
|
17
17
|
summarizeAccessibility,
|
|
18
18
|
usablePages
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-LAJ3FF54.js";
|
|
20
20
|
import {
|
|
21
21
|
checkGoal
|
|
22
22
|
} from "./chunk-T7ZUUOZ2.js";
|
|
@@ -170,7 +170,7 @@ function runChecks(crawl) {
|
|
|
170
170
|
if (!p.raw || !p.rendered) continue;
|
|
171
171
|
const renderedWords = wordSet(p.rendered.text);
|
|
172
172
|
if (renderedWords.size === 0) continue;
|
|
173
|
-
const rawWords = wordSet(p.raw.text);
|
|
173
|
+
const rawWords = wordSet(`${p.raw.text} ${p.raw.dataText ?? ""}`);
|
|
174
174
|
let missing = 0;
|
|
175
175
|
for (const w of renderedWords) if (!rawWords.has(w)) missing++;
|
|
176
176
|
perPage.push({
|
|
@@ -304,6 +304,7 @@ import { randomBytes } from "crypto";
|
|
|
304
304
|
import { z } from "zod";
|
|
305
305
|
|
|
306
306
|
// src/prospect/questions.ts
|
|
307
|
+
import { createHash } from "crypto";
|
|
307
308
|
var QUESTION_SET_VERSION = 1;
|
|
308
309
|
var UNIVERSAL = [
|
|
309
310
|
{ id: "cost", question: "What does this cost?" },
|
|
@@ -365,6 +366,15 @@ function questionSetFor(goal) {
|
|
|
365
366
|
questions: [...UNIVERSAL, ...specific]
|
|
366
367
|
};
|
|
367
368
|
}
|
|
369
|
+
function questionSetFromChosen(questions) {
|
|
370
|
+
const cleaned = questions.map((q) => q.trim()).filter((q) => q !== "");
|
|
371
|
+
const digest = createHash("sha256").update(cleaned.join("\n")).digest("hex").slice(0, 8);
|
|
372
|
+
return {
|
|
373
|
+
id: `chosen-${digest}-v${QUESTION_SET_VERSION}`,
|
|
374
|
+
goal: "unknown",
|
|
375
|
+
questions: cleaned.map((question, i) => ({ id: `chosen-${i + 1}`, question }))
|
|
376
|
+
};
|
|
377
|
+
}
|
|
368
378
|
|
|
369
379
|
// src/prospect/analyze.ts
|
|
370
380
|
var MAX_PAGES = 12;
|
|
@@ -676,13 +686,22 @@ function reconcileFixes(fixes, goalFit) {
|
|
|
676
686
|
const met = new Set(goalFit.requirements.filter((r) => r.status === "met").map((r) => r.key));
|
|
677
687
|
return fixes.filter((f) => !(f.addresses && met.has(f.addresses)));
|
|
678
688
|
}
|
|
679
|
-
async function analyzeSite(url, crawl, checks, deps = defaultAnalyzeDeps(), goal = "unknown", goalFit = null) {
|
|
680
|
-
const
|
|
689
|
+
async function analyzeSite(url, crawl, checks, deps = defaultAnalyzeDeps(), goal = "unknown", goalFit = null, chosen = {}) {
|
|
690
|
+
const clean = (xs) => (xs ?? []).map((x) => x.trim()).filter((x) => x !== "");
|
|
691
|
+
const chosenQuestions = clean(chosen.questions);
|
|
692
|
+
const chosenTerms = clean(chosen.terms);
|
|
693
|
+
const set = chosenQuestions.length > 0 ? questionSetFromChosen(chosenQuestions) : questionSetFor(goal);
|
|
681
694
|
const raw = await deps.run(buildAnalyzeInput(url, crawl, checks, set, goalFit));
|
|
682
695
|
const parsed = AnalyzeSchema.parse(raw);
|
|
683
696
|
const conformed = {
|
|
684
697
|
...parsed,
|
|
685
698
|
questionSetId: set.id,
|
|
699
|
+
questionsSource: chosenQuestions.length > 0 ? "chosen" : "generated",
|
|
700
|
+
// The operator's searches replace the model's outright. They are the five
|
|
701
|
+
// searches "Where you stand" is built from, and the whole reason to choose
|
|
702
|
+
// them is that a comparison over time needs them to stay fixed.
|
|
703
|
+
categoryQueries: chosenTerms.length > 0 ? chosenTerms : parsed.categoryQueries,
|
|
704
|
+
termsSource: chosenTerms.length > 0 ? "chosen" : "generated",
|
|
686
705
|
buyerQuestions: conformToSet(parsed.buyerQuestions, set),
|
|
687
706
|
// Stamped here, at the one place model fixes enter the system, so the
|
|
688
707
|
// renderer can label them as judgement and never as a finding.
|
|
@@ -922,6 +941,19 @@ function isDistinctiveName(brand) {
|
|
|
922
941
|
if (tokens.length < 2) return false;
|
|
923
942
|
return tokens.some((t) => !CATEGORY_WORDS.has(t));
|
|
924
943
|
}
|
|
944
|
+
function domainLabel(domain) {
|
|
945
|
+
const host = domain.replace(/^www\./i, "").toLowerCase();
|
|
946
|
+
return (host.split(".")[0] ?? "").replace(/[^a-z0-9]/g, "");
|
|
947
|
+
}
|
|
948
|
+
function isNamesake(domain, brand) {
|
|
949
|
+
if (!isDistinctiveName(brand)) return false;
|
|
950
|
+
const collapsed = normalizeForMatch(brand).replace(/ /g, "");
|
|
951
|
+
if (!collapsed) return false;
|
|
952
|
+
const label = domainLabel(domain);
|
|
953
|
+
if (label === collapsed) return true;
|
|
954
|
+
if (!label.startsWith(collapsed)) return false;
|
|
955
|
+
return CATEGORY_WORDS.has(label.slice(collapsed.length));
|
|
956
|
+
}
|
|
925
957
|
function isRateLimited(err) {
|
|
926
958
|
const message = err instanceof Error ? err.message : String(err);
|
|
927
959
|
return /429|529/.test(message);
|
|
@@ -1001,6 +1033,13 @@ async function runVisibilityProbes(input, engines, opts = {}) {
|
|
|
1001
1033
|
visibilityScore,
|
|
1002
1034
|
brandedRecognized,
|
|
1003
1035
|
competitorsSeen: [...competitorCounts.entries()].map(([domain, count]) => ({ domain, count })).sort((a, b) => b.count - a.count).slice(0, 8),
|
|
1036
|
+
// Drawn from the same counts rather than removed from them: `competitorsSeen`
|
|
1037
|
+
// is what every stored report and the current renderer read, and quietly
|
|
1038
|
+
// changing what it contains would rewrite documents already sent. The
|
|
1039
|
+
// renderer lifts these out for its own call-out and prints the remainder as
|
|
1040
|
+
// competitors. Not capped at 8 — a namesake is never a long list, and
|
|
1041
|
+
// truncating the one finding this exists to surface would be perverse.
|
|
1042
|
+
namesakes: [...competitorCounts.entries()].filter(([domain]) => isNamesake(domain, brand)).map(([domain, count]) => ({ domain, count })).sort((a, b) => b.count - a.count || a.domain.localeCompare(b.domain)),
|
|
1004
1043
|
categoryProbes: { attempted: categoryAttempted, answered: categoryAnswers.length },
|
|
1005
1044
|
// Derived from `answers` above rather than accumulated during the run: it is
|
|
1006
1045
|
// a pure read of citations already recorded, so computing it here keeps the
|
|
@@ -1274,7 +1313,7 @@ async function classifyDomains(prospectUrl, prospectPhones, domains, deps) {
|
|
|
1274
1313
|
}
|
|
1275
1314
|
|
|
1276
1315
|
// src/prospect/accuracy.ts
|
|
1277
|
-
var MAX_PAGES2 =
|
|
1316
|
+
var MAX_PAGES2 = 20;
|
|
1278
1317
|
var MAX_TOTAL_CHARS = 12e4;
|
|
1279
1318
|
var MAX_ASSERTIONS = 12;
|
|
1280
1319
|
var NO_CONFLATION = { detected: false, otherNames: [], engineQuote: null };
|
|
@@ -1361,6 +1400,10 @@ function pathDepth2(url) {
|
|
|
1361
1400
|
function viewOf(page) {
|
|
1362
1401
|
return page.rendered ?? page.raw;
|
|
1363
1402
|
}
|
|
1403
|
+
function siteUrlCountOf(crawl) {
|
|
1404
|
+
const sitemap = crawl.sitemap;
|
|
1405
|
+
return sitemap?.present ? sitemap.urlCount : null;
|
|
1406
|
+
}
|
|
1364
1407
|
function fullSiteText(crawl) {
|
|
1365
1408
|
return crawl.pages.map((p) => viewOf(p)?.text ?? "").join("\n");
|
|
1366
1409
|
}
|
|
@@ -1587,6 +1630,7 @@ async function checkAccuracy(url, crawl, answers, prospectPhones, deps) {
|
|
|
1587
1630
|
siteFullyRead: false,
|
|
1588
1631
|
pagesRead: 0,
|
|
1589
1632
|
pagesTotal: crawl.pages.length,
|
|
1633
|
+
siteUrlCount: siteUrlCountOf(crawl),
|
|
1590
1634
|
answersRead: 0,
|
|
1591
1635
|
conflation: NO_CONFLATION
|
|
1592
1636
|
};
|
|
@@ -1626,6 +1670,7 @@ async function checkAccuracy(url, crawl, answers, prospectPhones, deps) {
|
|
|
1626
1670
|
siteFullyRead: input.fullyRead,
|
|
1627
1671
|
pagesRead: input.pagesRead,
|
|
1628
1672
|
pagesTotal: crawl.pages.length,
|
|
1673
|
+
siteUrlCount: siteUrlCountOf(crawl),
|
|
1629
1674
|
answersRead: branded.length,
|
|
1630
1675
|
conflation
|
|
1631
1676
|
};
|
|
@@ -1925,7 +1970,7 @@ function claudeCodeEngine(run = defaultClaudeCodeRun) {
|
|
|
1925
1970
|
function defaultLighthouseDeps() {
|
|
1926
1971
|
return {
|
|
1927
1972
|
async audit(site) {
|
|
1928
|
-
const { lighthouseAudit } = await import("./lighthouse-
|
|
1973
|
+
const { lighthouseAudit } = await import("./lighthouse-52WSPZ5D.js");
|
|
1929
1974
|
return lighthouseAudit({ site });
|
|
1930
1975
|
}
|
|
1931
1976
|
};
|
|
@@ -2657,7 +2702,12 @@ function measuredFixes(input) {
|
|
|
2657
2702
|
impact: "medium",
|
|
2658
2703
|
effort: "low",
|
|
2659
2704
|
tier: "technical",
|
|
2660
|
-
addresses
|
|
2705
|
+
// A stable key, because `mergeFixes` dedupes on `addresses` and every
|
|
2706
|
+
// fix here used to pass null — so there was no handle to match on, and
|
|
2707
|
+
// our own report printed "Give 2 pages a top heading" (measured) and
|
|
2708
|
+
// "Give the two pages with no h1 a proper headline" (model) as fixes 2
|
|
2709
|
+
// and 10. The same defect, twice, in one document.
|
|
2710
|
+
addresses: "headings-h1",
|
|
2661
2711
|
origin: "measured"
|
|
2662
2712
|
});
|
|
2663
2713
|
}
|
|
@@ -2668,7 +2718,7 @@ function measuredFixes(input) {
|
|
|
2668
2718
|
impact: "medium",
|
|
2669
2719
|
effort: "low",
|
|
2670
2720
|
tier: "technical",
|
|
2671
|
-
addresses:
|
|
2721
|
+
addresses: "meta-canonical",
|
|
2672
2722
|
origin: "measured"
|
|
2673
2723
|
});
|
|
2674
2724
|
}
|
|
@@ -2813,7 +2863,13 @@ async function runProspectAudit(url, opts, deps = {}) {
|
|
|
2813
2863
|
checks.data,
|
|
2814
2864
|
deps.analyze ?? envAnalyzeDeps(),
|
|
2815
2865
|
opts.goal ?? "unknown",
|
|
2816
|
-
operatorFit
|
|
2866
|
+
operatorFit,
|
|
2867
|
+
// #676. Threaded exactly like `goal`: it must reach the stage that
|
|
2868
|
+
// asks, not be applied after it.
|
|
2869
|
+
{
|
|
2870
|
+
...opts.terms ? { terms: opts.terms } : {},
|
|
2871
|
+
...opts.questions ? { questions: opts.questions } : {}
|
|
2872
|
+
}
|
|
2817
2873
|
)
|
|
2818
2874
|
)
|
|
2819
2875
|
) : { ok: false, error: ANALYZE_SKIPPED };
|
|
@@ -2943,6 +2999,7 @@ async function runProspectAudit(url, opts, deps = {}) {
|
|
|
2943
2999
|
}
|
|
2944
3000
|
|
|
2945
3001
|
export {
|
|
3002
|
+
domainOf,
|
|
2946
3003
|
resolveBusinessName,
|
|
2947
3004
|
envAnalyzeDeps,
|
|
2948
3005
|
envAccuracyDeps,
|
|
@@ -2957,4 +3014,4 @@ export {
|
|
|
2957
3014
|
mergeFixes,
|
|
2958
3015
|
runProspectAudit
|
|
2959
3016
|
};
|
|
2960
|
-
//# sourceMappingURL=chunk-
|
|
3017
|
+
//# sourceMappingURL=chunk-QFSL2L2B.js.map
|