@reddoorla/maintenance 0.86.0 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/dist/airtable-ZBUPQJSC.js +9 -0
  2. package/dist/{announce-MXTUQ6C7.js → announce-I2Z4JN3I.js} +49 -28
  3. package/dist/announce-I2Z4JN3I.js.map +1 -0
  4. package/dist/attachments-NLZIJ5NU.js +9 -0
  5. package/dist/{bump-deps-EC74CPLS.js → bump-deps-CGVELRLK.js} +2 -2
  6. package/dist/{chunk-2ZPT7GLV.js → chunk-3JQ2FO3D.js} +3 -3
  7. package/dist/chunk-3JQ2FO3D.js.map +1 -0
  8. package/dist/{chunk-KTH5ZODA.js → chunk-7BZUOZPU.js} +2 -2
  9. package/dist/{chunk-LDWZSLSU.js → chunk-7NYN2P6I.js} +7 -4
  10. package/dist/{chunk-LDWZSLSU.js.map → chunk-7NYN2P6I.js.map} +1 -1
  11. package/dist/{chunk-3VQWSE3F.js → chunk-7UAYMAKR.js} +4 -4
  12. package/dist/{chunk-BYK66EWO.js → chunk-BBUSGEES.js} +52 -46
  13. package/dist/chunk-BBUSGEES.js.map +1 -0
  14. package/dist/{chunk-SLUQ7R6I.js → chunk-C6CLRKPV.js} +5 -5
  15. package/dist/{chunk-P2FE5AI6.js → chunk-CROKUHJB.js} +2 -2
  16. package/dist/{chunk-W7Z3BGCI.js → chunk-DEWJMMG3.js} +2 -2
  17. package/dist/{chunk-ACBRLLFT.js → chunk-F3JWOPTG.js} +7 -3
  18. package/dist/chunk-F3JWOPTG.js.map +1 -0
  19. package/dist/{chunk-TERE5SDF.js → chunk-HEWSRXYV.js} +4 -4
  20. package/dist/chunk-HHMMUR4P.js +335 -0
  21. package/dist/chunk-HHMMUR4P.js.map +1 -0
  22. package/dist/{chunk-X3B723J4.js → chunk-HSI6Q7MI.js} +38 -9
  23. package/dist/chunk-HSI6Q7MI.js.map +1 -0
  24. package/dist/{chunk-Z7AXYKQB.js → chunk-IY6256GK.js} +1 -1
  25. package/dist/{chunk-CQR4LIM7.js → chunk-MT2JINSS.js} +5 -1
  26. package/dist/{chunk-CQR4LIM7.js.map → chunk-MT2JINSS.js.map} +1 -1
  27. package/dist/{chunk-F5KM4LEQ.js → chunk-NCMZMLY7.js} +3 -3
  28. package/dist/{chunk-2NDRHXMR.js → chunk-NDWYJLRF.js} +17 -140
  29. package/dist/chunk-NDWYJLRF.js.map +1 -0
  30. package/dist/{chunk-PPE5KZJK.js → chunk-NXNPDCF3.js} +3 -3
  31. package/dist/{chunk-ITIOFN7A.js → chunk-P6AORG32.js} +29 -10
  32. package/dist/chunk-P6AORG32.js.map +1 -0
  33. package/dist/{chunk-N45M3FDK.js → chunk-SDDPUAWH.js} +4 -4
  34. package/dist/{chunk-ZKXSX4LO.js → chunk-TD2NNXLZ.js} +9 -9
  35. package/dist/{chunk-ILUNZHYV.js → chunk-YFAXOD7Z.js} +10 -6
  36. package/dist/chunk-YFAXOD7Z.js.map +1 -0
  37. package/dist/{chunk-2EVEFOZY.js → chunk-ZBDZTJUO.js} +2 -2
  38. package/dist/chunk-ZGBEIWSC.js +146 -0
  39. package/dist/chunk-ZGBEIWSC.js.map +1 -0
  40. package/dist/cli/bin.js +32 -27
  41. package/dist/cli/bin.js.map +1 -1
  42. package/dist/cli/commands/audit.js +15 -11
  43. package/dist/cli/commands/audit.js.map +1 -1
  44. package/dist/{convert-to-pnpm-6L6P74WR.js → convert-to-pnpm-QFJOEP5V.js} +2 -2
  45. package/dist/{db-B35OGV3G.js → db-UXNIZ2RG.js} +6 -6
  46. package/dist/{digest-BL6UJYKH.js → digest-FW6QMZZJ.js} +17 -16
  47. package/dist/{digest-BL6UJYKH.js.map → digest-FW6QMZZJ.js.map} +1 -1
  48. package/dist/{email-VTTTKKL5.js → email-KJXASVH5.js} +3 -3
  49. package/dist/{ensure-site-DFTB7MN3.js → ensure-site-4DCEBAPM.js} +19 -11
  50. package/dist/ensure-site-4DCEBAPM.js.map +1 -0
  51. package/dist/fleet-state-7TJ2GYT2.js +39 -0
  52. package/dist/{forms-notify-target-75U6OU7L.js → forms-notify-target-2EAWCWBW.js} +7 -4
  53. package/dist/forms-notify-target-2EAWCWBW.js.map +1 -0
  54. package/dist/{github-signals-IJUPGCUX.js → github-signals-E2WPYTBK.js} +9 -9
  55. package/dist/{header-image-AWLLQA4L.js → header-image-6IC53W27.js} +7 -7
  56. package/dist/{header-images-335L2AKB.js → header-images-SQBCE5OD.js} +21 -1
  57. package/dist/header-images-SQBCE5OD.js.map +1 -0
  58. package/dist/{health-endpoint-WJC2UMYS.js → health-endpoint-RFXL2F4A.js} +2 -2
  59. package/dist/health-mirror-L5JBEJVA.js +41 -0
  60. package/dist/health-mirror-L5JBEJVA.js.map +1 -0
  61. package/dist/{import-airtable-HE43FB4H.js → import-airtable-OHVY344G.js} +7 -5
  62. package/dist/index.d.ts +149 -0
  63. package/dist/index.js +147 -50
  64. package/dist/index.js.map +1 -1
  65. package/dist/{init-NJEOEO7I.js → init-XRAQZYID.js} +9 -9
  66. package/dist/{launch-XNAX427U.js → launch-IX4Y5JJC.js} +55 -26
  67. package/dist/launch-IX4Y5JJC.js.map +1 -0
  68. package/dist/{lighthouse-airtable-LBVIYYP5.js → lighthouse-airtable-3GUWHETW.js} +3 -3
  69. package/dist/{onboard-GK4PHPNI.js → onboard-O4IAHJMO.js} +2 -2
  70. package/dist/{orchestrate-77UO4BYJ.js → orchestrate-YLWQ6KK2.js} +10 -9
  71. package/dist/{parity-MWS24A3H.js → parity-56KA2DGR.js} +4 -4
  72. package/dist/{pipeline-S6CVHDGB.js → pipeline-3AANZFCR.js} +2 -2
  73. package/dist/{preflight-6WBNU2GZ.js → preflight-TNORKGCX.js} +11 -10
  74. package/dist/{preflight-6WBNU2GZ.js.map → preflight-TNORKGCX.js.map} +1 -1
  75. package/dist/{prismic-ci-3RHOZUUT.js → prismic-ci-G2AK53VL.js} +5 -5
  76. package/dist/{prismic-models-QBGUT2Q3.js → prismic-models-O6Z3OK3A.js} +13 -8
  77. package/dist/prismic-models-O6Z3OK3A.js.map +1 -0
  78. package/dist/prospect/types.d.ts +267 -0
  79. package/dist/prospect/types.js +1 -0
  80. package/dist/{prospect-audit-IUNCJ2LV.js → prospect-audit-KSXUNQ5J.js} +4 -4
  81. package/dist/recipes/sync-configs.js +2 -2
  82. package/dist/{render-AQAVQDPZ.js → render-IFOGFB5T.js} +3 -3
  83. package/dist/render-IFOGFB5T.js.map +1 -0
  84. package/dist/render-from-row-BWSOIYDS.js +17 -0
  85. package/dist/render-from-row-BWSOIYDS.js.map +1 -0
  86. package/dist/{renovate-dispatch-QJY75ODR.js → renovate-dispatch-6GQWTRKX.js} +10 -10
  87. package/dist/{renovate-dispatch-QJY75ODR.js.map → renovate-dispatch-6GQWTRKX.js.map} +1 -1
  88. package/dist/{report-4EVNDHOZ.js → report-R3E3Y7HR.js} +63 -21
  89. package/dist/report-R3E3Y7HR.js.map +1 -0
  90. package/dist/report-mirror-4MNORYQ4.js +49 -0
  91. package/dist/report-mirror-4MNORYQ4.js.map +1 -0
  92. package/dist/rerender-3FM6DNRH.js +32 -0
  93. package/dist/rerender-3FM6DNRH.js.map +1 -0
  94. package/dist/{self-updating-6DWI2Z56.js → self-updating-UCIWESAX.js} +2 -2
  95. package/dist/{selftest-RC36MTKP.js → selftest-VGHWPN72.js} +22 -20
  96. package/dist/{selftest-RC36MTKP.js.map → selftest-VGHWPN72.js.map} +1 -1
  97. package/dist/site-mirror-NIREX5ME.js +52 -0
  98. package/dist/site-mirror-NIREX5ME.js.map +1 -0
  99. package/dist/{smoke-suite-VYY2LXM2.js → smoke-suite-U3UKD3NS.js} +2 -2
  100. package/dist/{submissions-Q7DRFOCD.js → submissions-IQEJX6FK.js} +2 -2
  101. package/dist/{svelte-codemods-25J2FO22.js → svelte-codemods-GOKXTBHW.js} +2 -2
  102. package/dist/{sync-34HNZZ56.js → sync-CTUVXMCK.js} +4 -4
  103. package/dist/{sync-configs-3SE6PSCO.js → sync-configs-SPTLFJCC.js} +4 -4
  104. package/dist/{upgrade-WBRQ62FX.js → upgrade-W6ZR3FJH.js} +2 -2
  105. package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
  106. package/dist/{websites-SXWTP72S.js → websites-D6WYFAES.js} +2 -2
  107. package/dist/websites-D6WYFAES.js.map +1 -0
  108. package/dist/{write-audits-to-airtable-MBEILLPU.js → write-audits-to-airtable-CMKHGJNQ.js} +4 -4
  109. package/dist/write-audits-to-airtable-CMKHGJNQ.js.map +1 -0
  110. package/package.json +5 -1
  111. package/dist/airtable-G7WBQQMI.js +0 -9
  112. package/dist/announce-MXTUQ6C7.js.map +0 -1
  113. package/dist/chunk-2NDRHXMR.js.map +0 -1
  114. package/dist/chunk-2ZPT7GLV.js.map +0 -1
  115. package/dist/chunk-ACBRLLFT.js.map +0 -1
  116. package/dist/chunk-BYK66EWO.js.map +0 -1
  117. package/dist/chunk-ILUNZHYV.js.map +0 -1
  118. package/dist/chunk-ITIOFN7A.js.map +0 -1
  119. package/dist/chunk-X3B723J4.js.map +0 -1
  120. package/dist/ensure-site-DFTB7MN3.js.map +0 -1
  121. package/dist/forms-notify-target-75U6OU7L.js.map +0 -1
  122. package/dist/header-images-335L2AKB.js.map +0 -1
  123. package/dist/health-mirror-4B2L7T2Y.js +0 -75
  124. package/dist/health-mirror-4B2L7T2Y.js.map +0 -1
  125. package/dist/launch-XNAX427U.js.map +0 -1
  126. package/dist/prismic-models-QBGUT2Q3.js.map +0 -1
  127. package/dist/report-4EVNDHOZ.js.map +0 -1
  128. /package/dist/{airtable-G7WBQQMI.js.map → airtable-ZBUPQJSC.js.map} +0 -0
  129. /package/dist/{import-airtable-HE43FB4H.js.map → attachments-NLZIJ5NU.js.map} +0 -0
  130. /package/dist/{bump-deps-EC74CPLS.js.map → bump-deps-CGVELRLK.js.map} +0 -0
  131. /package/dist/{chunk-KTH5ZODA.js.map → chunk-7BZUOZPU.js.map} +0 -0
  132. /package/dist/{chunk-3VQWSE3F.js.map → chunk-7UAYMAKR.js.map} +0 -0
  133. /package/dist/{chunk-SLUQ7R6I.js.map → chunk-C6CLRKPV.js.map} +0 -0
  134. /package/dist/{chunk-P2FE5AI6.js.map → chunk-CROKUHJB.js.map} +0 -0
  135. /package/dist/{chunk-W7Z3BGCI.js.map → chunk-DEWJMMG3.js.map} +0 -0
  136. /package/dist/{chunk-TERE5SDF.js.map → chunk-HEWSRXYV.js.map} +0 -0
  137. /package/dist/{chunk-Z7AXYKQB.js.map → chunk-IY6256GK.js.map} +0 -0
  138. /package/dist/{chunk-F5KM4LEQ.js.map → chunk-NCMZMLY7.js.map} +0 -0
  139. /package/dist/{chunk-PPE5KZJK.js.map → chunk-NXNPDCF3.js.map} +0 -0
  140. /package/dist/{chunk-N45M3FDK.js.map → chunk-SDDPUAWH.js.map} +0 -0
  141. /package/dist/{chunk-ZKXSX4LO.js.map → chunk-TD2NNXLZ.js.map} +0 -0
  142. /package/dist/{chunk-2EVEFOZY.js.map → chunk-ZBDZTJUO.js.map} +0 -0
  143. /package/dist/{convert-to-pnpm-6L6P74WR.js.map → convert-to-pnpm-QFJOEP5V.js.map} +0 -0
  144. /package/dist/{db-B35OGV3G.js.map → db-UXNIZ2RG.js.map} +0 -0
  145. /package/dist/{email-VTTTKKL5.js.map → email-KJXASVH5.js.map} +0 -0
  146. /package/dist/{lighthouse-airtable-LBVIYYP5.js.map → fleet-state-7TJ2GYT2.js.map} +0 -0
  147. /package/dist/{github-signals-IJUPGCUX.js.map → github-signals-E2WPYTBK.js.map} +0 -0
  148. /package/dist/{header-image-AWLLQA4L.js.map → header-image-6IC53W27.js.map} +0 -0
  149. /package/dist/{health-endpoint-WJC2UMYS.js.map → health-endpoint-RFXL2F4A.js.map} +0 -0
  150. /package/dist/{orchestrate-77UO4BYJ.js.map → import-airtable-OHVY344G.js.map} +0 -0
  151. /package/dist/{init-NJEOEO7I.js.map → init-XRAQZYID.js.map} +0 -0
  152. /package/dist/{parity-MWS24A3H.js.map → lighthouse-airtable-3GUWHETW.js.map} +0 -0
  153. /package/dist/{onboard-GK4PHPNI.js.map → onboard-O4IAHJMO.js.map} +0 -0
  154. /package/dist/{pipeline-S6CVHDGB.js.map → orchestrate-YLWQ6KK2.js.map} +0 -0
  155. /package/dist/{render-AQAVQDPZ.js.map → parity-56KA2DGR.js.map} +0 -0
  156. /package/dist/{websites-SXWTP72S.js.map → pipeline-3AANZFCR.js.map} +0 -0
  157. /package/dist/{prismic-ci-3RHOZUUT.js.map → prismic-ci-G2AK53VL.js.map} +0 -0
  158. /package/dist/{write-audits-to-airtable-MBEILLPU.js.map → prospect/types.js.map} +0 -0
  159. /package/dist/{prospect-audit-IUNCJ2LV.js.map → prospect-audit-KSXUNQ5J.js.map} +0 -0
  160. /package/dist/{self-updating-6DWI2Z56.js.map → self-updating-UCIWESAX.js.map} +0 -0
  161. /package/dist/{smoke-suite-VYY2LXM2.js.map → smoke-suite-U3UKD3NS.js.map} +0 -0
  162. /package/dist/{submissions-Q7DRFOCD.js.map → submissions-IQEJX6FK.js.map} +0 -0
  163. /package/dist/{svelte-codemods-25J2FO22.js.map → svelte-codemods-GOKXTBHW.js.map} +0 -0
  164. /package/dist/{sync-34HNZZ56.js.map → sync-CTUVXMCK.js.map} +0 -0
  165. /package/dist/{sync-configs-3SE6PSCO.js.map → sync-configs-SPTLFJCC.js.map} +0 -0
  166. /package/dist/{upgrade-WBRQ62FX.js.map → upgrade-W6ZR3FJH.js.map} +0 -0
  167. /package/dist/{webflow-IRKOIT7S.js.map → webflow-624FEBFD.js.map} +0 -0
@@ -0,0 +1,267 @@
1
+ import { a as AuditResult } from '../types-OIQZ5hWN.js';
2
+
3
+ /** Every pipeline stage resolves to this. A failed stage degrades its report
4
+ * section to "not measured" — it never kills the run (spec: error handling). */
5
+ type StageResult<T> = {
6
+ ok: true;
7
+ data: T;
8
+ } | {
9
+ ok: false;
10
+ error: string;
11
+ };
12
+ type RobotsAgentAccess = {
13
+ agent: string;
14
+ /** May this agent fetch "/" per robots.txt? (No robots.txt → allowed.) */
15
+ allowed: boolean;
16
+ /** The deciding rule, e.g. "User-agent: GPTBot → Disallow: /". Null = no rule matched. */
17
+ matchedRule: string | null;
18
+ };
19
+ type PageExtract = {
20
+ title: string | null;
21
+ metaDescription: string | null;
22
+ canonical: string | null;
23
+ /** property/name → content for og:* and twitter:* metas. */
24
+ social: Record<string, string>;
25
+ /** `level` is 1-6 (H1-H6). */
26
+ headings: {
27
+ level: number;
28
+ text: string;
29
+ }[];
30
+ /** Raw text of each <script type="application/ld+json"> block. */
31
+ jsonLd: string[];
32
+ images: {
33
+ total: number;
34
+ withAlt: number;
35
+ };
36
+ hasViewportMeta: boolean;
37
+ /** Visible text, whitespace-collapsed. */
38
+ text: string;
39
+ };
40
+ type PageCapture = {
41
+ url: string;
42
+ /** HTTP status of the raw fetch. Null = the fetch itself failed. */
43
+ status: number | null;
44
+ /** Extract of the raw HTTP HTML (what non-JS crawlers see). */
45
+ raw: PageExtract | null;
46
+ /** Extract of the Playwright-rendered DOM (what a browser sees). */
47
+ rendered: PageExtract | null;
48
+ error: string | null;
49
+ };
50
+ type CrawlResult = {
51
+ /** Normalized origin, e.g. "https://example.com". */
52
+ origin: string;
53
+ robotsTxt: string | null;
54
+ /** One entry per agent in crawl.ts's ALL_AGENTS (6 AI + 2 classical). */
55
+ agentAccess: RobotsAgentAccess[];
56
+ sitemap: {
57
+ present: boolean;
58
+ urlCount: number;
59
+ };
60
+ llmsTxt: {
61
+ present: boolean;
62
+ firstLine: string | null;
63
+ };
64
+ /** Per sidecar, the error that stopped us fetching it, or null. A fetch that
65
+ * FAILED must never be reported as "the site has no robots.txt" — that would
66
+ * claim the crawlers are unrestricted when we simply did not look. */
67
+ sidecarErrors: {
68
+ robots: string | null;
69
+ llms: string | null;
70
+ sitemap: string | null;
71
+ };
72
+ /** Lower-cased homepage response headers (security-header check input). */
73
+ homeHeaders: Record<string, string>;
74
+ pages: PageCapture[];
75
+ };
76
+ type ChecksResult = {
77
+ /** False when the robots.txt fetch itself failed, so "no AI crawler is
78
+ * blocked" would be our silence rather than the site's answer. The report
79
+ * must say "not measured" for crawler access when this is false. */
80
+ crawlerAccessMeasured: boolean;
81
+ crawlerAccess: {
82
+ blockedAi: string[];
83
+ allowedAi: string[];
84
+ blockedClassical: string[];
85
+ };
86
+ jsDependence: {
87
+ /** 0..1 — fraction of rendered words absent from the raw HTML, weighted by
88
+ * page size (total missing words / total rendered words across every
89
+ * comparable page) — or null when no page produced a comparable pair.
90
+ * Never 0 for "nothing measured": a page whose raw fetch was blocked, or
91
+ * whose rendered text tokenizes to nothing, drops out of the average
92
+ * rather than forcing it toward a false "perfectly crawlable". */
93
+ avgMissing: number | null;
94
+ perPage: {
95
+ url: string;
96
+ /** 0..1 — this page's own missing fraction (unweighted). */
97
+ missing: number;
98
+ /** Token count wordSet(rendered.text) produced for this page — the
99
+ * weight avgMissing gives it, and why a "coming soon" stub can't swing
100
+ * the headline number the way a full page does. */
101
+ renderedWords: number;
102
+ }[];
103
+ };
104
+ schema: {
105
+ typesFound: string[];
106
+ missingExpected: string[];
107
+ invalidBlocks: number;
108
+ };
109
+ meta: {
110
+ pageCount: number;
111
+ missingTitle: number;
112
+ missingDescription: number;
113
+ missingCanonical: number;
114
+ missingSocial: number;
115
+ /** Pages where neither the raw nor the rendered fetch produced an extract
116
+ * — excluded from pageCount and every other meta denominator above, so a
117
+ * report reading "1 of 2 pages missing a description" doesn't silently
118
+ * hide the third page that produced nothing at all. */
119
+ pagesWithoutExtract: number;
120
+ };
121
+ headings: {
122
+ pagesWithoutH1: number;
123
+ pagesWithLevelSkips: number;
124
+ };
125
+ securityHeaders: {
126
+ present: string[];
127
+ missing: string[];
128
+ };
129
+ /** False when the sitemap.xml fetch itself failed (crawl.sidecarErrors.sitemap
130
+ * !== null), so `sitemapPresent: false` would otherwise read as "confirmed
131
+ * absent" rather than "we never got an answer". The report must say "not
132
+ * measured" for sitemap presence when this is false, and computeScores must
133
+ * treat it as neutral rather than penalizing the technical component for a
134
+ * fetch we could not complete. */
135
+ sitemapMeasured: boolean;
136
+ sitemapPresent: boolean;
137
+ /** Same as sitemapMeasured, for the llms.txt fetch (crawl.sidecarErrors.llms). */
138
+ llmsTxtMeasured: boolean;
139
+ llmsTxtPresent: boolean;
140
+ viewportOk: boolean;
141
+ };
142
+ type BuyerQuestion = {
143
+ question: string;
144
+ answered: "yes" | "partial" | "no";
145
+ /** Is there a passage an AI answer could quote verbatim? */
146
+ quotable: boolean;
147
+ page: string | null;
148
+ evidence: string | null;
149
+ };
150
+ type Fix = {
151
+ title: string;
152
+ why: string;
153
+ impact: "high" | "medium" | "low";
154
+ effort: "low" | "medium" | "high";
155
+ tier: "crawl" | "content" | "technical";
156
+ };
157
+ type AnalyzeResult = {
158
+ /** The business's proper name, as a searchable proper noun — "Acme Roofing",
159
+ * not a description of it. Empty string when the site never states one,
160
+ * which is itself a finding. This is what the visibility probes query and
161
+ * brand-match against; `business` below is prose and unsearchable. */
162
+ businessName: string;
163
+ /** The model's read of what this company does, for whom, where. */
164
+ business: string;
165
+ entityClarity: {
166
+ score: number;
167
+ missing: string[];
168
+ };
169
+ buyerQuestions: BuyerQuestion[];
170
+ /** Standalone searches for the visibility probes — what a buyer types before
171
+ * they know this company exists. Distinct from `buyerQuestions`, which are
172
+ * phrased about this site and are unanswerable on their own; see the schema
173
+ * comment in analyze.ts for what that cost us. Never mentions the business. */
174
+ categoryQueries: string[];
175
+ fixes: Fix[];
176
+ narrative: {
177
+ findability: string;
178
+ readability: string;
179
+ answers: string;
180
+ };
181
+ };
182
+ type ProbeAnswer = {
183
+ engine: string;
184
+ query: string;
185
+ /** "branded" ("who is X" / "X reviews") always echoes the name back even when
186
+ * an engine knows nothing real, so it cannot be trusted as a discoverability
187
+ * signal. "category" is a real buyer question — the one the score is built
188
+ * from. "competitor" is a head-to-head comparison query. */
189
+ kind: "branded" | "category" | "competitor";
190
+ domainCited: boolean;
191
+ brandMentioned: boolean;
192
+ citedDomains: string[];
193
+ /** First ~300 chars of the engine's answer — the report's receipt. */
194
+ snippet: string;
195
+ /** True when `snippet` is a truncated prefix of a longer answer — set
196
+ * right where the truncation happens (probes.ts's SNIPPET_CHARS), so the
197
+ * renderer never has to re-derive "was this cut short?" from a length
198
+ * check of its own against a constant it can't see. */
199
+ truncated: boolean;
200
+ /** ISO-8601, set when the answer came back — this stage's whole output is
201
+ * quotable claims about a live third party, and answer engines' answers
202
+ * drift, so a disputed line in a report needs a date attached. */
203
+ askedAt: string;
204
+ };
205
+ type ProbesResult = {
206
+ answers: ProbeAnswer[];
207
+ /** 0..100 — fraction of CATEGORY answers (real buyer questions) where the
208
+ * prospect was cited or mentioned. Null — never 0 — when no category query
209
+ * ran at all, e.g. the analyze stage failed and supplied no buyer questions.
210
+ * Deliberately excludes branded answers: "who is X"/"X reviews" echo the name
211
+ * back even when the engine knows nothing real, which would put a floor
212
+ * under the score that has nothing to do with discoverability. */
213
+ visibilityScore: number | null;
214
+ /** Did any branded query produce a real citation of the prospect's own
215
+ * domain — not merely an echo of the name in prose. Reported separately from
216
+ * visibilityScore because it answers a different question ("does the engine
217
+ * know this business exists at all") from the one the score answers ("would
218
+ * it surface this business to someone who didn't already name it"). */
219
+ brandedRecognized: boolean;
220
+ competitorsSeen: {
221
+ domain: string;
222
+ count: number;
223
+ }[];
224
+ };
225
+ type LighthouseScores = {
226
+ performance: number | null;
227
+ accessibility: number | null;
228
+ bestPractices: number | null;
229
+ seo: number | null;
230
+ summary: string;
231
+ status: AuditResult["status"];
232
+ };
233
+ type Scores = {
234
+ findability: number | null;
235
+ readability: number | null;
236
+ answers: number | null;
237
+ aiVisibility: number | null;
238
+ };
239
+ type ProspectAuditResult = {
240
+ url: string;
241
+ /** The resolved searchable proper NOUN — "Acme Roofing", not a description
242
+ * of it (mirrors `AnalyzeResult.businessName`, the value this is usually
243
+ * copied from). Distinct from `AnalyzeResult.business` (prose, nested
244
+ * under `analyze.data`) — the two share a root name only because they're
245
+ * one level apart, not because they hold the same kind of value. Null
246
+ * when no name was ever resolved (operator override empty, model returned
247
+ * none) — itself a finding, not an error. Persisted verbatim into the
248
+ * database's `business` COLUMN (kept as-is; a rename there would need its
249
+ * own migration for no benefit) — map at that one boundary, not here. */
250
+ businessName: string | null;
251
+ generatedAt: string;
252
+ scores: Scores;
253
+ /** The crawl is the one fatal stage (see pipeline.ts): a crawl failure
254
+ * throws before a ProspectAuditResult is ever built, so the `ok:false`
255
+ * branch of StageResult can never appear here. Narrowed to the success
256
+ * shape only, so consumers never handle the impossible branch. */
257
+ crawl: {
258
+ ok: true;
259
+ data: CrawlResult;
260
+ };
261
+ checks: StageResult<ChecksResult>;
262
+ lighthouse: StageResult<LighthouseScores>;
263
+ analyze: StageResult<AnalyzeResult>;
264
+ probes: StageResult<ProbesResult>;
265
+ };
266
+
267
+ export type { AnalyzeResult, BuyerQuestion, ChecksResult, CrawlResult, Fix, LighthouseScores, PageCapture, PageExtract, ProbeAnswer, ProbesResult, ProspectAuditResult, RobotsAgentAccess, Scores, StageResult };
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -85,7 +85,7 @@ async function runProspectAuditCommand(url, opts) {
85
85
  "No TURSO_DATABASE_URL, so the report cannot be saved or shared. Re-run with --out <file>, or set the Turso credentials."
86
86
  );
87
87
  }
88
- const { runProspectAudit } = await import("./pipeline-S6CVHDGB.js");
88
+ const { runProspectAudit } = await import("./pipeline-3AANZFCR.js");
89
89
  const onStage = (name, status, detail) => {
90
90
  if (status === "start") console.error(`\u2026 ${name}`);
91
91
  else if (status === "ok") console.error(`\u2713 ${name}`);
@@ -103,7 +103,7 @@ async function runProspectAuditCommand(url, opts) {
103
103
  },
104
104
  { ...opts.deps ?? {}, onStage }
105
105
  );
106
- const { renderProspectReport } = await import("./render-AQAVQDPZ.js");
106
+ const { renderProspectReport } = await import("./render-IFOGFB5T.js");
107
107
  const html = renderProspectReport(result);
108
108
  const warnings = [];
109
109
  let file = null;
@@ -146,7 +146,7 @@ async function runProspectAuditCommand(url, opts) {
146
146
  let email = null;
147
147
  if (opts.email) {
148
148
  try {
149
- const { sendAuditEmail, parseProspectAuditRecipients } = await import("./email-VTTTKKL5.js");
149
+ const { sendAuditEmail, parseProspectAuditRecipients } = await import("./email-KJXASVH5.js");
150
150
  const recipients = parseProspectAuditRecipients(process.env.PROSPECT_AUDIT_RECIPIENTS);
151
151
  email = await sendAuditEmail(result, { link, recipients, auditId });
152
152
  if (!email.sent) {
@@ -171,4 +171,4 @@ async function runProspectAuditCommand(url, opts) {
171
171
  export {
172
172
  runProspectAuditCommand
173
173
  };
174
- //# sourceMappingURL=prospect-audit-IUNCJ2LV.js.map
174
+ //# sourceMappingURL=prospect-audit-KSXUNQ5J.js.map
@@ -2,9 +2,9 @@ import {
2
2
  ALL_CONFIG_NAMES,
3
3
  isConfigName,
4
4
  syncConfigs
5
- } from "../chunk-3VQWSE3F.js";
6
- import "../chunk-3G25KIWW.js";
5
+ } from "../chunk-7UAYMAKR.js";
7
6
  import "../chunk-73UFRZKY.js";
7
+ import "../chunk-3G25KIWW.js";
8
8
  import "../chunk-XTK5VIQB.js";
9
9
  import "../chunk-XXTZBPUY.js";
10
10
  export {
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  renderProspectReport
3
- } from "./chunk-W7Z3BGCI.js";
4
- import "./chunk-X3B723J4.js";
3
+ } from "./chunk-DEWJMMG3.js";
4
+ import "./chunk-HSI6Q7MI.js";
5
5
  import "./chunk-DJZWYIPP.js";
6
6
  import "./chunk-EVBCK2KC.js";
7
7
  export {
8
8
  renderProspectReport
9
9
  };
10
- //# sourceMappingURL=render-AQAVQDPZ.js.map
10
+ //# sourceMappingURL=render-IFOGFB5T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,17 @@
1
+ import {
2
+ renderReportFromRow,
3
+ requireLighthouse,
4
+ windowDays
5
+ } from "./chunk-ZGBEIWSC.js";
6
+ import "./chunk-GECESMVS.js";
7
+ import "./chunk-MT2JINSS.js";
8
+ import "./chunk-DJZWYIPP.js";
9
+ import "./chunk-5UINJNAJ.js";
10
+ import "./chunk-7NYN2P6I.js";
11
+ import "./chunk-EVBCK2KC.js";
12
+ export {
13
+ renderReportFromRow,
14
+ requireLighthouse,
15
+ windowDays
16
+ };
17
+ //# sourceMappingURL=render-from-row-BWSOIYDS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -5,16 +5,16 @@ import {
5
5
  openBase,
6
6
  readAirtableConfig
7
7
  } from "./chunk-HN6I2XWI.js";
8
- import {
9
- makeGitHub
10
- } from "./chunk-XHHMM5HX.js";
11
- import "./chunk-LBYOLBW7.js";
12
8
  import {
13
9
  allActionableVulnsTransitive,
14
10
  isDashboardVisible,
15
11
  listWebsites,
16
12
  updateAutoFixAttempts
17
- } from "./chunk-LDWZSLSU.js";
13
+ } from "./chunk-7NYN2P6I.js";
14
+ import {
15
+ makeGitHub
16
+ } from "./chunk-XHHMM5HX.js";
17
+ import "./chunk-LBYOLBW7.js";
18
18
  import "./chunk-6VAL7XJT.js";
19
19
 
20
20
  // src/github/renovate-dispatch.ts
@@ -130,10 +130,10 @@ async function runRenovateDispatchCommand(opts) {
130
130
  for (const f of result.failed) lines.push(` \u21B3 ${f.repo}: ${f.error}`);
131
131
  }
132
132
  const attemptUpdates = computeAutoFixAttemptUpdates(websites, result);
133
- const attemptTally = await applyAutoFixAttemptUpdates(
134
- attemptUpdates,
135
- (id, attempts) => updateAutoFixAttempts(base, id, attempts)
136
- );
133
+ const attemptTally = await applyAutoFixAttemptUpdates(attemptUpdates, async (id, attempts) => {
134
+ const fields = await updateAutoFixAttempts(base, id, attempts);
135
+ await opts.siteMirror?.health(id, fields);
136
+ });
137
137
  lines.push(formatRenovateDispatchSummary(result));
138
138
  lines.push(formatAutoFixAttemptsSummary(attemptTally));
139
139
  return { output: lines.join("\n"), code: 0 };
@@ -141,4 +141,4 @@ async function runRenovateDispatchCommand(opts) {
141
141
  export {
142
142
  runRenovateDispatchCommand
143
143
  };
144
- //# sourceMappingURL=renovate-dispatch-QJY75ODR.js.map
144
+ //# sourceMappingURL=renovate-dispatch-6GQWTRKX.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/github/renovate-dispatch.ts","../src/cli/commands/renovate-dispatch.ts"],"sourcesContent":["import {\n allActionableVulnsTransitive,\n isDashboardVisible,\n type WebsiteRow,\n} from \"../reports/airtable/websites.js\";\nimport { isRenovatePR } from \"../alerts/renovate.js\";\nimport type { PullRequestSummary } from \"./gh.js\";\n\n/**\n * On-demand Renovate trigger for fleet sites.\n *\n * The nightly security sweep (`fleet-security.yml`) writes each active site's\n * vulnerability counts to Airtable. This module turns that into action: for the\n * sites the sweep just flagged with *actionable* (critical or high) vulns, it\n * dispatches that repo's `renovate.yml` `workflow_dispatch` so Renovate runs off\n * its twice-daily schedule. Renovate's OSV vulnerability alerts bypass the\n * schedule, so the remediation PR opens immediately — and because GitHub's\n * platform auto-merge is disabled fleet-wide, Renovate merges it from inside that\n * same run (or a later scheduled one) per the shared preset, which is what keeps\n * the preset's \"majors are never auto-merged\" rule authoritative. Moderate/low\n * vulns are left to the normal schedule — not urgent enough to dispatch + churn\n * CI nightly.\n */\n\n/** The per-repo workflow file the fleet uses for its self-hosted Renovate run. */\nexport const RENOVATE_WORKFLOW_FILE = \"renovate.yml\";\n\n/**\n * Does the repo have a HEALTHY open Renovate PR — one that is NOT conflicting?\n *\n * A healthy PR means remediation is genuinely in flight (moving toward merge), so\n * re-dispatching would just churn CI — skip it. A CONFLICTING Renovate PR is STUCK\n * (its branch fell behind the base after another PR merged the same lockfile), so\n * it does NOT count as healthy: re-dispatching triggers Renovate to rebase it.\n * `UNKNOWN` mergeability (GitHub still computing it) is treated as healthy — don't\n * churn on uncertainty. Reuses {@link isRenovatePR} for the branch-prefix check.\n */\nexport function hasHealthyRenovatePr(prs: PullRequestSummary[]): boolean {\n return prs.some((pr) => isRenovatePR(pr) && pr.mergeable !== \"CONFLICTING\");\n}\n\n/** A site selected for an on-demand Renovate run, with the reason (vuln counts). */\nexport type RenovateDispatchTarget = {\n repo: string; // owner/repo, from the Websites \"Git repo\" field\n siteName: string;\n critical: number;\n high: number;\n};\n\n/**\n * Pick the active, repo-backed sites whose latest security sweep found a\n * critical or high vulnerability. A `null` vuln count is treated as zero (a site\n * that was never swept, or had its counts cleared, is not dispatched). Inactive\n * sites and sites without a `Git repo` are excluded — there's nothing to kick.\n *\n * Counts are only guaranteed fresh for sites the sweep actually audited (active +\n * URL present); a site whose write-back failed/was skipped this run is selected on\n * its last-known counts. That's safe — a stale-high false dispatch is an idempotent\n * Renovate no-op, and the open-PR skip in `dispatchRenovateAcross` avoids re-firing\n * when remediation is already in flight.\n */\nexport function selectRenovateTargets(sites: WebsiteRow[]): RenovateDispatchTarget[] {\n const targets: RenovateDispatchTarget[] = [];\n for (const s of sites) {\n if (!isDashboardVisible(s)) continue; // active (maintenance / launch period) only\n const repo = s.gitRepo?.trim();\n if (!repo) continue; // repo-backed only — the dispatch target is its renovate.yml\n const critical = s.securityVulnsCritical ?? 0;\n const high = s.securityVulnsHigh ?? 0;\n if (critical + high <= 0) continue; // actionable (critical/high) vulns only\n targets.push({ repo, siteName: s.name, critical, high });\n }\n return targets;\n}\n\n/** Injected GitHub operations — real ones come from `makeGitHub`, fakes from tests. */\nexport type RenovateDispatchDeps = {\n /** True iff the repo has a HEALTHY (non-conflicting) open Renovate PR —\n * remediation in flight, so don't re-dispatch. A conflicting/stuck PR returns\n * false so the dispatch goes through and Renovate rebases it. */\n hasHealthyOpenRenovatePr: (repo: string) => Promise<boolean>;\n defaultBranch: (repo: string) => Promise<string>;\n dispatch: (repo: string, workflow: string, ref: string) => Promise<void>;\n};\n\nexport type RenovateDispatchResult = {\n dispatched: string[];\n /** Repos skipped because a Renovate PR is already open — no point re-firing. */\n skipped: string[];\n failed: { repo: string; error: string }[];\n};\n\n/**\n * Dispatch `renovate.yml` for each target on its default branch.\n *\n * Skips a repo with a HEALTHY (non-conflicting) open Renovate PR — remediation is\n * in flight, so re-dispatching would just churn CI (keeps a persistent vuln from\n * re-firing every nightly run while its fix PR waits, e.g. a human-gated major\n * bump). A repo whose open Renovate PR is CONFLICTING/stuck is NOT skipped:\n * dispatching triggers Renovate to rebase it. Best-effort and isolated: a repo\n * that lacks `renovate.yml`, or whose\n * dispatch/PR-check is refused (the token's `actions:write` scope), is recorded in\n * `failed` and the rest still run. Never throws — the caller (a follow-up step on\n * the security sweep) must not be able to fail the sweep over a dispatch hiccup.\n */\nexport async function dispatchRenovateAcross(\n targets: RenovateDispatchTarget[],\n deps: RenovateDispatchDeps,\n): Promise<RenovateDispatchResult> {\n const dispatched: string[] = [];\n const skipped: string[] = [];\n const failed: { repo: string; error: string }[] = [];\n for (const t of targets) {\n try {\n if (await deps.hasHealthyOpenRenovatePr(t.repo)) {\n skipped.push(t.repo);\n continue;\n }\n const ref = await deps.defaultBranch(t.repo);\n await deps.dispatch(t.repo, RENOVATE_WORKFLOW_FILE, ref);\n dispatched.push(t.repo);\n } catch (e) {\n failed.push({ repo: t.repo, error: e instanceof Error ? e.message : String(e) });\n }\n }\n return { dispatched, skipped, failed };\n}\n\n/** Machine-readable counts line the workflow greps to annotate partial failures. */\nexport function formatRenovateDispatchSummary(result: RenovateDispatchResult): string {\n return `RENOVATE_DISPATCH_SUMMARY dispatched=${result.dispatched.length} skipped=${result.skipped.length} failed=${result.failed.length}`;\n}\n\n/**\n * Plan the per-site auto-fix-attempt counter writes from a dispatch result. PURE.\n * For each site:\n * - vulns now 0 (or never counted) → reset to 0 (episode resolved)\n * - else dispatched this run → attempts + 1 (a fresh failed-so-far attempt)\n * - else (skipped / failed / untouched) → unchanged\n * The RESET applies fleet-wide — regardless of Status or Git repo — so a site\n * archived or un-repo'd after its episode can't carry a stale counter into its\n * next vuln (Alamo sat at 7 from a long-closed episode). A null vuln count\n * (never-audited) reads as zero, consistent with the existing null-as-zero\n * semantics, so a lingering counter on such a row also clears. The INCREMENT\n * keeps its active + repo-backed filters (only those sites are dispatched).\n * Returns only the rows whose value CHANGES (a steady fleet writes nothing —\n * a 0→0 site emits no write). A skipped repo (healthy Renovate PR in flight) is\n * NOT a failed attempt — a fix is genuinely moving toward merge — so its counter\n * holds. A dispatched site whose critical/high advisories are ALL proven\n * TRANSITIVE also holds: Renovate has no direct-dep bump to open, so the run is a\n * known green no-op waiting on the weekly lockfile window, not a failed fix\n * attempt (Sonder rode 5 such no-ops to a false \"auto-fix failed\", 2026-08-10).\n * Missing/unknown relationship data keeps the increment — never mute on absence.\n */\nexport function computeAutoFixAttemptUpdates(\n sites: WebsiteRow[],\n result: RenovateDispatchResult,\n): { id: string; attempts: number }[] {\n const dispatched = new Set(result.dispatched);\n const updates: { id: string; attempts: number }[] = [];\n for (const s of sites) {\n const current = s.securityAutoFixAttempts ?? 0;\n const vulns = (s.securityVulnsCritical ?? 0) + (s.securityVulnsHigh ?? 0);\n if (vulns === 0) {\n // Episode resolved (or never confirmed): reset REGARDLESS of status/repo —\n // a site archived or un-repo'd after its episode must not carry a stale\n // counter into its next vuln (Alamo sat at 7 from a long-closed episode).\n if (current !== 0) updates.push({ id: s.id, attempts: 0 });\n continue;\n }\n if (!isDashboardVisible(s)) continue; // increments stay active-only\n const repo = s.gitRepo?.trim();\n if (!repo) continue; // …and repo-backed-only\n if (!dispatched.has(repo)) continue;\n // Transitive-only episode: the dispatch can't produce a fix PR (no direct dep\n // to bump) — don't count the no-op as a failed attempt.\n if (allActionableVulnsTransitive(s.securityAdvisories)) continue;\n updates.push({ id: s.id, attempts: current + 1 });\n }\n return updates;\n}\n\n/**\n * Apply the planned counter updates with an injected writer, BEST-EFFORT: a writer\n * that throws (e.g. the Airtable field not yet created, or a transient error) is\n * counted in `failed` and never propagates — the security sweep must not fail over\n * counter bookkeeping. Returns the applied/failed tallies for the summary line.\n */\nexport async function applyAutoFixAttemptUpdates(\n updates: { id: string; attempts: number }[],\n write: (id: string, attempts: number) => Promise<void>,\n): Promise<{ written: number; failed: number }> {\n let written = 0;\n let failed = 0;\n for (const u of updates) {\n try {\n await write(u.id, u.attempts);\n written++;\n } catch {\n failed++;\n }\n }\n return { written, failed };\n}\n\n/** Machine-readable counts line the workflow can grep, mirroring RENOVATE_DISPATCH_SUMMARY. */\nexport function formatAutoFixAttemptsSummary(tally: { written: number; failed: number }): string {\n return `AUTO_FIX_ATTEMPTS_SUMMARY written=${tally.written} failed=${tally.failed}`;\n}\n","import { openBase, readAirtableConfig, type AirtableBase } from \"../../reports/airtable/client.js\";\nimport { listWebsites, updateAutoFixAttempts } from \"../../reports/airtable/websites.js\";\nimport { makeGitHub } from \"../../github/gh.js\";\nimport {\n selectRenovateTargets,\n dispatchRenovateAcross,\n formatRenovateDispatchSummary,\n hasHealthyRenovatePr,\n computeAutoFixAttemptUpdates,\n applyAutoFixAttemptUpdates,\n formatAutoFixAttemptsSummary,\n type RenovateDispatchResult,\n} from \"../../github/renovate-dispatch.js\";\n\n/**\n * `renovate-dispatch --fleet`: read the Websites table, pick the active,\n * repo-backed sites the latest security sweep flagged with critical/high vulns,\n * and fire each one's `renovate.yml` `workflow_dispatch` so Renovate's OSV\n * vulnerability alerts open the remediation PR now (instead of waiting for the\n * weekly schedule). Designed to run as a best-effort follow-up step on\n * `fleet-security.yml` AFTER the sweep has written fresh counts to Airtable.\n *\n * Always exits 0: a missing token is a clean skip; a partial dispatch failure\n * (a repo without `renovate.yml`, or a token lacking `actions:write`) is reported\n * in the summary for the workflow to annotate, never failing the security sweep.\n */\nexport async function runRenovateDispatchCommand(opts: {\n fleet?: boolean | undefined;\n /** Inject a pre-opened Airtable base (tests). Defaults to env config. */\n base?: AirtableBase;\n}): Promise<{ output: string; code: number }> {\n if (!opts.fleet) {\n return { output: \"renovate-dispatch currently supports only --fleet\", code: 2 };\n }\n const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();\n if (!token) {\n return {\n output: \"renovate-dispatch skipped: no RENOVATE_TOKEN/GH_TOKEN (fleet dispatch) configured.\",\n code: 0,\n };\n }\n\n const base = opts.base ?? openBase(readAirtableConfig());\n const websites = await listWebsites(base);\n const targets = selectRenovateTargets(websites);\n\n const lines: string[] = [];\n let result: RenovateDispatchResult = { dispatched: [], skipped: [], failed: [] };\n if (targets.length === 0) {\n lines.push(\n \"No active repo-backed sites with critical/high vulnerabilities — nothing to dispatch.\",\n );\n } else {\n const gh = makeGitHub({ token });\n result = await dispatchRenovateAcross(targets, {\n hasHealthyOpenRenovatePr: async (repo) =>\n hasHealthyRenovatePr(await gh.openPullRequests(repo)),\n defaultBranch: (repo) => gh.defaultBranch(repo),\n dispatch: (repo, workflow, ref) => gh.dispatchWorkflow(repo, workflow, ref),\n });\n\n const failedRepos = new Set(result.failed.map((f) => f.repo));\n const skippedRepos = new Set(result.skipped);\n for (const t of targets) {\n const status = failedRepos.has(t.repo)\n ? \"FAILED \"\n : skippedRepos.has(t.repo)\n ? \"skipped (open Renovate PR) \"\n : \"dispatched \";\n lines.push(`${status} ${t.repo} (critical=${t.critical} high=${t.high}) — ${t.siteName}`);\n }\n for (const f of result.failed) lines.push(` ↳ ${f.repo}: ${f.error}`);\n }\n\n // Counter bookkeeping runs on EVERY fleet run — the reset-on-clean branch was\n // previously unreachable behind the zero-targets early return, so a fully-clean\n // fleet never cleared stale counters (Alamo stuck at 7, 2026-07). Best-effort:\n // a failed write is tallied, never thrown. Uses the full `websites` list so\n // 0-vuln sites reset.\n const attemptUpdates = computeAutoFixAttemptUpdates(websites, result);\n const attemptTally = await applyAutoFixAttemptUpdates(attemptUpdates, (id, attempts) =>\n updateAutoFixAttempts(base, id, attempts),\n );\n\n lines.push(formatRenovateDispatchSummary(result));\n lines.push(formatAutoFixAttemptsSummary(attemptTally));\n\n return { output: lines.join(\"\\n\"), code: 0 };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/github/renovate-dispatch.ts","../src/cli/commands/renovate-dispatch.ts"],"sourcesContent":["import {\n allActionableVulnsTransitive,\n isDashboardVisible,\n type WebsiteRow,\n} from \"../reports/airtable/websites.js\";\nimport { isRenovatePR } from \"../alerts/renovate.js\";\nimport type { PullRequestSummary } from \"./gh.js\";\n\n/**\n * On-demand Renovate trigger for fleet sites.\n *\n * The nightly security sweep (`fleet-security.yml`) writes each active site's\n * vulnerability counts to Airtable. This module turns that into action: for the\n * sites the sweep just flagged with *actionable* (critical or high) vulns, it\n * dispatches that repo's `renovate.yml` `workflow_dispatch` so Renovate runs off\n * its twice-daily schedule. Renovate's OSV vulnerability alerts bypass the\n * schedule, so the remediation PR opens immediately — and because GitHub's\n * platform auto-merge is disabled fleet-wide, Renovate merges it from inside that\n * same run (or a later scheduled one) per the shared preset, which is what keeps\n * the preset's \"majors are never auto-merged\" rule authoritative. Moderate/low\n * vulns are left to the normal schedule — not urgent enough to dispatch + churn\n * CI nightly.\n */\n\n/** The per-repo workflow file the fleet uses for its self-hosted Renovate run. */\nexport const RENOVATE_WORKFLOW_FILE = \"renovate.yml\";\n\n/**\n * Does the repo have a HEALTHY open Renovate PR — one that is NOT conflicting?\n *\n * A healthy PR means remediation is genuinely in flight (moving toward merge), so\n * re-dispatching would just churn CI — skip it. A CONFLICTING Renovate PR is STUCK\n * (its branch fell behind the base after another PR merged the same lockfile), so\n * it does NOT count as healthy: re-dispatching triggers Renovate to rebase it.\n * `UNKNOWN` mergeability (GitHub still computing it) is treated as healthy — don't\n * churn on uncertainty. Reuses {@link isRenovatePR} for the branch-prefix check.\n */\nexport function hasHealthyRenovatePr(prs: PullRequestSummary[]): boolean {\n return prs.some((pr) => isRenovatePR(pr) && pr.mergeable !== \"CONFLICTING\");\n}\n\n/** A site selected for an on-demand Renovate run, with the reason (vuln counts). */\nexport type RenovateDispatchTarget = {\n repo: string; // owner/repo, from the Websites \"Git repo\" field\n siteName: string;\n critical: number;\n high: number;\n};\n\n/**\n * Pick the active, repo-backed sites whose latest security sweep found a\n * critical or high vulnerability. A `null` vuln count is treated as zero (a site\n * that was never swept, or had its counts cleared, is not dispatched). Inactive\n * sites and sites without a `Git repo` are excluded — there's nothing to kick.\n *\n * Counts are only guaranteed fresh for sites the sweep actually audited (active +\n * URL present); a site whose write-back failed/was skipped this run is selected on\n * its last-known counts. That's safe — a stale-high false dispatch is an idempotent\n * Renovate no-op, and the open-PR skip in `dispatchRenovateAcross` avoids re-firing\n * when remediation is already in flight.\n */\nexport function selectRenovateTargets(sites: WebsiteRow[]): RenovateDispatchTarget[] {\n const targets: RenovateDispatchTarget[] = [];\n for (const s of sites) {\n if (!isDashboardVisible(s)) continue; // active (maintenance / launch period) only\n const repo = s.gitRepo?.trim();\n if (!repo) continue; // repo-backed only — the dispatch target is its renovate.yml\n const critical = s.securityVulnsCritical ?? 0;\n const high = s.securityVulnsHigh ?? 0;\n if (critical + high <= 0) continue; // actionable (critical/high) vulns only\n targets.push({ repo, siteName: s.name, critical, high });\n }\n return targets;\n}\n\n/** Injected GitHub operations — real ones come from `makeGitHub`, fakes from tests. */\nexport type RenovateDispatchDeps = {\n /** True iff the repo has a HEALTHY (non-conflicting) open Renovate PR —\n * remediation in flight, so don't re-dispatch. A conflicting/stuck PR returns\n * false so the dispatch goes through and Renovate rebases it. */\n hasHealthyOpenRenovatePr: (repo: string) => Promise<boolean>;\n defaultBranch: (repo: string) => Promise<string>;\n dispatch: (repo: string, workflow: string, ref: string) => Promise<void>;\n};\n\nexport type RenovateDispatchResult = {\n dispatched: string[];\n /** Repos skipped because a Renovate PR is already open — no point re-firing. */\n skipped: string[];\n failed: { repo: string; error: string }[];\n};\n\n/**\n * Dispatch `renovate.yml` for each target on its default branch.\n *\n * Skips a repo with a HEALTHY (non-conflicting) open Renovate PR — remediation is\n * in flight, so re-dispatching would just churn CI (keeps a persistent vuln from\n * re-firing every nightly run while its fix PR waits, e.g. a human-gated major\n * bump). A repo whose open Renovate PR is CONFLICTING/stuck is NOT skipped:\n * dispatching triggers Renovate to rebase it. Best-effort and isolated: a repo\n * that lacks `renovate.yml`, or whose\n * dispatch/PR-check is refused (the token's `actions:write` scope), is recorded in\n * `failed` and the rest still run. Never throws — the caller (a follow-up step on\n * the security sweep) must not be able to fail the sweep over a dispatch hiccup.\n */\nexport async function dispatchRenovateAcross(\n targets: RenovateDispatchTarget[],\n deps: RenovateDispatchDeps,\n): Promise<RenovateDispatchResult> {\n const dispatched: string[] = [];\n const skipped: string[] = [];\n const failed: { repo: string; error: string }[] = [];\n for (const t of targets) {\n try {\n if (await deps.hasHealthyOpenRenovatePr(t.repo)) {\n skipped.push(t.repo);\n continue;\n }\n const ref = await deps.defaultBranch(t.repo);\n await deps.dispatch(t.repo, RENOVATE_WORKFLOW_FILE, ref);\n dispatched.push(t.repo);\n } catch (e) {\n failed.push({ repo: t.repo, error: e instanceof Error ? e.message : String(e) });\n }\n }\n return { dispatched, skipped, failed };\n}\n\n/** Machine-readable counts line the workflow greps to annotate partial failures. */\nexport function formatRenovateDispatchSummary(result: RenovateDispatchResult): string {\n return `RENOVATE_DISPATCH_SUMMARY dispatched=${result.dispatched.length} skipped=${result.skipped.length} failed=${result.failed.length}`;\n}\n\n/**\n * Plan the per-site auto-fix-attempt counter writes from a dispatch result. PURE.\n * For each site:\n * - vulns now 0 (or never counted) → reset to 0 (episode resolved)\n * - else dispatched this run → attempts + 1 (a fresh failed-so-far attempt)\n * - else (skipped / failed / untouched) → unchanged\n * The RESET applies fleet-wide — regardless of Status or Git repo — so a site\n * archived or un-repo'd after its episode can't carry a stale counter into its\n * next vuln (Alamo sat at 7 from a long-closed episode). A null vuln count\n * (never-audited) reads as zero, consistent with the existing null-as-zero\n * semantics, so a lingering counter on such a row also clears. The INCREMENT\n * keeps its active + repo-backed filters (only those sites are dispatched).\n * Returns only the rows whose value CHANGES (a steady fleet writes nothing —\n * a 0→0 site emits no write). A skipped repo (healthy Renovate PR in flight) is\n * NOT a failed attempt — a fix is genuinely moving toward merge — so its counter\n * holds. A dispatched site whose critical/high advisories are ALL proven\n * TRANSITIVE also holds: Renovate has no direct-dep bump to open, so the run is a\n * known green no-op waiting on the weekly lockfile window, not a failed fix\n * attempt (Sonder rode 5 such no-ops to a false \"auto-fix failed\", 2026-08-10).\n * Missing/unknown relationship data keeps the increment — never mute on absence.\n */\nexport function computeAutoFixAttemptUpdates(\n sites: WebsiteRow[],\n result: RenovateDispatchResult,\n): { id: string; attempts: number }[] {\n const dispatched = new Set(result.dispatched);\n const updates: { id: string; attempts: number }[] = [];\n for (const s of sites) {\n const current = s.securityAutoFixAttempts ?? 0;\n const vulns = (s.securityVulnsCritical ?? 0) + (s.securityVulnsHigh ?? 0);\n if (vulns === 0) {\n // Episode resolved (or never confirmed): reset REGARDLESS of status/repo —\n // a site archived or un-repo'd after its episode must not carry a stale\n // counter into its next vuln (Alamo sat at 7 from a long-closed episode).\n if (current !== 0) updates.push({ id: s.id, attempts: 0 });\n continue;\n }\n if (!isDashboardVisible(s)) continue; // increments stay active-only\n const repo = s.gitRepo?.trim();\n if (!repo) continue; // …and repo-backed-only\n if (!dispatched.has(repo)) continue;\n // Transitive-only episode: the dispatch can't produce a fix PR (no direct dep\n // to bump) — don't count the no-op as a failed attempt.\n if (allActionableVulnsTransitive(s.securityAdvisories)) continue;\n updates.push({ id: s.id, attempts: current + 1 });\n }\n return updates;\n}\n\n/**\n * Apply the planned counter updates with an injected writer, BEST-EFFORT: a writer\n * that throws (e.g. the Airtable field not yet created, or a transient error) is\n * counted in `failed` and never propagates — the security sweep must not fail over\n * counter bookkeeping. Returns the applied/failed tallies for the summary line.\n */\nexport async function applyAutoFixAttemptUpdates(\n updates: { id: string; attempts: number }[],\n write: (id: string, attempts: number) => Promise<void>,\n): Promise<{ written: number; failed: number }> {\n let written = 0;\n let failed = 0;\n for (const u of updates) {\n try {\n await write(u.id, u.attempts);\n written++;\n } catch {\n failed++;\n }\n }\n return { written, failed };\n}\n\n/** Machine-readable counts line the workflow can grep, mirroring RENOVATE_DISPATCH_SUMMARY. */\nexport function formatAutoFixAttemptsSummary(tally: { written: number; failed: number }): string {\n return `AUTO_FIX_ATTEMPTS_SUMMARY written=${tally.written} failed=${tally.failed}`;\n}\n","import { openBase, readAirtableConfig, type AirtableBase } from \"../../reports/airtable/client.js\";\nimport type { SiteMirror } from \"../../db/site-mirror.js\";\nimport { listWebsites, updateAutoFixAttempts } from \"../../reports/airtable/websites.js\";\nimport { makeGitHub } from \"../../github/gh.js\";\nimport {\n selectRenovateTargets,\n dispatchRenovateAcross,\n formatRenovateDispatchSummary,\n hasHealthyRenovatePr,\n computeAutoFixAttemptUpdates,\n applyAutoFixAttemptUpdates,\n formatAutoFixAttemptsSummary,\n type RenovateDispatchResult,\n} from \"../../github/renovate-dispatch.js\";\n\n/**\n * `renovate-dispatch --fleet`: read the Websites table, pick the active,\n * repo-backed sites the latest security sweep flagged with critical/high vulns,\n * and fire each one's `renovate.yml` `workflow_dispatch` so Renovate's OSV\n * vulnerability alerts open the remediation PR now (instead of waiting for the\n * weekly schedule). Designed to run as a best-effort follow-up step on\n * `fleet-security.yml` AFTER the sweep has written fresh counts to Airtable.\n *\n * Always exits 0: a missing token is a clean skip; a partial dispatch failure\n * (a repo without `renovate.yml`, or a token lacking `actions:write`) is reported\n * in the summary for the workflow to annotate, never failing the security sweep.\n */\nexport async function runRenovateDispatchCommand(opts: {\n fleet?: boolean | undefined;\n /** Inject a pre-opened Airtable base (tests). Defaults to env config. */\n base?: AirtableBase;\n /** #539 Phase 5: Turso write-through for the auto-fix counter. Injected from\n * bin.ts rather than defaulted here — this function is called directly by\n * tests, and a default would open a real libSQL handle inside the suite. */\n siteMirror?: SiteMirror;\n}): Promise<{ output: string; code: number }> {\n if (!opts.fleet) {\n return { output: \"renovate-dispatch currently supports only --fleet\", code: 2 };\n }\n const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();\n if (!token) {\n return {\n output: \"renovate-dispatch skipped: no RENOVATE_TOKEN/GH_TOKEN (fleet dispatch) configured.\",\n code: 0,\n };\n }\n\n const base = opts.base ?? openBase(readAirtableConfig());\n const websites = await listWebsites(base);\n const targets = selectRenovateTargets(websites);\n\n const lines: string[] = [];\n let result: RenovateDispatchResult = { dispatched: [], skipped: [], failed: [] };\n if (targets.length === 0) {\n lines.push(\n \"No active repo-backed sites with critical/high vulnerabilities — nothing to dispatch.\",\n );\n } else {\n const gh = makeGitHub({ token });\n result = await dispatchRenovateAcross(targets, {\n hasHealthyOpenRenovatePr: async (repo) =>\n hasHealthyRenovatePr(await gh.openPullRequests(repo)),\n defaultBranch: (repo) => gh.defaultBranch(repo),\n dispatch: (repo, workflow, ref) => gh.dispatchWorkflow(repo, workflow, ref),\n });\n\n const failedRepos = new Set(result.failed.map((f) => f.repo));\n const skippedRepos = new Set(result.skipped);\n for (const t of targets) {\n const status = failedRepos.has(t.repo)\n ? \"FAILED \"\n : skippedRepos.has(t.repo)\n ? \"skipped (open Renovate PR) \"\n : \"dispatched \";\n lines.push(`${status} ${t.repo} (critical=${t.critical} high=${t.high}) — ${t.siteName}`);\n }\n for (const f of result.failed) lines.push(` ↳ ${f.repo}: ${f.error}`);\n }\n\n // Counter bookkeeping runs on EVERY fleet run — the reset-on-clean branch was\n // previously unreachable behind the zero-targets early return, so a fully-clean\n // fleet never cleared stale counters (Alamo stuck at 7, 2026-07). Best-effort:\n // a failed write is tallied, never thrown. Uses the full `websites` list so\n // 0-vuln sites reset.\n const attemptUpdates = computeAutoFixAttemptUpdates(websites, result);\n const attemptTally = await applyAutoFixAttemptUpdates(attemptUpdates, async (id, attempts) => {\n const fields = await updateAutoFixAttempts(base, id, attempts);\n await opts.siteMirror?.health(id, fields);\n });\n\n lines.push(formatRenovateDispatchSummary(result));\n lines.push(formatAutoFixAttemptsSummary(attemptTally));\n\n return { output: lines.join(\"\\n\"), code: 0 };\n}\n"],"mappings":"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","names":[]}
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  draftReportForSite,
6
6
  readGaConfig
7
- } from "./chunk-BYK66EWO.js";
7
+ } from "./chunk-BBUSGEES.js";
8
8
  import {
9
9
  findDueReports,
10
10
  nextDueDate,
@@ -12,27 +12,27 @@ import {
12
12
  } from "./chunk-PZR4EBPY.js";
13
13
  import {
14
14
  reportTier
15
- } from "./chunk-ILUNZHYV.js";
16
- import "./chunk-5UINJNAJ.js";
17
- import "./chunk-CQR4LIM7.js";
18
- import "./chunk-Z7AXYKQB.js";
19
- import {
20
- escapeHtml
21
- } from "./chunk-DJZWYIPP.js";
15
+ } from "./chunk-YFAXOD7Z.js";
22
16
  import {
23
17
  listAllReports
24
- } from "./chunk-ACBRLLFT.js";
18
+ } from "./chunk-F3JWOPTG.js";
25
19
  import "./chunk-VAD43MOR.js";
20
+ import "./chunk-IY6256GK.js";
26
21
  import {
27
22
  openBase,
28
23
  readAirtableConfig
29
24
  } from "./chunk-HN6I2XWI.js";
30
- import "./chunk-EVBCK2KC.js";
25
+ import "./chunk-MT2JINSS.js";
26
+ import {
27
+ escapeHtml
28
+ } from "./chunk-DJZWYIPP.js";
29
+ import "./chunk-5UINJNAJ.js";
31
30
  import {
32
31
  listWebsites,
33
32
  siteSlug,
34
33
  updateNextDueDates
35
- } from "./chunk-LDWZSLSU.js";
34
+ } from "./chunk-7NYN2P6I.js";
35
+ import "./chunk-EVBCK2KC.js";
36
36
  import "./chunk-6VAL7XJT.js";
37
37
 
38
38
  // src/alerts/analytics-health.ts
@@ -78,12 +78,34 @@ function dashboardBaseUrl() {
78
78
  }
79
79
  async function runReportCommand(slug, opts) {
80
80
  if (opts.digest) {
81
- const { runDigest } = await import("./digest-BL6UJYKH.js");
81
+ const { runDigest } = await import("./digest-FW6QMZZJ.js");
82
82
  return runDigest({ baseUrl: dashboardBaseUrl() });
83
83
  }
84
84
  if (opts.sendReady) {
85
- const { sendApprovedReports } = await import("./orchestrate-77UO4BYJ.js");
86
- return sendApprovedReports();
85
+ const { sendApprovedReports } = await import("./orchestrate-YLWQ6KK2.js");
86
+ const { makeSiteMirror } = await import("./site-mirror-NIREX5ME.js");
87
+ return sendApprovedReports({ siteMirror: await makeSiteMirror() });
88
+ }
89
+ if (opts.rerender) {
90
+ const { rerenderReport, formatRerenderResult } = await import("./rerender-3FM6DNRH.js");
91
+ const { renderReportFromRow } = await import("./render-from-row-BWSOIYDS.js");
92
+ const { openDb, readDbConfig } = await import("./client-AUE3XUPO.js");
93
+ const { getReportById, getSiteById, storeRenderedHtml } = await import("./fleet-state-7TJ2GYT2.js");
94
+ const { loadHeaderImage } = await import("./header-images-SQBCE5OD.js");
95
+ const { fetchAttachmentBytes } = await import("./attachments-NLZIJ5NU.js");
96
+ const db = await openDb(readDbConfig());
97
+ const result = await rerenderReport(
98
+ {
99
+ getReport: (id) => getReportById(db, id),
100
+ getSite: (id) => getSiteById(db, id),
101
+ loadHeaderPlate: async (id) => (await loadHeaderImage(db, id))?.bytes ?? null,
102
+ fetchAirtableHeader: async (url) => (await fetchAttachmentBytes(url)).bytes,
103
+ render: (site, report, plate) => renderReportFromRow(site, report, plate),
104
+ store: (id, html) => storeRenderedHtml(db, id, html)
105
+ },
106
+ opts.rerender
107
+ );
108
+ return { output: formatRerenderResult(result), code: result.status === "rendered" ? 0 : 1 };
87
109
  }
88
110
  if (opts.due) {
89
111
  return runDueDraft();
@@ -107,8 +129,16 @@ async function runReportCommand(slug, opts) {
107
129
  }
108
130
  async function runDueDraft() {
109
131
  const base = openBase(readAirtableConfig());
110
- const { makeScheduleMirrorBestEffort } = await import("./health-mirror-4B2L7T2Y.js");
111
- const result = await draftDueReports(base, /* @__PURE__ */ new Date(), await makeScheduleMirrorBestEffort());
132
+ const { makeScheduleMirrorBestEffort } = await import("./health-mirror-L5JBEJVA.js");
133
+ const { makeReportMirror } = await import("./report-mirror-4MNORYQ4.js");
134
+ const { makeSiteMirror } = await import("./site-mirror-NIREX5ME.js");
135
+ const result = await draftDueReports(
136
+ base,
137
+ /* @__PURE__ */ new Date(),
138
+ await makeScheduleMirrorBestEffort(),
139
+ await makeReportMirror(),
140
+ await makeSiteMirror()
141
+ );
112
142
  await alertOnFleetAnalyticsFailure(result.health);
113
143
  return { output: result.output, code: result.code };
114
144
  }
@@ -165,7 +195,11 @@ async function writeNextDueDates(base, websites, reports, today, scheduleMirror
165
195
  const mirrorNote = scheduleMirror ? ` mirrored=${mirrored} mirror_failed=${mirrorFailed} mirror_missed=${mirrorMissed}` : " mirror=absent";
166
196
  console.log(`NEXT_DUE_WRITE wrote=${wrote} skipped=${skipped} failed=${failed}${mirrorNote}`);
167
197
  }
168
- async function draftDueReports(base, today, scheduleMirror = null) {
198
+ async function draftDueReports(base, today, scheduleMirror = null, reportMirror, siteMirror) {
199
+ const mirrorOpt = {
200
+ ...reportMirror ? { reportMirror } : {},
201
+ ...siteMirror ? { siteMirror } : {}
202
+ };
169
203
  const websites = await listWebsites(base);
170
204
  const reports = await listAllReports(base);
171
205
  await writeNextDueDates(base, websites, reports, today, scheduleMirror);
@@ -210,7 +244,8 @@ async function draftDueReports(base, today, scheduleMirror = null) {
210
244
  const result = await draftReportForSite(base, item.site, item.reportType, {
211
245
  period,
212
246
  completeRowId: existing.id,
213
- existingRow: existing
247
+ existingRow: existing,
248
+ ...mirrorOpt
214
249
  });
215
250
  existing.draftReady = result.queued === true;
216
251
  lines.push(
@@ -241,7 +276,10 @@ async function draftDueReports(base, today, scheduleMirror = null) {
241
276
  continue;
242
277
  }
243
278
  try {
244
- const result = await draftReportForSite(base, item.site, item.reportType, { period });
279
+ const result = await draftReportForSite(base, item.site, item.reportType, {
280
+ period,
281
+ ...mirrorOpt
282
+ });
245
283
  lines.push(draftLine(result.reportRow?.reportId, result.queued, result.supersededIds));
246
284
  if (result.reportRow) reports.push(result.reportRow);
247
285
  if (isAnalyticsConfigured(item.site)) gaConfiguredSites++;
@@ -283,11 +321,15 @@ async function runSingleSiteDraft(slug, opts) {
283
321
  if (!site) {
284
322
  throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
285
323
  }
324
+ const reportMirror = opts.previewOnly ? null : await (await import("./report-mirror-4MNORYQ4.js")).makeReportMirror();
325
+ const siteMirror = opts.previewOnly ? null : await (await import("./site-mirror-NIREX5ME.js")).makeSiteMirror();
286
326
  const result = await draftReportForSite(opts.previewOnly ? null : base, site, opts.reportType, {
287
327
  previewOnly: opts.previewOnly,
288
328
  // Only forced on. Left undefined, draftReportForSite keeps its default
289
329
  // (enrich iff there is a base), so the real drafting path is untouched.
290
- ...opts.enrich ? { enrich: true } : {}
330
+ ...opts.enrich ? { enrich: true } : {},
331
+ ...reportMirror ? { reportMirror } : {},
332
+ ...siteMirror ? { siteMirror } : {}
291
333
  });
292
334
  if (opts.previewOnly) {
293
335
  return { output: `Preview written to ${result.htmlPath}`, code: 0 };
@@ -308,4 +350,4 @@ export {
308
350
  runReportCommand,
309
351
  writeNextDueDates
310
352
  };
311
- //# sourceMappingURL=report-4EVNDHOZ.js.map
353
+ //# sourceMappingURL=report-R3E3Y7HR.js.map