@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.
- package/dist/airtable-ZBUPQJSC.js +9 -0
- package/dist/{announce-MXTUQ6C7.js → announce-I2Z4JN3I.js} +49 -28
- package/dist/announce-I2Z4JN3I.js.map +1 -0
- package/dist/attachments-NLZIJ5NU.js +9 -0
- package/dist/{bump-deps-EC74CPLS.js → bump-deps-CGVELRLK.js} +2 -2
- package/dist/{chunk-2ZPT7GLV.js → chunk-3JQ2FO3D.js} +3 -3
- package/dist/chunk-3JQ2FO3D.js.map +1 -0
- package/dist/{chunk-KTH5ZODA.js → chunk-7BZUOZPU.js} +2 -2
- package/dist/{chunk-LDWZSLSU.js → chunk-7NYN2P6I.js} +7 -4
- package/dist/{chunk-LDWZSLSU.js.map → chunk-7NYN2P6I.js.map} +1 -1
- package/dist/{chunk-3VQWSE3F.js → chunk-7UAYMAKR.js} +4 -4
- package/dist/{chunk-BYK66EWO.js → chunk-BBUSGEES.js} +52 -46
- package/dist/chunk-BBUSGEES.js.map +1 -0
- package/dist/{chunk-SLUQ7R6I.js → chunk-C6CLRKPV.js} +5 -5
- package/dist/{chunk-P2FE5AI6.js → chunk-CROKUHJB.js} +2 -2
- package/dist/{chunk-W7Z3BGCI.js → chunk-DEWJMMG3.js} +2 -2
- package/dist/{chunk-ACBRLLFT.js → chunk-F3JWOPTG.js} +7 -3
- package/dist/chunk-F3JWOPTG.js.map +1 -0
- package/dist/{chunk-TERE5SDF.js → chunk-HEWSRXYV.js} +4 -4
- package/dist/chunk-HHMMUR4P.js +335 -0
- package/dist/chunk-HHMMUR4P.js.map +1 -0
- package/dist/{chunk-X3B723J4.js → chunk-HSI6Q7MI.js} +38 -9
- package/dist/chunk-HSI6Q7MI.js.map +1 -0
- package/dist/{chunk-Z7AXYKQB.js → chunk-IY6256GK.js} +1 -1
- package/dist/{chunk-CQR4LIM7.js → chunk-MT2JINSS.js} +5 -1
- package/dist/{chunk-CQR4LIM7.js.map → chunk-MT2JINSS.js.map} +1 -1
- package/dist/{chunk-F5KM4LEQ.js → chunk-NCMZMLY7.js} +3 -3
- package/dist/{chunk-2NDRHXMR.js → chunk-NDWYJLRF.js} +17 -140
- package/dist/chunk-NDWYJLRF.js.map +1 -0
- package/dist/{chunk-PPE5KZJK.js → chunk-NXNPDCF3.js} +3 -3
- package/dist/{chunk-ITIOFN7A.js → chunk-P6AORG32.js} +29 -10
- package/dist/chunk-P6AORG32.js.map +1 -0
- package/dist/{chunk-N45M3FDK.js → chunk-SDDPUAWH.js} +4 -4
- package/dist/{chunk-ZKXSX4LO.js → chunk-TD2NNXLZ.js} +9 -9
- package/dist/{chunk-ILUNZHYV.js → chunk-YFAXOD7Z.js} +10 -6
- package/dist/chunk-YFAXOD7Z.js.map +1 -0
- package/dist/{chunk-2EVEFOZY.js → chunk-ZBDZTJUO.js} +2 -2
- package/dist/chunk-ZGBEIWSC.js +146 -0
- package/dist/chunk-ZGBEIWSC.js.map +1 -0
- package/dist/cli/bin.js +32 -27
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +15 -11
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/{convert-to-pnpm-6L6P74WR.js → convert-to-pnpm-QFJOEP5V.js} +2 -2
- package/dist/{db-B35OGV3G.js → db-UXNIZ2RG.js} +6 -6
- package/dist/{digest-BL6UJYKH.js → digest-FW6QMZZJ.js} +17 -16
- package/dist/{digest-BL6UJYKH.js.map → digest-FW6QMZZJ.js.map} +1 -1
- package/dist/{email-VTTTKKL5.js → email-KJXASVH5.js} +3 -3
- package/dist/{ensure-site-DFTB7MN3.js → ensure-site-4DCEBAPM.js} +19 -11
- package/dist/ensure-site-4DCEBAPM.js.map +1 -0
- package/dist/fleet-state-7TJ2GYT2.js +39 -0
- package/dist/{forms-notify-target-75U6OU7L.js → forms-notify-target-2EAWCWBW.js} +7 -4
- package/dist/forms-notify-target-2EAWCWBW.js.map +1 -0
- package/dist/{github-signals-IJUPGCUX.js → github-signals-E2WPYTBK.js} +9 -9
- package/dist/{header-image-AWLLQA4L.js → header-image-6IC53W27.js} +7 -7
- package/dist/{header-images-335L2AKB.js → header-images-SQBCE5OD.js} +21 -1
- package/dist/header-images-SQBCE5OD.js.map +1 -0
- package/dist/{health-endpoint-WJC2UMYS.js → health-endpoint-RFXL2F4A.js} +2 -2
- package/dist/health-mirror-L5JBEJVA.js +41 -0
- package/dist/health-mirror-L5JBEJVA.js.map +1 -0
- package/dist/{import-airtable-HE43FB4H.js → import-airtable-OHVY344G.js} +7 -5
- package/dist/index.d.ts +149 -0
- package/dist/index.js +147 -50
- package/dist/index.js.map +1 -1
- package/dist/{init-NJEOEO7I.js → init-XRAQZYID.js} +9 -9
- package/dist/{launch-XNAX427U.js → launch-IX4Y5JJC.js} +55 -26
- package/dist/launch-IX4Y5JJC.js.map +1 -0
- package/dist/{lighthouse-airtable-LBVIYYP5.js → lighthouse-airtable-3GUWHETW.js} +3 -3
- package/dist/{onboard-GK4PHPNI.js → onboard-O4IAHJMO.js} +2 -2
- package/dist/{orchestrate-77UO4BYJ.js → orchestrate-YLWQ6KK2.js} +10 -9
- package/dist/{parity-MWS24A3H.js → parity-56KA2DGR.js} +4 -4
- package/dist/{pipeline-S6CVHDGB.js → pipeline-3AANZFCR.js} +2 -2
- package/dist/{preflight-6WBNU2GZ.js → preflight-TNORKGCX.js} +11 -10
- package/dist/{preflight-6WBNU2GZ.js.map → preflight-TNORKGCX.js.map} +1 -1
- package/dist/{prismic-ci-3RHOZUUT.js → prismic-ci-G2AK53VL.js} +5 -5
- package/dist/{prismic-models-QBGUT2Q3.js → prismic-models-O6Z3OK3A.js} +13 -8
- package/dist/prismic-models-O6Z3OK3A.js.map +1 -0
- package/dist/prospect/types.d.ts +267 -0
- package/dist/prospect/types.js +1 -0
- package/dist/{prospect-audit-IUNCJ2LV.js → prospect-audit-KSXUNQ5J.js} +4 -4
- package/dist/recipes/sync-configs.js +2 -2
- package/dist/{render-AQAVQDPZ.js → render-IFOGFB5T.js} +3 -3
- package/dist/render-IFOGFB5T.js.map +1 -0
- package/dist/render-from-row-BWSOIYDS.js +17 -0
- package/dist/render-from-row-BWSOIYDS.js.map +1 -0
- package/dist/{renovate-dispatch-QJY75ODR.js → renovate-dispatch-6GQWTRKX.js} +10 -10
- package/dist/{renovate-dispatch-QJY75ODR.js.map → renovate-dispatch-6GQWTRKX.js.map} +1 -1
- package/dist/{report-4EVNDHOZ.js → report-R3E3Y7HR.js} +63 -21
- package/dist/report-R3E3Y7HR.js.map +1 -0
- package/dist/report-mirror-4MNORYQ4.js +49 -0
- package/dist/report-mirror-4MNORYQ4.js.map +1 -0
- package/dist/rerender-3FM6DNRH.js +32 -0
- package/dist/rerender-3FM6DNRH.js.map +1 -0
- package/dist/{self-updating-6DWI2Z56.js → self-updating-UCIWESAX.js} +2 -2
- package/dist/{selftest-RC36MTKP.js → selftest-VGHWPN72.js} +22 -20
- package/dist/{selftest-RC36MTKP.js.map → selftest-VGHWPN72.js.map} +1 -1
- package/dist/site-mirror-NIREX5ME.js +52 -0
- package/dist/site-mirror-NIREX5ME.js.map +1 -0
- package/dist/{smoke-suite-VYY2LXM2.js → smoke-suite-U3UKD3NS.js} +2 -2
- package/dist/{submissions-Q7DRFOCD.js → submissions-IQEJX6FK.js} +2 -2
- package/dist/{svelte-codemods-25J2FO22.js → svelte-codemods-GOKXTBHW.js} +2 -2
- package/dist/{sync-34HNZZ56.js → sync-CTUVXMCK.js} +4 -4
- package/dist/{sync-configs-3SE6PSCO.js → sync-configs-SPTLFJCC.js} +4 -4
- package/dist/{upgrade-WBRQ62FX.js → upgrade-W6ZR3FJH.js} +2 -2
- package/dist/{webflow-IRKOIT7S.js → webflow-624FEBFD.js} +4 -4
- package/dist/{websites-SXWTP72S.js → websites-D6WYFAES.js} +2 -2
- package/dist/websites-D6WYFAES.js.map +1 -0
- package/dist/{write-audits-to-airtable-MBEILLPU.js → write-audits-to-airtable-CMKHGJNQ.js} +4 -4
- package/dist/write-audits-to-airtable-CMKHGJNQ.js.map +1 -0
- package/package.json +5 -1
- package/dist/airtable-G7WBQQMI.js +0 -9
- package/dist/announce-MXTUQ6C7.js.map +0 -1
- package/dist/chunk-2NDRHXMR.js.map +0 -1
- package/dist/chunk-2ZPT7GLV.js.map +0 -1
- package/dist/chunk-ACBRLLFT.js.map +0 -1
- package/dist/chunk-BYK66EWO.js.map +0 -1
- package/dist/chunk-ILUNZHYV.js.map +0 -1
- package/dist/chunk-ITIOFN7A.js.map +0 -1
- package/dist/chunk-X3B723J4.js.map +0 -1
- package/dist/ensure-site-DFTB7MN3.js.map +0 -1
- package/dist/forms-notify-target-75U6OU7L.js.map +0 -1
- package/dist/header-images-335L2AKB.js.map +0 -1
- package/dist/health-mirror-4B2L7T2Y.js +0 -75
- package/dist/health-mirror-4B2L7T2Y.js.map +0 -1
- package/dist/launch-XNAX427U.js.map +0 -1
- package/dist/prismic-models-QBGUT2Q3.js.map +0 -1
- package/dist/report-4EVNDHOZ.js.map +0 -1
- /package/dist/{airtable-G7WBQQMI.js.map → airtable-ZBUPQJSC.js.map} +0 -0
- /package/dist/{import-airtable-HE43FB4H.js.map → attachments-NLZIJ5NU.js.map} +0 -0
- /package/dist/{bump-deps-EC74CPLS.js.map → bump-deps-CGVELRLK.js.map} +0 -0
- /package/dist/{chunk-KTH5ZODA.js.map → chunk-7BZUOZPU.js.map} +0 -0
- /package/dist/{chunk-3VQWSE3F.js.map → chunk-7UAYMAKR.js.map} +0 -0
- /package/dist/{chunk-SLUQ7R6I.js.map → chunk-C6CLRKPV.js.map} +0 -0
- /package/dist/{chunk-P2FE5AI6.js.map → chunk-CROKUHJB.js.map} +0 -0
- /package/dist/{chunk-W7Z3BGCI.js.map → chunk-DEWJMMG3.js.map} +0 -0
- /package/dist/{chunk-TERE5SDF.js.map → chunk-HEWSRXYV.js.map} +0 -0
- /package/dist/{chunk-Z7AXYKQB.js.map → chunk-IY6256GK.js.map} +0 -0
- /package/dist/{chunk-F5KM4LEQ.js.map → chunk-NCMZMLY7.js.map} +0 -0
- /package/dist/{chunk-PPE5KZJK.js.map → chunk-NXNPDCF3.js.map} +0 -0
- /package/dist/{chunk-N45M3FDK.js.map → chunk-SDDPUAWH.js.map} +0 -0
- /package/dist/{chunk-ZKXSX4LO.js.map → chunk-TD2NNXLZ.js.map} +0 -0
- /package/dist/{chunk-2EVEFOZY.js.map → chunk-ZBDZTJUO.js.map} +0 -0
- /package/dist/{convert-to-pnpm-6L6P74WR.js.map → convert-to-pnpm-QFJOEP5V.js.map} +0 -0
- /package/dist/{db-B35OGV3G.js.map → db-UXNIZ2RG.js.map} +0 -0
- /package/dist/{email-VTTTKKL5.js.map → email-KJXASVH5.js.map} +0 -0
- /package/dist/{lighthouse-airtable-LBVIYYP5.js.map → fleet-state-7TJ2GYT2.js.map} +0 -0
- /package/dist/{github-signals-IJUPGCUX.js.map → github-signals-E2WPYTBK.js.map} +0 -0
- /package/dist/{header-image-AWLLQA4L.js.map → header-image-6IC53W27.js.map} +0 -0
- /package/dist/{health-endpoint-WJC2UMYS.js.map → health-endpoint-RFXL2F4A.js.map} +0 -0
- /package/dist/{orchestrate-77UO4BYJ.js.map → import-airtable-OHVY344G.js.map} +0 -0
- /package/dist/{init-NJEOEO7I.js.map → init-XRAQZYID.js.map} +0 -0
- /package/dist/{parity-MWS24A3H.js.map → lighthouse-airtable-3GUWHETW.js.map} +0 -0
- /package/dist/{onboard-GK4PHPNI.js.map → onboard-O4IAHJMO.js.map} +0 -0
- /package/dist/{pipeline-S6CVHDGB.js.map → orchestrate-YLWQ6KK2.js.map} +0 -0
- /package/dist/{render-AQAVQDPZ.js.map → parity-56KA2DGR.js.map} +0 -0
- /package/dist/{websites-SXWTP72S.js.map → pipeline-3AANZFCR.js.map} +0 -0
- /package/dist/{prismic-ci-3RHOZUUT.js.map → prismic-ci-G2AK53VL.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-MBEILLPU.js.map → prospect/types.js.map} +0 -0
- /package/dist/{prospect-audit-IUNCJ2LV.js.map → prospect-audit-KSXUNQ5J.js.map} +0 -0
- /package/dist/{self-updating-6DWI2Z56.js.map → self-updating-UCIWESAX.js.map} +0 -0
- /package/dist/{smoke-suite-VYY2LXM2.js.map → smoke-suite-U3UKD3NS.js.map} +0 -0
- /package/dist/{submissions-Q7DRFOCD.js.map → submissions-IQEJX6FK.js.map} +0 -0
- /package/dist/{svelte-codemods-25J2FO22.js.map → svelte-codemods-GOKXTBHW.js.map} +0 -0
- /package/dist/{sync-34HNZZ56.js.map → sync-CTUVXMCK.js.map} +0 -0
- /package/dist/{sync-configs-3SE6PSCO.js.map → sync-configs-SPTLFJCC.js.map} +0 -0
- /package/dist/{upgrade-WBRQ62FX.js.map → upgrade-W6ZR3FJH.js.map} +0 -0
- /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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
4
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
135
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
86
|
-
|
|
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-
|
|
111
|
-
const
|
|
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, {
|
|
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-
|
|
353
|
+
//# sourceMappingURL=report-R3E3Y7HR.js.map
|