@reddoorla/maintenance 0.87.0 → 0.88.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/dist/airtable-QG5ICI3Y.js +9 -0
  2. package/dist/{announce-I2Z4JN3I.js → announce-GKD5YJNB.js} +9 -9
  3. package/dist/{blux-ZGDSHI3U.js → blux-EATHOHKK.js} +5 -5
  4. package/dist/{bump-deps-CGVELRLK.js → bump-deps-KGH5UYSF.js} +4 -4
  5. package/dist/{chunk-SDDPUAWH.js → chunk-6I6L3CMX.js} +2 -2
  6. package/dist/{chunk-NXNPDCF3.js → chunk-6UCVWYDA.js} +11 -1
  7. package/dist/chunk-6UCVWYDA.js.map +1 -0
  8. package/dist/{chunk-ZBDZTJUO.js → chunk-7FFUBF4I.js} +5 -2
  9. package/dist/chunk-7FFUBF4I.js.map +1 -0
  10. package/dist/{chunk-3JQ2FO3D.js → chunk-B23FFWU4.js} +2 -2
  11. package/dist/{chunk-DJZWYIPP.js → chunk-BGTYPVLT.js} +1 -1
  12. package/dist/chunk-BGTYPVLT.js.map +1 -0
  13. package/dist/chunk-BRYXFH77.js +70 -0
  14. package/dist/chunk-BRYXFH77.js.map +1 -0
  15. package/dist/{chunk-HSI6Q7MI.js → chunk-CEW5PEHZ.js} +168 -31
  16. package/dist/chunk-CEW5PEHZ.js.map +1 -0
  17. package/dist/{chunk-NDWYJLRF.js → chunk-DGZWFI2G.js} +3 -3
  18. package/dist/{chunk-HHMMUR4P.js → chunk-GQDDWYU2.js} +45 -8
  19. package/dist/chunk-GQDDWYU2.js.map +1 -0
  20. package/dist/{chunk-BBUSGEES.js → chunk-J3WEZGIN.js} +6 -6
  21. package/dist/{chunk-BNM5POSJ.js → chunk-JRHIFA5R.js} +2 -2
  22. package/dist/{chunk-MT2JINSS.js → chunk-KJGKZLCT.js} +3 -3
  23. package/dist/{chunk-3SF7D4DY.js → chunk-MFHS7IQ2.js} +36 -1
  24. package/dist/{chunk-3SF7D4DY.js.map → chunk-MFHS7IQ2.js.map} +1 -1
  25. package/dist/{chunk-SNSTJLQW.js → chunk-NO74DXDP.js} +48 -2
  26. package/dist/chunk-NO74DXDP.js.map +1 -0
  27. package/dist/{chunk-C6B4RHXR.js → chunk-O3GT46R2.js} +8 -2
  28. package/dist/chunk-O3GT46R2.js.map +1 -0
  29. package/dist/{chunk-C6CLRKPV.js → chunk-PR343K7F.js} +2 -2
  30. package/dist/{chunk-CROKUHJB.js → chunk-Q47DU255.js} +4 -4
  31. package/dist/{chunk-EVBCK2KC.js → chunk-RARREDQE.js} +2 -1
  32. package/dist/{chunk-EVBCK2KC.js.map → chunk-RARREDQE.js.map} +1 -1
  33. package/dist/chunk-RUPPWAD6.js +7 -0
  34. package/dist/chunk-RUPPWAD6.js.map +1 -0
  35. package/dist/{chunk-VSBFM7DS.js → chunk-SJOXWJ5Y.js} +3 -3
  36. package/dist/{chunk-HEWSRXYV.js → chunk-ULHPALI5.js} +3 -3
  37. package/dist/{chunk-XUGSZP7Z.js → chunk-W45FZEGE.js} +2 -2
  38. package/dist/{chunk-NCMZMLY7.js → chunk-WPL5GERE.js} +5 -69
  39. package/dist/chunk-WPL5GERE.js.map +1 -0
  40. package/dist/{chunk-JHGHLKFS.js → chunk-WS6NU475.js} +2 -2
  41. package/dist/{chunk-DEWJMMG3.js → chunk-XKDCR4UE.js} +19 -8
  42. package/dist/chunk-XKDCR4UE.js.map +1 -0
  43. package/dist/{chunk-ZGBEIWSC.js → chunk-XP5WM2LV.js} +5 -5
  44. package/dist/{chunk-TD2NNXLZ.js → chunk-XSOINOPY.js} +8 -8
  45. package/dist/{chunk-6TWWY7ZU.js → chunk-Z3JXLULD.js} +2 -2
  46. package/dist/cli/bin.js +31 -28
  47. package/dist/cli/bin.js.map +1 -1
  48. package/dist/cli/commands/audit.d.ts +4 -0
  49. package/dist/cli/commands/audit.js +16 -13
  50. package/dist/cli/commands/audit.js.map +1 -1
  51. package/dist/client-JYKPVCJX.js +11 -0
  52. package/dist/configs/playwright-a11y.js +1 -1
  53. package/dist/{convert-to-pnpm-QFJOEP5V.js → convert-to-pnpm-NWB5FCQ2.js} +4 -4
  54. package/dist/{db-UXNIZ2RG.js → db-DXUUAMXT.js} +121 -26
  55. package/dist/db-DXUUAMXT.js.map +1 -0
  56. package/dist/{digest-FW6QMZZJ.js → digest-NBLECFM7.js} +110 -29
  57. package/dist/digest-NBLECFM7.js.map +1 -0
  58. package/dist/digest-collectors-NM7WUWQP.js +50 -0
  59. package/dist/digest-state-3KHZF7GD.js +50 -0
  60. package/dist/digest-state-3KHZF7GD.js.map +1 -0
  61. package/dist/digest-state-F37XUCS5.js +13 -0
  62. package/dist/{dump-P6EBSIOH.js → dump-EPL7AZFR.js} +38 -13
  63. package/dist/dump-EPL7AZFR.js.map +1 -0
  64. package/dist/{email-KJXASVH5.js → email-FMMEUPX6.js} +16 -6
  65. package/dist/{email-KJXASVH5.js.map → email-FMMEUPX6.js.map} +1 -1
  66. package/dist/{ensure-site-4DCEBAPM.js → ensure-site-FU6527OU.js} +2 -2
  67. package/dist/{fleet-state-7TJ2GYT2.js → fleet-state-R5LU7IC3.js} +4 -2
  68. package/dist/{forms-notify-target-2EAWCWBW.js → forms-notify-target-SGDGXXK5.js} +6 -6
  69. package/dist/{github-signals-E2WPYTBK.js → github-signals-DB25UWY4.js} +7 -6
  70. package/dist/{github-signals-E2WPYTBK.js.map → github-signals-DB25UWY4.js.map} +1 -1
  71. package/dist/{header-image-6IC53W27.js → header-image-7A7XLVEU.js} +2 -2
  72. package/dist/{health-endpoint-RFXL2F4A.js → health-endpoint-UVX6R5N5.js} +4 -4
  73. package/dist/{health-mirror-L5JBEJVA.js → health-mirror-GCDN7BJL.js} +11 -6
  74. package/dist/health-mirror-GCDN7BJL.js.map +1 -0
  75. package/dist/index.d.ts +3 -0
  76. package/dist/index.js +136 -95
  77. package/dist/index.js.map +1 -1
  78. package/dist/{init-XRAQZYID.js → init-32AWPS7A.js} +9 -9
  79. package/dist/{launch-IX4Y5JJC.js → launch-52WUE4TZ.js} +12 -11
  80. package/dist/{launch-IX4Y5JJC.js.map → launch-52WUE4TZ.js.map} +1 -1
  81. package/dist/migrate-YITCXBLS.js +7 -0
  82. package/dist/{notify-C645PGD5.js → notify-4HP4GZ2H.js} +5 -5
  83. package/dist/{onboard-O4IAHJMO.js → onboard-BKCYBAVW.js} +4 -4
  84. package/dist/{orchestrate-YLWQ6KK2.js → orchestrate-JNKCWU5K.js} +9 -9
  85. package/dist/{parity-56KA2DGR.js → parity-NBYZUBFU.js} +4 -2
  86. package/dist/pdf-N5QLQ5ZQ.js +38 -0
  87. package/dist/pdf-N5QLQ5ZQ.js.map +1 -0
  88. package/dist/{pipeline-3AANZFCR.js → pipeline-YIBIQ7XK.js} +3 -3
  89. package/dist/{preflight-TNORKGCX.js → preflight-IMTYDDD4.js} +10 -10
  90. package/dist/{prismic-ci-G2AK53VL.js → prismic-ci-MY2U6NN7.js} +4 -4
  91. package/dist/{prismic-models-O6Z3OK3A.js → prismic-models-IJWZW2EO.js} +6 -6
  92. package/dist/prospect/types.d.ts +33 -0
  93. package/dist/{prospect-audit-KSXUNQ5J.js → prospect-audit-WJQI7V5V.js} +32 -13
  94. package/dist/prospect-audit-WJQI7V5V.js.map +1 -0
  95. package/dist/render-CVKE7IWC.js +10 -0
  96. package/dist/{render-from-row-BWSOIYDS.js → render-from-row-DOYJDO2L.js} +5 -5
  97. package/dist/{replay-C2LXMJZP.js → replay-4S6ENYCH.js} +3 -3
  98. package/dist/{report-R3E3Y7HR.js → report-2LM2SX67.js} +20 -20
  99. package/dist/{report-mirror-4MNORYQ4.js → report-mirror-AAK3MXHY.js} +12 -6
  100. package/dist/report-mirror-AAK3MXHY.js.map +1 -0
  101. package/dist/schema-75PJECAE.js +18 -0
  102. package/dist/schema-75PJECAE.js.map +1 -0
  103. package/dist/{self-updating-UCIWESAX.js → self-updating-NTCCWJYZ.js} +4 -4
  104. package/dist/{selftest-VGHWPN72.js → selftest-DIZ5JD2X.js} +12 -12
  105. package/dist/{site-mirror-NIREX5ME.js → site-mirror-Q763PLGG.js} +17 -7
  106. package/dist/site-mirror-Q763PLGG.js.map +1 -0
  107. package/dist/{smoke-suite-U3UKD3NS.js → smoke-suite-4JFZZYVW.js} +4 -4
  108. package/dist/{spam-classifier-XLBFCO7G.js → spam-classifier-T5H4VTP6.js} +4 -2
  109. package/dist/spam-classifier-T5H4VTP6.js.map +1 -0
  110. package/dist/{submissions-IQEJX6FK.js → submissions-TEHPBSZJ.js} +4 -4
  111. package/dist/{svelte-codemods-GOKXTBHW.js → svelte-codemods-QAPDZC6F.js} +4 -4
  112. package/dist/{sync-CTUVXMCK.js → sync-GBFV45WX.js} +4 -2
  113. package/dist/{sync-CTUVXMCK.js.map → sync-GBFV45WX.js.map} +1 -1
  114. package/dist/{sync-configs-SPTLFJCC.js → sync-configs-34JIA6KZ.js} +4 -4
  115. package/dist/{upgrade-W6ZR3FJH.js → upgrade-IR2OO3KP.js} +4 -4
  116. package/dist/{webhook-VOGOW3W6.js → webhook-XM6EV4XT.js} +2 -2
  117. package/dist/{write-audits-to-airtable-CMKHGJNQ.js → write-audits-to-airtable-OPBJXGS4.js} +5 -2
  118. package/dist/write-audits-to-airtable-OPBJXGS4.js.map +1 -0
  119. package/package.json +1 -1
  120. package/dist/airtable-ZBUPQJSC.js +0 -9
  121. package/dist/chunk-C6B4RHXR.js.map +0 -1
  122. package/dist/chunk-DEWJMMG3.js.map +0 -1
  123. package/dist/chunk-DJZWYIPP.js.map +0 -1
  124. package/dist/chunk-HHMMUR4P.js.map +0 -1
  125. package/dist/chunk-HSI6Q7MI.js.map +0 -1
  126. package/dist/chunk-NCMZMLY7.js.map +0 -1
  127. package/dist/chunk-NXNPDCF3.js.map +0 -1
  128. package/dist/chunk-SNSTJLQW.js.map +0 -1
  129. package/dist/chunk-ZBDZTJUO.js.map +0 -1
  130. package/dist/client-AUE3XUPO.js +0 -11
  131. package/dist/db-UXNIZ2RG.js.map +0 -1
  132. package/dist/digest-FW6QMZZJ.js.map +0 -1
  133. package/dist/dump-P6EBSIOH.js.map +0 -1
  134. package/dist/health-mirror-L5JBEJVA.js.map +0 -1
  135. package/dist/migrate-GQH237A6.js +0 -7
  136. package/dist/prospect-audit-KSXUNQ5J.js.map +0 -1
  137. package/dist/render-IFOGFB5T.js +0 -10
  138. package/dist/report-mirror-4MNORYQ4.js.map +0 -1
  139. package/dist/site-mirror-NIREX5ME.js.map +0 -1
  140. /package/dist/{airtable-ZBUPQJSC.js.map → airtable-QG5ICI3Y.js.map} +0 -0
  141. /package/dist/{announce-I2Z4JN3I.js.map → announce-GKD5YJNB.js.map} +0 -0
  142. /package/dist/{blux-ZGDSHI3U.js.map → blux-EATHOHKK.js.map} +0 -0
  143. /package/dist/{bump-deps-CGVELRLK.js.map → bump-deps-KGH5UYSF.js.map} +0 -0
  144. /package/dist/{chunk-SDDPUAWH.js.map → chunk-6I6L3CMX.js.map} +0 -0
  145. /package/dist/{chunk-3JQ2FO3D.js.map → chunk-B23FFWU4.js.map} +0 -0
  146. /package/dist/{chunk-NDWYJLRF.js.map → chunk-DGZWFI2G.js.map} +0 -0
  147. /package/dist/{chunk-BBUSGEES.js.map → chunk-J3WEZGIN.js.map} +0 -0
  148. /package/dist/{chunk-BNM5POSJ.js.map → chunk-JRHIFA5R.js.map} +0 -0
  149. /package/dist/{chunk-MT2JINSS.js.map → chunk-KJGKZLCT.js.map} +0 -0
  150. /package/dist/{chunk-C6CLRKPV.js.map → chunk-PR343K7F.js.map} +0 -0
  151. /package/dist/{chunk-CROKUHJB.js.map → chunk-Q47DU255.js.map} +0 -0
  152. /package/dist/{chunk-VSBFM7DS.js.map → chunk-SJOXWJ5Y.js.map} +0 -0
  153. /package/dist/{chunk-HEWSRXYV.js.map → chunk-ULHPALI5.js.map} +0 -0
  154. /package/dist/{chunk-XUGSZP7Z.js.map → chunk-W45FZEGE.js.map} +0 -0
  155. /package/dist/{chunk-JHGHLKFS.js.map → chunk-WS6NU475.js.map} +0 -0
  156. /package/dist/{chunk-ZGBEIWSC.js.map → chunk-XP5WM2LV.js.map} +0 -0
  157. /package/dist/{chunk-TD2NNXLZ.js.map → chunk-XSOINOPY.js.map} +0 -0
  158. /package/dist/{chunk-6TWWY7ZU.js.map → chunk-Z3JXLULD.js.map} +0 -0
  159. /package/dist/{client-AUE3XUPO.js.map → client-JYKPVCJX.js.map} +0 -0
  160. /package/dist/{convert-to-pnpm-QFJOEP5V.js.map → convert-to-pnpm-NWB5FCQ2.js.map} +0 -0
  161. /package/dist/{fleet-state-7TJ2GYT2.js.map → digest-collectors-NM7WUWQP.js.map} +0 -0
  162. /package/dist/{migrate-GQH237A6.js.map → digest-state-F37XUCS5.js.map} +0 -0
  163. /package/dist/{ensure-site-4DCEBAPM.js.map → ensure-site-FU6527OU.js.map} +0 -0
  164. /package/dist/{notify-C645PGD5.js.map → fleet-state-R5LU7IC3.js.map} +0 -0
  165. /package/dist/{forms-notify-target-2EAWCWBW.js.map → forms-notify-target-SGDGXXK5.js.map} +0 -0
  166. /package/dist/{header-image-6IC53W27.js.map → header-image-7A7XLVEU.js.map} +0 -0
  167. /package/dist/{health-endpoint-RFXL2F4A.js.map → health-endpoint-UVX6R5N5.js.map} +0 -0
  168. /package/dist/{init-XRAQZYID.js.map → init-32AWPS7A.js.map} +0 -0
  169. /package/dist/{orchestrate-YLWQ6KK2.js.map → migrate-YITCXBLS.js.map} +0 -0
  170. /package/dist/{parity-56KA2DGR.js.map → notify-4HP4GZ2H.js.map} +0 -0
  171. /package/dist/{onboard-O4IAHJMO.js.map → onboard-BKCYBAVW.js.map} +0 -0
  172. /package/dist/{pipeline-3AANZFCR.js.map → orchestrate-JNKCWU5K.js.map} +0 -0
  173. /package/dist/{render-IFOGFB5T.js.map → parity-NBYZUBFU.js.map} +0 -0
  174. /package/dist/{render-from-row-BWSOIYDS.js.map → pipeline-YIBIQ7XK.js.map} +0 -0
  175. /package/dist/{preflight-TNORKGCX.js.map → preflight-IMTYDDD4.js.map} +0 -0
  176. /package/dist/{prismic-ci-G2AK53VL.js.map → prismic-ci-MY2U6NN7.js.map} +0 -0
  177. /package/dist/{prismic-models-O6Z3OK3A.js.map → prismic-models-IJWZW2EO.js.map} +0 -0
  178. /package/dist/{spam-classifier-XLBFCO7G.js.map → render-CVKE7IWC.js.map} +0 -0
  179. /package/dist/{write-audits-to-airtable-CMKHGJNQ.js.map → render-from-row-DOYJDO2L.js.map} +0 -0
  180. /package/dist/{replay-C2LXMJZP.js.map → replay-4S6ENYCH.js.map} +0 -0
  181. /package/dist/{report-R3E3Y7HR.js.map → report-2LM2SX67.js.map} +0 -0
  182. /package/dist/{self-updating-UCIWESAX.js.map → self-updating-NTCCWJYZ.js.map} +0 -0
  183. /package/dist/{selftest-VGHWPN72.js.map → selftest-DIZ5JD2X.js.map} +0 -0
  184. /package/dist/{smoke-suite-U3UKD3NS.js.map → smoke-suite-4JFZZYVW.js.map} +0 -0
  185. /package/dist/{submissions-IQEJX6FK.js.map → submissions-TEHPBSZJ.js.map} +0 -0
  186. /package/dist/{svelte-codemods-GOKXTBHW.js.map → svelte-codemods-QAPDZC6F.js.map} +0 -0
  187. /package/dist/{sync-configs-SPTLFJCC.js.map → sync-configs-34JIA6KZ.js.map} +0 -0
  188. /package/dist/{upgrade-W6ZR3FJH.js.map → upgrade-IR2OO3KP.js.map} +0 -0
  189. /package/dist/{webhook-VOGOW3W6.js.map → webhook-XM6EV4XT.js.map} +0 -0
@@ -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\";\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 function-health sweep older than this can't confirm the CURRENT widget state.\n * The sweep is nightly, so 3 days (mirrors GITHUB_SIGNALS_STALE_DAYS) tolerates a\n * weekend of 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 deployed\n * `/health` reports the Turnstile widget NOT configured (`turnstileWidget === \"fail\"`,\n * fresh per {@link TURNSTILE_WIDGET_STALE_DAYS}). That combination silently buckets\n * 100% of the site's real leads (token-less submissions escalate to spam_auto with\n * notify skipped) and the form-e2e probe cannot see it — testMode bypasses the gate —\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 const at = s.functionHealthCheckedAt;\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 title:\n \"Require Turnstile is ON but /health reports no widget — 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, number>,\n baseUrl: string,\n): AttentionItem[] {\n const items: AttentionItem[] = [];\n for (const s of sites) {\n const n = bouncedBySite.get(s.id) ?? 0;\n if (n < NOTIFY_BOUNCE_THRESHOLD) continue;\n items.push({\n key: `notify-bounce:${s.id}`,\n kind: \"notify-bounce\",\n siteName: s.name,\n title: `${n} lead notifications bounced (${NOTIFY_BOUNCE_WINDOW_DAYS}d) — check the point-of-contact address`,\n url: dashboardUrl(baseUrl, s.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 airtable` sweep is EXPECTED to\n * cover this site. Mirrors the Airtable inventory's own filter (src/inventory/\n * airtable.ts): live `maintenance` 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-airtable` 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
  runMigrations
3
- } from "./chunk-3SF7D4DY.js";
3
+ } from "./chunk-MFHS7IQ2.js";
4
4
  import {
5
5
  defaultCredentialsPath
6
6
  } from "./chunk-6VAL7XJT.js";
@@ -48,4 +48,4 @@ export {
48
48
  readDbConfig,
49
49
  openDb
50
50
  };
51
- //# sourceMappingURL=chunk-JHGHLKFS.js.map
51
+ //# sourceMappingURL=chunk-WS6NU475.js.map
@@ -1,14 +1,15 @@
1
1
  import {
2
2
  ANALYZE_SKIPPED,
3
- PROBES_SKIPPED
4
- } from "./chunk-HSI6Q7MI.js";
3
+ PROBES_SKIPPED,
4
+ resolveBusinessName
5
+ } from "./chunk-CEW5PEHZ.js";
5
6
  import {
6
7
  escapeHtml,
7
8
  safeUrl
8
- } from "./chunk-DJZWYIPP.js";
9
+ } from "./chunk-BGTYPVLT.js";
9
10
  import {
10
11
  hostnameOf
11
- } from "./chunk-EVBCK2KC.js";
12
+ } from "./chunk-RARREDQE.js";
12
13
 
13
14
  // src/prospect/render.ts
14
15
  var RED = "#d71920";
@@ -109,6 +110,9 @@ function buildProbesSection(p, businessNameUsed) {
109
110
  }
110
111
  const recognition = p.brandedRecognized ? "<p>When the engines were asked about the business by name, they recognized it.</p>" : businessNameUsed ? "<p>When the engines were asked about the business by name, they did not recognize it \u2014 a real citation of the site never showed up, even with the name handed to them.</p>" : "<p>The engines did not recognize the business \u2014 there was no business name available to ask them with, so this reflects a harder starting point than a named query would.</p>";
111
112
  const categoryCaveat = byKind.has("category") ? "" : `<p class="muted"><strong>No buyer-question (category) query was tested here</strong> \u2014 only name-recognition. An engine echoing back a name it was just given says nothing about whether a buyer who had never heard of the business would be shown it; treat the line above as a floor, not a visibility signal.</p>`;
113
+ const tally = p.categoryProbes;
114
+ const missing = tally ? tally.attempted - tally.answered : 0;
115
+ const degradedNotice = tally && missing > 0 ? `<p class="muted"><strong>${missing} of ${tally.attempted} buyer-question searches did not come back</strong> \u2014 the engine errored on them. They are counted as "not found", so the visibility figure above is a floor: the real number could be higher, not lower. Worth re-running before drawing a conclusion from it.</p>` : "";
112
116
  const groups = KIND_ORDER.filter((k) => byKind.has(k)).map((kind) => {
113
117
  const answers = byKind.get(kind) ?? [];
114
118
  const cards = answers.map(
@@ -116,13 +120,18 @@ function buildProbesSection(p, businessNameUsed) {
116
120
  <div class="q">${escapeHtml(a.engine)} \xB7 \u201C${escapeHtml(a.query)}\u201D</div>
117
121
  <p class="muted">Asked ${escapeHtml(formatIsoDate(a.askedAt))}</p>
118
122
  <p>${escapeHtml(a.snippet)}${a.truncated ? "\u2026" : ""}</p>
119
- <p class="muted">${a.domainCited || a.brandMentioned ? "You were named in this answer." : "You were not named in this answer."}${a.citedDomains.length ? ` Cited: ${escapeHtml(a.citedDomains.join(", "))}` : ""}</p>
123
+ <p class="muted">${// The scorer's own decision, recorded on the answer NOT
124
+ // `domainCited || brandMentioned`, which is a looser rule. Using the
125
+ // loose one printed "You were named in this answer" above a card
126
+ // contributing zero to the score beside it. Reports persisted before
127
+ // the field existed fall back to what they already said.
128
+ a.countedAsVisible ?? (a.domainCited || a.brandMentioned) ? "You were named in this answer." : "You were not named in this answer."}${a.citedDomains.length ? ` Sources the engine retrieved: ${escapeHtml(a.citedDomains.join(", "))}` : ""}</p>
120
129
  </div>`
121
130
  ).join("");
122
131
  return `<h3>${escapeHtml(KIND_LABEL[kind])}</h3>${cards}`;
123
132
  }).join("");
124
133
  const competitors = p.competitorsSeen.length ? `<h3>Who the engines cited instead</h3><ul>${p.competitorsSeen.map((c) => `<li>${escapeHtml(c.domain)} \u2014 ${c.count} time${c.count === 1 ? "" : "s"}</li>`).join("")}</ul>` : "";
125
- return recognition + categoryCaveat + groups + competitors;
134
+ return recognition + categoryCaveat + degradedNotice + groups + competitors;
126
135
  }
127
136
  function buildFindabilitySection(c) {
128
137
  const accessBlock = !c.crawlerAccessMeasured ? `<p class="muted">Crawler access: not measured \u2014 ${STAGE_FAILED_MESSAGE}.</p>` : c.crawlerAccess.blockedAi.length ? `<p><strong>Blocked AI crawlers:</strong> ${escapeHtml(
@@ -181,7 +190,9 @@ function renderProspectReport(result) {
181
190
  const findabilitySection = stageBody(result.checks, buildFindabilitySection);
182
191
  const lighthouseBlock = buildLighthouseBlock(result.lighthouse);
183
192
  const readabilitySection = stageBody(result.checks, buildReadabilitySection);
184
- const businessNameUsed = Boolean(result.businessName && result.businessName.trim());
193
+ const businessNameUsed = Boolean(
194
+ result.businessName && result.businessName.trim() && resolveBusinessName(result.businessName, result.url) === result.businessName.trim()
195
+ );
185
196
  const fixes = result.analyze.ok ? (() => {
186
197
  const sorted = [...result.analyze.data.fixes].sort(
187
198
  (x, y) => IMPACT_ORDER[x.impact] - IMPACT_ORDER[y.impact]
@@ -271,4 +282,4 @@ function renderProspectReport(result) {
271
282
  export {
272
283
  renderProspectReport
273
284
  };
274
- //# sourceMappingURL=chunk-DEWJMMG3.js.map
285
+ //# sourceMappingURL=chunk-XKDCR4UE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/prospect/render.ts"],"sourcesContent":["import { escapeHtml, safeUrl } from \"../util/html.js\";\nimport { hostnameOf } from \"../util/url.js\";\nimport { ANALYZE_SKIPPED, PROBES_SKIPPED } from \"./pipeline.js\";\nimport { resolveBusinessName } from \"./probes.js\";\nimport type {\n AnalyzeResult,\n ChecksResult,\n Fix,\n LighthouseScores,\n ProbeAnswer,\n ProbesResult,\n ProspectAuditResult,\n StageResult,\n} from \"./types.js\";\n\nconst RED = \"#d71920\";\nconst IMPACT_ORDER: Record<Fix[\"impact\"], number> = { high: 0, medium: 1, low: 2 };\nconst KIND_LABEL: Record<ProbeAnswer[\"kind\"], string> = {\n branded: \"Branded — asked about the business by name\",\n category: \"Category — the questions a buyer would actually type\",\n competitor: \"Competitor — head-to-head comparisons\",\n};\nconst KIND_ORDER: ProbeAnswer[\"kind\"][] = [\"category\", \"branded\", \"competitor\"];\n\n/** Terse product mapping so a recipient who has never seen a crawler's raw\n * user-agent string can place it — factual labelling, not a voice change. */\nconst AI_AGENT_LABELS: Record<string, string> = {\n GPTBot: \"ChatGPT\",\n \"OAI-SearchBot\": \"ChatGPT's search results\",\n ClaudeBot: \"Claude\",\n PerplexityBot: \"Perplexity\",\n \"Google-Extended\": \"Gemini and Google's AI Overviews\",\n CCBot: \"Common Crawl, used to train many AI models\",\n};\n\nfunction describeAgent(agent: string): string {\n const label = AI_AGENT_LABELS[agent];\n return label ? `${agent} (feeds ${label})` : agent;\n}\n\n/** Every genuine stage failure collapses to this one phrase — the real error\n * (status codes, retry counts, timeouts — operator vocabulary) stays in the\n * persisted JSON and CLI output for operators, but never reaches a stranger\n * who would read it as broken software rather than a diagnostic. */\nconst STAGE_FAILED_MESSAGE = \"we could not complete this part of the analysis\";\n\nconst STYLES = `\n :root { color-scheme: light; }\n * { box-sizing: border-box; }\n body { margin: 0; background: #faf8f5; color: #1a1a1a;\n font: 16px/1.6 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif; }\n .wrap { max-width: 860px; margin: 0 auto; padding: 48px 24px 80px; }\n h1, h2, h3 { font-family: Besley, Georgia, \"Times New Roman\", serif; font-weight: 600; line-height: 1.2; }\n h1 { font-size: 40px; margin: 0 0 8px; }\n h2 { font-size: 26px; margin: 48px 0 12px; border-top: 2px solid #e6e1da; padding-top: 24px; }\n h3 { font-size: 18px; margin: 24px 0 8px; }\n .lede { color: #57544f; margin: 0 0 8px; }\n .scores { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin: 32px 0; }\n .score { background: #fff; border: 1px solid #e6e1da; border-radius: 8px; padding: 16px; }\n .score .n { font-family: Besley, Georgia, serif; font-size: 38px; line-height: 1; color: ${RED}; }\n .score .n.na { font-size: 18px; color: #8a857e; }\n .score .l { font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: #57544f; margin-top: 8px; }\n .score .hint { font-size: 12px; color: #8a857e; margin-top: 4px; }\n .card { background: #fff; border: 1px solid #e6e1da; border-radius: 8px; padding: 16px; margin: 12px 0; }\n .q { font-weight: 600; }\n .tag { display: inline-block; font-size: 12px; letter-spacing: .06em; text-transform: uppercase;\n border-radius: 999px; padding: 2px 10px; border: 1px solid currentColor; }\n .yes { color: #14663c; } .partial { color: #8a6d00; } .no { color: ${RED}; }\n ul { padding-left: 20px; } li { margin: 6px 0; }\n .muted { color: #8a857e; }\n .cta { margin-top: 56px; background: ${RED}; color: #fff; border-radius: 8px; padding: 24px 28px; }\n .cta h2 { border: 0; padding: 0; margin: 0 0 8px; color: #fff; }\n .cta a { color: #fff; }\n table { width: 100%; border-collapse: collapse; margin-top: 8px; }\n td, th { text-align: left; padding: 6px 8px; border-bottom: 1px solid #eee; vertical-align: top; }\n @media print {\n body { background: #fff; }\n .card, .score, .cta { break-inside: avoid; }\n /* Browsers default \"print background graphics\" OFF, which drops the red\n .cta background but keeps its white text — the only ask in the report\n would otherwise print white-on-white and vanish. Fall back to dark\n text on no background with a visible border instead of relying on the\n background for contrast. */\n .cta { background: none; color: #1a1a1a; border: 2px solid ${RED}; }\n .cta h2, .cta a { color: #1a1a1a; }\n }\n`;\n\nfunction scoreCard(label: string, value: number | null, hint?: string): string {\n const n =\n value === null ? `<div class=\"n na\">Not measured</div>` : `<div class=\"n\">${value}</div>`;\n const hintHtml = hint ? `<div class=\"hint\">${escapeHtml(hint)}</div>` : \"\";\n return `<div class=\"score\">${n}<div class=\"l\">${escapeHtml(label)}</div>${hintHtml}</div>`;\n}\n\n/** Body for a stage that succeeded, or a uniform \"Not measured\" note when it didn't. */\nfunction stageBody<T>(stage: StageResult<T>, body: (data: T) => string): string {\n return stage.ok ? body(stage.data) : notMeasuredNote();\n}\n\n/** The stage's real error is deliberately not a parameter here — see\n * STAGE_FAILED_MESSAGE above. Every genuine failure reads the same\n * client-safe way, regardless of what actually broke. */\nfunction notMeasuredNote(): string {\n return `<p class=\"muted\">Not measured — ${STAGE_FAILED_MESSAGE}.</p>`;\n}\n\n/** A stage a human deliberately turned off (--no-probes, or analyze skipped\n * because checks failed upstream) must read as \"you asked us to skip this\" —\n * never as \"we tried and could not\", which is what a bare \"Not measured —\n * {error}\" would otherwise imply. Compares against the pipeline's exported\n * constants rather than a retyped string literal. */\nfunction skippableStageNote(error: string, skipConstant: string, skipMessage: string): string {\n if (error === skipConstant) {\n return `<p class=\"muted\">${escapeHtml(skipMessage)}</p>`;\n }\n return notMeasuredNote();\n}\n\n/** `new Date(iso).toLocaleDateString()` never throws on a bad timestamp — it\n * silently returns \"Invalid Date\" — so a try/catch here is dead code. Guard\n * on getTime() instead, and fall back to the raw ISO string so a corrupt\n * timestamp is at least visibly a timestamp rather than the literal words\n * \"Invalid Date\". */\nfunction formatIsoDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return iso;\n return d.toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n timeZone: \"UTC\",\n });\n}\n\n/** The probes section leads the report, so a branded-only run (analyze\n * failed upstream and supplied no buyer questions, or every category query\n * itself failed) must not let the branded recognition line read as a\n * positive discoverability finding on its own — an engine echoing a name\n * it was just handed proves nothing about whether a buyer who never named\n * the business would find it. */\n/** `businessNameUsed` is whether the engines were actually queried with the\n * business NAME — not merely whether one existed. `resolveBusinessName` falls\n * back to the bare domain for anything it cannot use, so a name can be present\n * and still never reach a query. Without this gate, \"even with the name handed\n * to them\" is a false claim: whenever businessName was empty, and (until the\n * caller was fixed) whenever the name was rejected and the probes searched for\n * the domain instead. Never derived from the raw ProbesResult, which has no\n * way to say \"no name was available\". */\nfunction buildProbesSection(p: ProbesResult, businessNameUsed: boolean): string {\n const byKind = new Map<ProbeAnswer[\"kind\"], ProbeAnswer[]>();\n for (const a of p.answers) {\n const bucket = byKind.get(a.kind) ?? [];\n bucket.push(a);\n byKind.set(a.kind, bucket);\n }\n\n const recognition = p.brandedRecognized\n ? \"<p>When the engines were asked about the business by name, they recognized it.</p>\"\n : businessNameUsed\n ? \"<p>When the engines were asked about the business by name, they did not recognize it — a real citation of the site never showed up, even with the name handed to them.</p>\"\n : \"<p>The engines did not recognize the business — there was no business name available to ask them with, so this reflects a harder starting point than a named query would.</p>\";\n\n const categoryCaveat = byKind.has(\"category\")\n ? \"\"\n : `<p class=\"muted\"><strong>No buyer-question (category) query was tested here</strong> — only name-recognition. An engine echoing back a name it was just given says nothing about whether a buyer who had never heard of the business would be shown it; treat the line above as a floor, not a visibility signal.</p>`;\n\n // A degraded run says so. The score now divides by what was ASKED, so failed\n // probes push it DOWN rather than silently inflating it — which is the safe\n // direction, but only if the reader is told the run was incomplete. Silence\n // here would trade one misleading number for another.\n // Bound to a local so the narrowing survives into the template — `missing > 0`\n // says nothing to the compiler about `p.categoryProbes` itself, and a stored\n // report from before the field existed genuinely does not have it.\n const tally = p.categoryProbes;\n const missing = tally ? tally.attempted - tally.answered : 0;\n const degradedNotice =\n tally && missing > 0\n ? `<p class=\"muted\"><strong>${missing} of ${tally.attempted} buyer-question searches did not come back</strong> — the engine errored on them. They are counted as \"not found\", so the visibility figure above is a floor: the real number could be higher, not lower. Worth re-running before drawing a conclusion from it.</p>`\n : \"\";\n\n const groups = KIND_ORDER.filter((k) => byKind.has(k))\n .map((kind) => {\n const answers = byKind.get(kind) ?? [];\n const cards = answers\n .map(\n (a) => `<div class=\"card\">\n <div class=\"q\">${escapeHtml(a.engine)} · “${escapeHtml(a.query)}”</div>\n <p class=\"muted\">Asked ${escapeHtml(formatIsoDate(a.askedAt))}</p>\n <p>${escapeHtml(a.snippet)}${a.truncated ? \"…\" : \"\"}</p>\n <p class=\"muted\">${\n // The scorer's own decision, recorded on the answer — NOT\n // `domainCited || brandMentioned`, which is a looser rule. Using the\n // loose one printed \"You were named in this answer\" above a card\n // contributing zero to the score beside it. Reports persisted before\n // the field existed fall back to what they already said.\n (a.countedAsVisible ?? (a.domainCited || a.brandMentioned))\n ? \"You were named in this answer.\"\n : \"You were not named in this answer.\"\n }${a.citedDomains.length ? ` Sources the engine retrieved: ${escapeHtml(a.citedDomains.join(\", \"))}` : \"\"}</p>\n </div>`,\n )\n .join(\"\");\n return `<h3>${escapeHtml(KIND_LABEL[kind])}</h3>${cards}`;\n })\n .join(\"\");\n\n const competitors = p.competitorsSeen.length\n ? `<h3>Who the engines cited instead</h3><ul>${p.competitorsSeen\n .map((c) => `<li>${escapeHtml(c.domain)} — ${c.count} time${c.count === 1 ? \"\" : \"s\"}</li>`)\n .join(\"\")}</ul>`\n : \"\";\n\n return recognition + categoryCaveat + degradedNotice + groups + competitors;\n}\n\n/** crawlerAccessMeasured is false only when the robots.txt fetch itself\n * failed, so the blocked/allowed lists are empty out of ignorance, not\n * because we confirmed access. Saying \"every crawler can reach the site\" in\n * that case would manufacture a finding from our own missing data.\n * sitemapMeasured/llmsTxtMeasured get the same treatment as\n * crawlerAccessMeasured: a failed fetch reads as \"not measured\", never as a\n * confirmed \"missing\" — that's a claim about the prospect's site we haven't\n * earned. Item 1: none of the three \"not measured\" lines below take the raw\n * fetch error as input anymore — status codes and transport vocabulary\n * (\"503 Service Unavailable\", \"ETIMEDOUT\") are operator vocabulary, same as\n * a stage failure (see STAGE_FAILED_MESSAGE); the real error still lives in\n * the persisted JSON and CLI output, never in this client-facing page. */\nfunction buildFindabilitySection(c: ChecksResult): string {\n const accessBlock = !c.crawlerAccessMeasured\n ? `<p class=\"muted\">Crawler access: not measured — ${STAGE_FAILED_MESSAGE}.</p>`\n : c.crawlerAccess.blockedAi.length\n ? `<p><strong>Blocked AI crawlers:</strong> ${escapeHtml(\n c.crawlerAccess.blockedAi.map(describeAgent).join(\", \"),\n )}</p>`\n : `<p>Every AI crawler we checked can reach the site.</p>`;\n const classical =\n c.crawlerAccessMeasured && c.crawlerAccess.blockedClassical.length\n ? `<p><strong>Blocked search crawlers:</strong> ${escapeHtml(\n c.crawlerAccess.blockedClassical.join(\", \"),\n )}</p>`\n : \"\";\n const sitemapLine = c.sitemapMeasured\n ? `<li>sitemap.xml: ${c.sitemapPresent ? \"present\" : \"missing\"}</li>`\n : `<li>sitemap.xml: not measured — ${STAGE_FAILED_MESSAGE}.</li>`;\n const llmsLine = c.llmsTxtMeasured\n ? `<li>llms.txt: ${c.llmsTxtPresent ? \"present\" : \"missing\"}</li>`\n : `<li>llms.txt: not measured — ${STAGE_FAILED_MESSAGE}.</li>`;\n return `${accessBlock}${classical}\n <ul>\n ${sitemapLine}\n ${llmsLine}\n <li>Pages missing a meta description: ${c.meta.missingDescription} of ${c.meta.pageCount}</li>\n <li>Pages missing a canonical URL: ${c.meta.missingCanonical} of ${c.meta.pageCount}</li>\n <li>Pages missing share images/titles: ${c.meta.missingSocial} of ${c.meta.pageCount}</li>\n <li>Security headers missing: ${\n c.securityHeaders.missing.length ? escapeHtml(c.securityHeaders.missing.join(\", \")) : \"none\"\n }</li>\n ${\n c.meta.pagesWithoutExtract > 0\n ? `<li><strong>${c.meta.pagesWithoutExtract}</strong> additional page${\n c.meta.pagesWithoutExtract === 1 ? \"\" : \"s\"\n } produced no readable content at all and ${\n c.meta.pagesWithoutExtract === 1 ? \"is\" : \"are\"\n } not counted in the ${c.meta.pageCount} pages above.</li>`\n : \"\"\n }\n </ul>`;\n}\n\n/** Lighthouse runs as its own independent pipeline stage (see pipeline.ts) —\n * it does not depend on `checks` succeeding. Rendering it off `result.checks`\n * would throw away real, measured performance/SEO/accessibility numbers\n * whenever an unrelated stage failed, so this reads `result.lighthouse`\n * directly and is rendered unconditionally, not nested inside the checks\n * degrade path. */\nfunction buildLighthouseBlock(lh: StageResult<LighthouseScores>): string {\n return lh.ok\n ? `<p class=\"muted\">Lighthouse — performance ${lh.data.performance ?? \"not measured\"},\n SEO ${lh.data.seo ?? \"not measured\"}, accessibility ${lh.data.accessibility ?? \"not measured\"}.</p>`\n : `<p class=\"muted\">Lighthouse not measured — ${STAGE_FAILED_MESSAGE}.</p>`;\n}\n\nfunction buildReadabilitySection(c: ChecksResult): string {\n const jsLine =\n c.jsDependence.avgMissing === null\n ? `<p class=\"muted\">JavaScript-dependence: not measured — no page produced a comparable raw/rendered pair.</p>`\n : `<p><strong>${Math.round(c.jsDependence.avgMissing * 100)}%</strong> of the words a visitor reads only appear after JavaScript runs.\n Most AI crawlers do not run JavaScript, so that share of your site is invisible to them.</p>`;\n return `${jsLine}\n <ul>\n <li>Structured data found: ${c.schema.typesFound.length ? escapeHtml(c.schema.typesFound.join(\", \")) : \"none\"}</li>\n <li>Expected structured data missing: ${\n c.schema.missingExpected.length ? escapeHtml(c.schema.missingExpected.join(\", \")) : \"none\"\n }</li>\n <li>Pages without a top-level heading: ${c.headings.pagesWithoutH1} of ${c.meta.pageCount}</li>\n ${\n c.meta.pagesWithoutExtract > 0\n ? `<li><strong>${c.meta.pagesWithoutExtract}</strong> page${\n c.meta.pagesWithoutExtract === 1 ? \"\" : \"s\"\n } produced no extract at all — excluded from every figure above, not counted as readable or unreadable.</li>`\n : \"\"\n }\n </ul>`;\n}\n\n/** The report's whole premise is claims the recipient can go check — so the\n * one quoted passage of evidence links to the page it came from (via\n * safeUrl; a null/non-http page renders as plain escaped text, no dead\n * \"#\" anchor). `answered`/`impact`/`effort` are escaped even though a zod\n * enum constrains them upstream — the renderer should not depend on\n * another file staying in sync to stay safe. */\nfunction buildAnswersSection(a: AnalyzeResult): string {\n const rows = a.buyerQuestions\n .map((q) => {\n const answered = escapeHtml(q.answered);\n const evidenceText = q.evidence ? escapeHtml(q.evidence) : null;\n const evidenceHref = q.evidence && q.page ? safeUrl(q.page) : null;\n const evidenceCell =\n evidenceText === null\n ? '<span class=\"muted\">no passage on the site</span>'\n : evidenceHref && evidenceHref !== \"#\"\n ? `<a href=\"${escapeHtml(evidenceHref)}\" target=\"_blank\" rel=\"noopener noreferrer\">${evidenceText}</a>`\n : evidenceText;\n return `<tr>\n <td>${escapeHtml(q.question)}</td>\n <td><span class=\"tag ${answered}\">${answered}</span></td>\n <td>${evidenceCell}</td>\n </tr>`;\n })\n .join(\"\");\n return `<p>${escapeHtml(a.narrative.answers)}</p>\n <table><tr><th>What buyers ask</th><th>Answered</th><th>Evidence</th></tr>${rows}</table>`;\n}\n\nexport function renderProspectReport(result: ProspectAuditResult): string {\n const host = hostnameOf(result.url);\n // `businessName` is the searchable NAME (possibly \"\"), never the\n // description. An empty/missing name is itself a finding — fall back to\n // the hostname rather than printing nothing, or worse, printing \"\" as if\n // it were a verified fact about the business.\n const name = result.businessName && result.businessName.trim() ? result.businessName : host;\n const date = formatIsoDate(result.generatedAt);\n\n const findabilitySection = stageBody(result.checks, buildFindabilitySection);\n const lighthouseBlock = buildLighthouseBlock(result.lighthouse);\n const readabilitySection = stageBody(result.checks, buildReadabilitySection);\n\n // A real business name was resolved and handed to the engines only when\n // `result.businessName` is non-empty — the same value the pipeline\n // actually queried with (see pipeline.ts). Used to gate the probes\n // section's \"even with the name handed to them\" phrasing (Fix 6).\n // Whether the engines were actually queried with the NAME, which is not the\n // same as whether a name existed. resolveBusinessName falls back to the bare\n // domain for anything it cannot use, and this line used to read the\n // un-resolved value — so a report could claim the engines were given the name\n // \"even with the name handed to them\" while the probes had in fact searched\n // for \"stlouisroofing.com\". Resolve it the same way probes.ts does.\n const businessNameUsed = Boolean(\n result.businessName &&\n result.businessName.trim() &&\n resolveBusinessName(result.businessName, result.url) === result.businessName.trim(),\n );\n\n const fixes = result.analyze.ok\n ? (() => {\n const sorted = [...result.analyze.data.fixes].sort(\n (x, y) => IMPACT_ORDER[x.impact] - IMPACT_ORDER[y.impact],\n );\n if (sorted.length === 0) {\n // A genuinely well-optimized site: a bare empty list reads as a\n // rendering bug, not a compliment. Say so directly, and keep this\n // section as a pitch — it's the one the salesperson is sending\n // the report to make.\n return `<p>Nothing here reads as urgent — the essentials are already in place.\n Reddoor can still help push this further: tightening structured data, monitoring\n how the answer engines describe you, and keeping pace as they change.</p>`;\n }\n return `<ol>${sorted\n .map((f) => {\n const impact = escapeHtml(f.impact);\n const effort = escapeHtml(f.effort);\n return `<li><strong>${escapeHtml(f.title)}</strong> — ${escapeHtml(f.why)}\n <span class=\"muted\">(${impact} impact, ${effort} effort)</span></li>`;\n })\n .join(\"\")}</ol>`;\n })()\n : skippableStageNote(\n result.analyze.error,\n ANALYZE_SKIPPED,\n \"Skipped — the checks stage failed, so there was nothing to build fixes from.\",\n );\n\n const probesSectionFinal = result.probes.ok\n ? buildProbesSection(result.probes.data, businessNameUsed)\n : skippableStageNote(\n result.probes.error,\n PROBES_SKIPPED,\n \"This audit did not run the AI-visibility probes.\",\n );\n\n const answersSectionFinal = result.analyze.ok\n ? buildAnswersSection(result.analyze.data)\n : skippableStageNote(\n result.analyze.error,\n ANALYZE_SKIPPED,\n \"Skipped — the checks stage failed, so there were no buyer questions to check.\",\n );\n\n const description = result.analyze.ok ? result.analyze.data.business : null;\n const narrative = result.analyze.ok\n ? `<p class=\"lede\">${escapeHtml(description ?? \"\")}</p>\n <p class=\"lede\">${escapeHtml(result.analyze.data.narrative.findability)}\n ${escapeHtml(result.analyze.data.narrative.readability)}</p>`\n : \"\";\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<meta name=\"robots\" content=\"noindex\" />\n<title>Can AI find ${escapeHtml(name)}? — Reddoor audit</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n<link href=\"https://fonts.googleapis.com/css2?family=Besley:wght@400;600&display=swap\" rel=\"stylesheet\" />\n<style>${STYLES}</style>\n</head>\n<body>\n<div class=\"wrap\">\n <h1>Can AI and Google actually find ${escapeHtml(name)}?</h1>\n <p class=\"lede\"><a href=\"${escapeHtml(safeUrl(result.url))}\" target=\"_blank\" rel=\"noopener noreferrer\">${escapeHtml(result.url)}</a> · audited ${escapeHtml(date)} by Reddoor Creative</p>\n ${narrative}\n\n <p class=\"muted\">Each score below runs 0–100 — read it as a score, not a percentage.</p>\n <div class=\"scores\">\n ${scoreCard(\"Findability\", result.scores.findability, \"How easily AI and search crawlers can find and reach your site — crawl rules, sitemap, key metadata\")}\n ${scoreCard(\"Readability\", result.scores.readability, \"How much of your site's content those crawlers can actually read once they're in — most don't run JavaScript\")}\n ${scoreCard(\"Answers\", result.scores.answers, \"How many buyer questions your site's own content answers — separate from what the AI engines say back\")}\n ${scoreCard(\"AI Visibility\", result.scores.aiVisibility, \"Based on buyer questions — not questions that name the business directly\")}\n </div>\n\n <h2>What the AI engines said about you</h2>\n ${probesSectionFinal}\n\n <h2>What the crawlers can reach</h2>\n ${findabilitySection}\n ${lighthouseBlock}\n\n <h2>What the crawlers can read</h2>\n ${readabilitySection}\n\n <h2>The questions your buyers ask</h2>\n ${answersSectionFinal}\n\n <h2>What to fix first</h2>\n ${fixes}\n\n <div class=\"cta\">\n <h2>Want this fixed?</h2>\n <p>Reddoor Creative rebuilds sites so answer engines can read, quote and recommend them.\n Reply to the email this link came from, or start at\n <a href=\"https://reddoorla.com/\">reddoorla.com</a>.</p>\n </div>\n</div>\n</body>\n</html>`;\n}\n"],"mappings":"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","names":[]}
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  defaultReportSubject
3
3
  } from "./chunk-GECESMVS.js";
4
+ import {
5
+ applyReportTypeHeadline
6
+ } from "./chunk-5UINJNAJ.js";
4
7
  import {
5
8
  announcementSiteExtras,
6
9
  loadBundledImages,
7
10
  renderReportHtml,
8
11
  resolveCopy
9
- } from "./chunk-MT2JINSS.js";
10
- import {
11
- applyReportTypeHeadline
12
- } from "./chunk-5UINJNAJ.js";
12
+ } from "./chunk-KJGKZLCT.js";
13
13
  import {
14
14
  siteSlug
15
15
  } from "./chunk-7NYN2P6I.js";
@@ -143,4 +143,4 @@ export {
143
143
  renderReportFromRow,
144
144
  windowDays
145
145
  };
146
- //# sourceMappingURL=chunk-ZGBEIWSC.js.map
146
+ //# sourceMappingURL=chunk-XP5WM2LV.js.map
@@ -1,3 +1,9 @@
1
+ import {
2
+ convertToPnpm
3
+ } from "./chunk-XIDUYLSR.js";
4
+ import {
5
+ svelteCodemods
6
+ } from "./chunk-IYSEC543.js";
1
7
  import {
2
8
  healthEndpoint
3
9
  } from "./chunk-BQRFQSGY.js";
@@ -7,12 +13,6 @@ import {
7
13
  import {
8
14
  onboard
9
15
  } from "./chunk-7RJ2HMMJ.js";
10
- import {
11
- convertToPnpm
12
- } from "./chunk-XIDUYLSR.js";
13
- import {
14
- svelteCodemods
15
- } from "./chunk-IYSEC543.js";
16
16
  import {
17
17
  syncConfigs
18
18
  } from "./chunk-7UAYMAKR.js";
@@ -21,7 +21,7 @@ import {
21
21
  } from "./chunk-3G25KIWW.js";
22
22
  import {
23
23
  runAudits
24
- } from "./chunk-3JQ2FO3D.js";
24
+ } from "./chunk-B23FFWU4.js";
25
25
  import {
26
26
  siteLabel
27
27
  } from "./chunk-XXTZBPUY.js";
@@ -143,4 +143,4 @@ export {
143
143
  DEFAULT_INIT_STEPS,
144
144
  init
145
145
  };
146
- //# sourceMappingURL=chunk-TD2NNXLZ.js.map
146
+ //# sourceMappingURL=chunk-XSOINOPY.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  isHttpUrl
3
- } from "./chunk-EVBCK2KC.js";
3
+ } from "./chunk-RARREDQE.js";
4
4
 
5
5
  // src/inventory/local.ts
6
6
  import { basename } from "path";
@@ -67,4 +67,4 @@ export {
67
67
  localPath,
68
68
  fromJsonFile
69
69
  };
70
- //# sourceMappingURL=chunk-6TWWY7ZU.js.map
70
+ //# sourceMappingURL=chunk-Z3JXLULD.js.map
package/dist/cli/bin.js CHANGED
@@ -111,7 +111,7 @@ cli.command("sync-configs [site]", "Sync canonical configs into a site.").option
111
111
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
112
112
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
113
113
  async (site, opts) => runOrExit(
114
- async () => (await import("../sync-configs-SPTLFJCC.js")).runSyncConfigsCommand(site, opts),
114
+ async () => (await import("../sync-configs-34JIA6KZ.js")).runSyncConfigsCommand(site, opts),
115
115
  opts
116
116
  )
117
117
  );
@@ -120,7 +120,7 @@ cli.command("bump-deps [site]", "Bump dependencies.").option("--group <group>",
120
120
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
121
121
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
122
122
  async (site, opts) => runOrExit(
123
- async () => (await import("../bump-deps-CGVELRLK.js")).runBumpDepsCommand(site, opts),
123
+ async () => (await import("../bump-deps-KGH5UYSF.js")).runBumpDepsCommand(site, opts),
124
124
  opts
125
125
  )
126
126
  );
@@ -129,7 +129,7 @@ cli.command(
129
129
  "Bootstrap a repo to keep itself updated (Renovate + protection; platform auto-merge off; ci.yml is the starter's, not managed here)."
130
130
  ).option("--dry", "List what would be enabled without writing or opening PRs").option("--fleet <inventory>", 'Inventory file (.json or .mjs/.js), or "airtable"').option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
131
131
  async (site, opts) => runOrExit(
132
- async () => (await import("../self-updating-UCIWESAX.js")).runSelfUpdatingCommand(site, opts),
132
+ async () => (await import("../self-updating-NTCCWJYZ.js")).runSelfUpdatingCommand(site, opts),
133
133
  opts
134
134
  )
135
135
  );
@@ -138,7 +138,7 @@ cli.command(
138
138
  "Add the Prismic model delivery workflow to a site via PR (one PR per repo; never pushes to main)."
139
139
  ).option("--dry", "List the sites that would be offered the workflow, without opening any PR").option("--fleet <inventory>", 'Inventory file (.json or .mjs/.js), or "airtable"').option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
140
140
  async (site, opts) => runOrExit(
141
- async () => (await import("../prismic-ci-G2AK53VL.js")).runPrismicCiCommand(site, opts),
141
+ async () => (await import("../prismic-ci-MY2U6NN7.js")).runPrismicCiCommand(site, opts),
142
142
  opts
143
143
  )
144
144
  );
@@ -153,7 +153,7 @@ cli.command(
153
153
  "Write the report to this file (the CI workflow posts it as a PR comment)"
154
154
  ).action(
155
155
  async (site, opts) => runOrExit(
156
- async () => (await import("../prismic-models-O6Z3OK3A.js")).runPrismicModelsCommand(site, opts),
156
+ async () => (await import("../prismic-models-IJWZW2EO.js")).runPrismicModelsCommand(site, opts),
157
157
  opts
158
158
  )
159
159
  );
@@ -162,7 +162,7 @@ cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to
162
162
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
163
163
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
164
164
  async (upgrade, site, opts) => runOrExit(
165
- async () => (await import("../upgrade-W6ZR3FJH.js")).runUpgradeCommand(upgrade, site, opts),
165
+ async () => (await import("../upgrade-IR2OO3KP.js")).runUpgradeCommand(upgrade, site, opts),
166
166
  opts
167
167
  )
168
168
  );
@@ -174,7 +174,7 @@ cli.command(
174
174
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
175
175
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
176
176
  async (site, opts) => runOrExit(
177
- async () => (await import("../convert-to-pnpm-QFJOEP5V.js")).runConvertToPnpmCommand(site, opts),
177
+ async () => (await import("../convert-to-pnpm-NWB5FCQ2.js")).runConvertToPnpmCommand(site, opts),
178
178
  opts
179
179
  )
180
180
  );
@@ -183,7 +183,7 @@ cli.command("svelte-codemods [site]", "Apply Svelte 5 gotcha codemods to an alre
183
183
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
184
184
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
185
185
  async (site, opts) => runOrExit(
186
- async () => (await import("../svelte-codemods-GOKXTBHW.js")).runSvelteCodemodsCommand(site, opts),
186
+ async () => (await import("../svelte-codemods-QAPDZC6F.js")).runSvelteCodemodsCommand(site, opts),
187
187
  opts
188
188
  )
189
189
  );
@@ -195,7 +195,7 @@ cli.command(
195
195
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
196
196
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
197
197
  async (site, opts) => runOrExit(
198
- async () => (await import("../health-endpoint-RFXL2F4A.js")).runHealthEndpointCommand(site, opts),
198
+ async () => (await import("../health-endpoint-UVX6R5N5.js")).runHealthEndpointCommand(site, opts),
199
199
  opts
200
200
  )
201
201
  );
@@ -207,7 +207,7 @@ cli.command(
207
207
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
208
208
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
209
209
  async (site, opts) => runOrExit(
210
- async () => (await import("../smoke-suite-U3UKD3NS.js")).runSmokeSuiteCommand(site, opts),
210
+ async () => (await import("../smoke-suite-4JFZZYVW.js")).runSmokeSuiteCommand(site, opts),
211
211
  opts
212
212
  )
213
213
  );
@@ -219,7 +219,7 @@ cli.command(
219
219
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
220
220
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
221
221
  async (site, opts) => runOrExit(
222
- async () => (await import("../onboard-O4IAHJMO.js")).runOnboardCommand(site, opts),
222
+ async () => (await import("../onboard-BKCYBAVW.js")).runOnboardCommand(site, opts),
223
223
  opts
224
224
  )
225
225
  );
@@ -230,20 +230,20 @@ cli.command(
230
230
  "--fleet <inventory>",
231
231
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
232
232
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
233
- async (site, opts) => runOrExit(async () => (await import("../init-XRAQZYID.js")).runInitCommand(site, opts), opts)
233
+ async (site, opts) => runOrExit(async () => (await import("../init-32AWPS7A.js")).runInitCommand(site, opts), opts)
234
234
  );
235
235
  cli.command(
236
236
  "launch <site>",
237
237
  "Bootstrap + first-audit a site, then draft its launch email for approval."
238
238
  ).action(
239
239
  async (site, opts) => runOrExit(
240
- async () => (await import("../launch-IX4Y5JJC.js")).runLaunchCommand(site, opts),
240
+ async () => (await import("../launch-52WUE4TZ.js")).runLaunchCommand(site, opts),
241
241
  opts
242
242
  )
243
243
  );
244
244
  cli.command("header-image [site]", "Generate a site's report header image from its live homepage.").option("--all", "Every live site with no Header image yet (backfill)").option("--force", "With --all, regenerate sites that already have one").option("--write-airtable", "Upload to the Websites row instead of writing a local file").option("--out-dir <path>", "Directory for local output (default: reports/)").option("--settle-ms <ms>", "Override the post-load settle delay for slow/animated homepages").action(
245
245
  async (site, opts) => runOrExit(
246
- async () => (await import("../header-image-6IC53W27.js")).runHeaderImageCommand(site, opts),
246
+ async () => (await import("../header-image-7A7XLVEU.js")).runHeaderImageCommand(site, opts),
247
247
  opts
248
248
  )
249
249
  );
@@ -252,7 +252,7 @@ cli.command(
252
252
  "Draft the monthly-report announcement email for maintenance sites (all, or one) for approval."
253
253
  ).action(
254
254
  async (site, opts) => runOrExit(
255
- async () => (await import("../announce-I2Z4JN3I.js")).runAnnounceCommand(site, opts),
255
+ async () => (await import("../announce-GKD5YJNB.js")).runAnnounceCommand(site, opts),
256
256
  opts
257
257
  )
258
258
  );
@@ -261,7 +261,7 @@ cli.command(
261
261
  "Show who a form submission would email, and optionally flip the pre-launch guard (read-back confirmed)."
262
262
  ).option("--set <on|off>", "Flip the guard: on = route to operator, off = restore.").option("--restore <status>", "Status to restore with --set off. Required; never inferred.").action(
263
263
  async (site, opts) => runOrExit(
264
- async () => (await import("../forms-notify-target-2EAWCWBW.js")).runFormsNotifyTargetCommand(
264
+ async () => (await import("../forms-notify-target-SGDGXXK5.js")).runFormsNotifyTargetCommand(
265
265
  site,
266
266
  opts
267
267
  ),
@@ -273,7 +273,7 @@ cli.command(
273
273
  "Operator self-tests. kind=email: preview a report email for a site (or --all) to yourself."
274
274
  ).option("--type <type>", "Report type: announcement (default) | maintenance | testing | launch").option("--to <addr>", "Recipient(s), comma-separated. Default: OPERATOR_EMAIL.").option("--all", "Send a preview for every maintenance site (to --to/operator).").option("--dry-run", "Render only; write reports/<slug>/selftest-<type>.html; do not send.").action(
275
275
  async (kind, site, opts) => runOrExit(
276
- async () => (await import("../selftest-VGHWPN72.js")).runSelftestCommand(kind, site, opts),
276
+ async () => (await import("../selftest-DIZ5JD2X.js")).runSelftestCommand(kind, site, opts),
277
277
  opts
278
278
  )
279
279
  );
@@ -285,13 +285,13 @@ cli.command(
285
285
  "Fleet mode: announcement\u2192maintenance sites; maintenance/testing\u2192all schedulable sites."
286
286
  ).action(
287
287
  async (site, opts) => runOrExit(
288
- async () => (await import("../preflight-TNORKGCX.js")).runPreflightCommand(site, opts),
288
+ async () => (await import("../preflight-IMTYDDD4.js")).runPreflightCommand(site, opts),
289
289
  opts
290
290
  )
291
291
  );
292
292
  cli.command("ensure-site <slug>", "Create/verify the Airtable Websites row for a new site.").option("--name <display name>", "Human Name for the row (client-facing copy uses it verbatim).").option("--url <url>", "Deployed URL (e.g. the Netlify site URL).").option("--contact <email>", "point of contact \u2014 the client address reports resolve to.").option("--git-repo <owner/repo>", "GitHub identity. Default on create: reddoorla/<slug>.").action(
293
293
  async (slug, opts) => runOrExit(
294
- async () => (await import("../ensure-site-4DCEBAPM.js")).runEnsureSiteCommand(slug, opts),
294
+ async () => (await import("../ensure-site-FU6527OU.js")).runEnsureSiteCommand(slug, opts),
295
295
  opts
296
296
  )
297
297
  );
@@ -309,7 +309,7 @@ cli.command(
309
309
  "Email the internal sheet (scores, what wasn't measured, top fixes, the link) to PROSPECT_AUDIT_RECIPIENTS."
310
310
  ).action(
311
311
  async (url, opts) => runOrExit(
312
- async () => (await import("../prospect-audit-KSXUNQ5J.js")).runProspectAuditCommand(url, opts),
312
+ async () => (await import("../prospect-audit-WJQI7V5V.js")).runProspectAuditCommand(url, opts),
313
313
  opts
314
314
  )
315
315
  );
@@ -330,7 +330,7 @@ cli.command("report [site]", "Draft or send maintenance/testing reports.").optio
330
330
  "Regenerate one UNSENT report's stored HTML from its current row, so the console preview shows edited commentary. Refuses a sent report."
331
331
  ).action(
332
332
  async (site, opts) => runOrExit(
333
- async () => (await import("../report-R3E3Y7HR.js")).runReportCommand(site, opts),
333
+ async () => (await import("../report-2LM2SX67.js")).runReportCommand(site, opts),
334
334
  opts
335
335
  )
336
336
  );
@@ -339,7 +339,7 @@ cli.command(
339
339
  "Sweep the fleet for GitHub signals (Renovate-failing/CI/last-commit) and write Airtable."
340
340
  ).option("--fleet", "Run across every site in the Airtable inventory.").option("--write-airtable", "Write each site's signals back to its Websites row.").action(
341
341
  async (opts) => runOrExit(
342
- async () => (await import("../github-signals-E2WPYTBK.js")).runGitHubSignalsCommand({
342
+ async () => (await import("../github-signals-DB25UWY4.js")).runGitHubSignalsCommand({
343
343
  fleet: opts.fleet,
344
344
  writeAirtable: opts.writeAirtable
345
345
  }),
@@ -359,9 +359,12 @@ cli.command(
359
359
  );
360
360
  cli.command(
361
361
  "db <action>",
362
- "Operate the libSQL store (migrate | replay-deadletters | import-airtable | parity | sync | backfill-header-images | dump | verify-dump)."
363
- ).option("--file <path>", "verify-dump: the dump file to load into a scratch engine").action(
364
- async (action, opts) => runOrExit(async () => (await import("../db-UXNIZ2RG.js")).runDbCommand(action, opts), opts)
362
+ "Operate the libSQL store (migrate | replay-deadletters | import-airtable | parity | sync | backfill-header-images | backfill-digest-state | dump | verify-dump | restore)."
363
+ ).option("--file <path>", "verify-dump / restore: the dump file to load").option(
364
+ "--url <url>",
365
+ "restore: the TARGET database to load into. Required, never defaulted \u2014 a restore that could fall back to the ambient TURSO_DATABASE_URL is one keystroke from overwriting production."
366
+ ).action(
367
+ async (action, opts) => runOrExit(async () => (await import("../db-DXUUAMXT.js")).runDbCommand(action, opts), opts)
365
368
  );
366
369
  cli.command(
367
370
  "submissions <action>",
@@ -371,7 +374,7 @@ cli.command(
371
374
  "Write the re-buckets: status \u2192 spam_auto, spam_score/spam_reason replaced with the new verdict + 'retro-rescore' marker."
372
375
  ).action(
373
376
  async (action, opts) => runOrExit(
374
- async () => (await import("../submissions-IQEJX6FK.js")).runSubmissionsCommand(action, opts),
377
+ async () => (await import("../submissions-TEHPBSZJ.js")).runSubmissionsCommand(action, opts),
375
378
  opts
376
379
  )
377
380
  );
@@ -383,7 +386,7 @@ cli.command(
383
386
  async () => (await import("../renovate-dispatch-6GQWTRKX.js")).runRenovateDispatchCommand({
384
387
  fleet: opts.fleet,
385
388
  // #539 Phase 5: mirror the auto-fix counter into site_health.
386
- siteMirror: await (await import("../site-mirror-NIREX5ME.js")).makeSiteMirror()
389
+ siteMirror: await (await import("../site-mirror-Q763PLGG.js")).makeSiteMirror()
387
390
  }),
388
391
  opts
389
392
  )
@@ -408,7 +411,7 @@ cli.command(
408
411
  "freeze: JSON declaring slots the template has no token for (video poster, a panel rebuilt from a baked image). Keys must start with 'x.'"
409
412
  ).action(
410
413
  async (action, dir, opts) => runOrExit(
411
- async () => (await import("../blux-ZGDSHI3U.js")).runBluxCommand(action, dir, opts),
414
+ async () => (await import("../blux-EATHOHKK.js")).runBluxCommand(action, dir, opts),
412
415
  opts
413
416
  )
414
417
  );