@reddoorla/maintenance 0.71.0 → 0.74.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 (126) hide show
  1. package/README.md +15 -14
  2. package/dist/airtable-6L3JD7FY.js +9 -0
  3. package/dist/{announce-3JE5FKW7.js → announce-V6EYPPUR.js} +6 -6
  4. package/dist/{blux-FKZYMKLF.js → blux-OL5HKP26.js} +950 -69
  5. package/dist/blux-OL5HKP26.js.map +1 -0
  6. package/dist/{bump-deps-K3JMLMBB.js → bump-deps-OQR3IXLN.js} +2 -2
  7. package/dist/chunk-2OP4JKIQ.js +19 -0
  8. package/dist/chunk-2OP4JKIQ.js.map +1 -0
  9. package/dist/{chunk-5QC5KJHR.js → chunk-3PU2QWOJ.js} +2 -2
  10. package/dist/{chunk-Y7LZHPDF.js → chunk-4BHNP6Y2.js} +2 -2
  11. package/dist/{chunk-KOAGMTRI.js → chunk-6OYICQSJ.js} +47 -3
  12. package/dist/chunk-6OYICQSJ.js.map +1 -0
  13. package/dist/{chunk-WN4NEHE7.js → chunk-D4FVAIEJ.js} +5 -14
  14. package/dist/chunk-D4FVAIEJ.js.map +1 -0
  15. package/dist/{chunk-YWRQEDB3.js → chunk-GXVX4HUH.js} +46 -7
  16. package/dist/chunk-GXVX4HUH.js.map +1 -0
  17. package/dist/{chunk-J2SHWT2O.js → chunk-GYNRZCM6.js} +7 -2
  18. package/dist/{chunk-J2SHWT2O.js.map → chunk-GYNRZCM6.js.map} +1 -1
  19. package/dist/{chunk-VR246V3G.js → chunk-HBFMTM2E.js} +6 -6
  20. package/dist/chunk-HBFMTM2E.js.map +1 -0
  21. package/dist/{chunk-PSJQE6T6.js → chunk-I55YWV6T.js} +4 -4
  22. package/dist/{chunk-52TDNN7K.js → chunk-IDBMYDVH.js} +4 -4
  23. package/dist/{chunk-AYM2EFSK.js → chunk-JCITWZQR.js} +14 -14
  24. package/dist/chunk-JCITWZQR.js.map +1 -0
  25. package/dist/{chunk-CMQMNFFJ.js → chunk-JR67FZVC.js} +7 -3
  26. package/dist/chunk-JR67FZVC.js.map +1 -0
  27. package/dist/{chunk-MPRLMGJU.js → chunk-KKL3RYFQ.js} +2 -2
  28. package/dist/{chunk-2PWQ6MPG.js → chunk-KLVMJYZZ.js} +2 -2
  29. package/dist/{chunk-LPMQXS2N.js → chunk-LJPCMXR7.js} +1 -2
  30. package/dist/chunk-LJPCMXR7.js.map +1 -0
  31. package/dist/{chunk-6J25IIAI.js → chunk-NUIBQPB3.js} +187 -72
  32. package/dist/chunk-NUIBQPB3.js.map +1 -0
  33. package/dist/{chunk-M7HOQ4W7.js → chunk-PERP7LSS.js} +277 -21
  34. package/dist/chunk-PERP7LSS.js.map +1 -0
  35. package/dist/chunk-RHPDQW6D.js +29 -0
  36. package/dist/chunk-RHPDQW6D.js.map +1 -0
  37. package/dist/{chunk-F6OE6MGQ.js → chunk-SK44IFZN.js} +2 -2
  38. package/dist/cli/bin.js +31 -19
  39. package/dist/cli/bin.js.map +1 -1
  40. package/dist/cli/commands/audit.js +8 -8
  41. package/dist/configs/playwright-a11y.d.ts +1 -1
  42. package/dist/configs/playwright-a11y.js +1 -3
  43. package/dist/{convert-to-pnpm-6F4RAFEJ.js → convert-to-pnpm-OVMASATZ.js} +2 -2
  44. package/dist/{digest-GZCZYJKC.js → digest-LEMZS5ZK.js} +102 -10
  45. package/dist/digest-LEMZS5ZK.js.map +1 -0
  46. package/dist/{ensure-site-H5VMBPLF.js → ensure-site-3SOAWTZF.js} +2 -2
  47. package/dist/forms/index.d.ts +3 -2
  48. package/dist/forms/index.js +16 -9
  49. package/dist/forms/index.js.map +1 -1
  50. package/dist/{github-signals-SQECQNST.js → github-signals-TF3VSNIN.js} +5 -5
  51. package/dist/{health-endpoint-QXWM6GW2.js → health-endpoint-JZGYCMTV.js} +2 -2
  52. package/dist/index.d.ts +108 -22
  53. package/dist/index.js +127 -35
  54. package/dist/index.js.map +1 -1
  55. package/dist/{init-KQOIPLVT.js → init-7PTP4O2K.js} +5 -5
  56. package/dist/{launch-RX4VI7F2.js → launch-W33FWWOZ.js} +9 -9
  57. package/dist/{lighthouse-airtable-TTXULEGU.js → lighthouse-airtable-ZXNMDEPE.js} +3 -3
  58. package/dist/{onboard-TFBL62J3.js → onboard-PX36YDZA.js} +2 -2
  59. package/dist/{orchestrate-DYA4XAQJ.js → orchestrate-NYRYEKQM.js} +5 -5
  60. package/dist/{preflight-S3GYME3Q.js → preflight-JCQFKB6M.js} +7 -7
  61. package/dist/{renovate-dispatch-MZPAWVBL.js → renovate-dispatch-6II2JVOX.js} +29 -27
  62. package/dist/renovate-dispatch-6II2JVOX.js.map +1 -0
  63. package/dist/{report-BLZ67QAQ.js → report-POHQEV6F.js} +24 -8
  64. package/dist/report-POHQEV6F.js.map +1 -0
  65. package/dist/screenouts-7P5GOZMV.js +57 -0
  66. package/dist/screenouts-7P5GOZMV.js.map +1 -0
  67. package/dist/{self-updating-GSH427XP.js → self-updating-2YSVNMPP.js} +2 -2
  68. package/dist/{selftest-KPTL6KC4.js → selftest-OO2WDSDM.js} +12 -9
  69. package/dist/selftest-OO2WDSDM.js.map +1 -0
  70. package/dist/{smoke-suite-GP3W57JP.js → smoke-suite-CSP23NZM.js} +2 -2
  71. package/dist/spam-classifier-VG4ICIWS.js +217 -0
  72. package/dist/spam-classifier-VG4ICIWS.js.map +1 -0
  73. package/dist/submissions-BNRGY3JQ.js +122 -0
  74. package/dist/submissions-BNRGY3JQ.js.map +1 -0
  75. package/dist/submissions-MDKWJYMM.js +281 -0
  76. package/dist/submissions-MDKWJYMM.js.map +1 -0
  77. package/dist/{svelte-codemods-GYZZX742.js → svelte-codemods-V54KOBWU.js} +2 -2
  78. package/dist/{sync-configs-CQNOTJ2B.js → sync-configs-UBCJRASZ.js} +2 -2
  79. package/dist/{upgrade-RTAJZD56.js → upgrade-WR6ICPAG.js} +2 -2
  80. package/dist/{websites-SVI4KYSH.js → websites-U6642XSE.js} +10 -2
  81. package/dist/{write-audits-to-airtable-3XFNWMUE.js → write-audits-to-airtable-22T6D733.js} +4 -4
  82. package/package.json +1 -1
  83. package/dist/airtable-2PJ4NSAV.js +0 -9
  84. package/dist/blux-FKZYMKLF.js.map +0 -1
  85. package/dist/chunk-6J25IIAI.js.map +0 -1
  86. package/dist/chunk-AYM2EFSK.js.map +0 -1
  87. package/dist/chunk-CMQMNFFJ.js.map +0 -1
  88. package/dist/chunk-KOAGMTRI.js.map +0 -1
  89. package/dist/chunk-LPMQXS2N.js.map +0 -1
  90. package/dist/chunk-M7HOQ4W7.js.map +0 -1
  91. package/dist/chunk-QTHKOIFJ.js +0 -13
  92. package/dist/chunk-QTHKOIFJ.js.map +0 -1
  93. package/dist/chunk-VR246V3G.js.map +0 -1
  94. package/dist/chunk-WN4NEHE7.js.map +0 -1
  95. package/dist/chunk-YWRQEDB3.js.map +0 -1
  96. package/dist/digest-GZCZYJKC.js.map +0 -1
  97. package/dist/renovate-dispatch-MZPAWVBL.js.map +0 -1
  98. package/dist/report-BLZ67QAQ.js.map +0 -1
  99. package/dist/selftest-KPTL6KC4.js.map +0 -1
  100. /package/dist/{airtable-2PJ4NSAV.js.map → airtable-6L3JD7FY.js.map} +0 -0
  101. /package/dist/{announce-3JE5FKW7.js.map → announce-V6EYPPUR.js.map} +0 -0
  102. /package/dist/{bump-deps-K3JMLMBB.js.map → bump-deps-OQR3IXLN.js.map} +0 -0
  103. /package/dist/{chunk-5QC5KJHR.js.map → chunk-3PU2QWOJ.js.map} +0 -0
  104. /package/dist/{chunk-Y7LZHPDF.js.map → chunk-4BHNP6Y2.js.map} +0 -0
  105. /package/dist/{chunk-PSJQE6T6.js.map → chunk-I55YWV6T.js.map} +0 -0
  106. /package/dist/{chunk-52TDNN7K.js.map → chunk-IDBMYDVH.js.map} +0 -0
  107. /package/dist/{chunk-MPRLMGJU.js.map → chunk-KKL3RYFQ.js.map} +0 -0
  108. /package/dist/{chunk-2PWQ6MPG.js.map → chunk-KLVMJYZZ.js.map} +0 -0
  109. /package/dist/{chunk-F6OE6MGQ.js.map → chunk-SK44IFZN.js.map} +0 -0
  110. /package/dist/{convert-to-pnpm-6F4RAFEJ.js.map → convert-to-pnpm-OVMASATZ.js.map} +0 -0
  111. /package/dist/{ensure-site-H5VMBPLF.js.map → ensure-site-3SOAWTZF.js.map} +0 -0
  112. /package/dist/{github-signals-SQECQNST.js.map → github-signals-TF3VSNIN.js.map} +0 -0
  113. /package/dist/{health-endpoint-QXWM6GW2.js.map → health-endpoint-JZGYCMTV.js.map} +0 -0
  114. /package/dist/{init-KQOIPLVT.js.map → init-7PTP4O2K.js.map} +0 -0
  115. /package/dist/{launch-RX4VI7F2.js.map → launch-W33FWWOZ.js.map} +0 -0
  116. /package/dist/{lighthouse-airtable-TTXULEGU.js.map → lighthouse-airtable-ZXNMDEPE.js.map} +0 -0
  117. /package/dist/{onboard-TFBL62J3.js.map → onboard-PX36YDZA.js.map} +0 -0
  118. /package/dist/{orchestrate-DYA4XAQJ.js.map → orchestrate-NYRYEKQM.js.map} +0 -0
  119. /package/dist/{preflight-S3GYME3Q.js.map → preflight-JCQFKB6M.js.map} +0 -0
  120. /package/dist/{self-updating-GSH427XP.js.map → self-updating-2YSVNMPP.js.map} +0 -0
  121. /package/dist/{smoke-suite-GP3W57JP.js.map → smoke-suite-CSP23NZM.js.map} +0 -0
  122. /package/dist/{svelte-codemods-GYZZX742.js.map → svelte-codemods-V54KOBWU.js.map} +0 -0
  123. /package/dist/{sync-configs-CQNOTJ2B.js.map → sync-configs-UBCJRASZ.js.map} +0 -0
  124. /package/dist/{upgrade-RTAJZD56.js.map → upgrade-WR6ICPAG.js.map} +0 -0
  125. /package/dist/{websites-SVI4KYSH.js.map → websites-U6642XSE.js.map} +0 -0
  126. /package/dist/{write-audits-to-airtable-3XFNWMUE.js.map → write-audits-to-airtable-22T6D733.js.map} +0 -0
@@ -19,7 +19,7 @@ import {
19
19
  import {
20
20
  a11yRoutes,
21
21
  smokeRoutes
22
- } from "./chunk-LPMQXS2N.js";
22
+ } from "./chunk-LJPCMXR7.js";
23
23
 
24
24
  // src/audits/deps.ts
25
25
  import { readFile } from "fs/promises";
@@ -496,7 +496,10 @@ async function dependabotAudit(deps, repo, label) {
496
496
  let alerts;
497
497
  try {
498
498
  alerts = await deps.listAlerts(repo);
499
- } catch {
499
+ } catch (e) {
500
+ console.error(
501
+ `[security] ${label}: dependabot API failed \u2014 degraded to pnpm audit: ${e instanceof Error ? e.message : String(e)}`
502
+ );
500
503
  return null;
501
504
  }
502
505
  const counts = { low: 0, moderate: 0, high: 0, critical: 0 };
@@ -1116,6 +1119,67 @@ function parseHtmlLinks(html, baseUrl) {
1116
1119
  function family(pathname) {
1117
1120
  return pathname.split("/").filter(Boolean)[0] ?? "";
1118
1121
  }
1122
+ var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([
1123
+ "pdf",
1124
+ "doc",
1125
+ "docx",
1126
+ "xls",
1127
+ "xlsx",
1128
+ "ppt",
1129
+ "pptx",
1130
+ "csv",
1131
+ "txt",
1132
+ "rtf",
1133
+ "jpg",
1134
+ "jpeg",
1135
+ "png",
1136
+ "gif",
1137
+ "webp",
1138
+ "avif",
1139
+ "svg",
1140
+ "ico",
1141
+ "bmp",
1142
+ "tif",
1143
+ "tiff",
1144
+ "css",
1145
+ "js",
1146
+ "mjs",
1147
+ "map",
1148
+ "json",
1149
+ "xml",
1150
+ "webmanifest",
1151
+ "rss",
1152
+ "atom",
1153
+ "zip",
1154
+ "gz",
1155
+ "tar",
1156
+ "rar",
1157
+ "7z",
1158
+ "dmg",
1159
+ "exe",
1160
+ "mp3",
1161
+ "mp4",
1162
+ "webm",
1163
+ "mov",
1164
+ "avi",
1165
+ "wav",
1166
+ "ogg",
1167
+ "m4a",
1168
+ "m4v",
1169
+ "woff",
1170
+ "woff2",
1171
+ "ttf",
1172
+ "otf",
1173
+ "eot"
1174
+ ]);
1175
+ function isPageRoutePath(pathname) {
1176
+ const last = pathname.split("/").filter(Boolean).pop() ?? "";
1177
+ const dot = last.lastIndexOf(".");
1178
+ if (dot <= 0) return true;
1179
+ const ext = last.slice(dot + 1).toLowerCase();
1180
+ if (ext === "html" || ext === "htm") return true;
1181
+ return !ASSET_EXTENSIONS.has(ext);
1182
+ }
1119
1183
  function sampleRoutePaths(urlsOrPaths, cap = DEFAULT_CAP) {
1120
1184
  const seen = /* @__PURE__ */ new Set(["/"]);
1121
1185
  const buckets = /* @__PURE__ */ new Map();
@@ -1126,9 +1190,11 @@ function sampleRoutePaths(urlsOrPaths, cap = DEFAULT_CAP) {
1126
1190
  } catch {
1127
1191
  continue;
1128
1192
  }
1129
- if (pathname === "/") continue;
1193
+ pathname = pathname.replace(/\/+$/, "");
1194
+ if (pathname === "") continue;
1130
1195
  if (seen.has(pathname)) continue;
1131
1196
  seen.add(pathname);
1197
+ if (!isPageRoutePath(pathname)) continue;
1132
1198
  const fam = family(pathname);
1133
1199
  const arr = buckets.get(fam) ?? [];
1134
1200
  arr.push(pathname);
@@ -1180,32 +1246,174 @@ async function discoverRoutes(deployedUrl, deps, cap = DEFAULT_CAP) {
1180
1246
  function isBroken(status) {
1181
1247
  return status === null || status >= 400;
1182
1248
  }
1249
+ function isOkStatus(status) {
1250
+ return status !== null && status >= 200 && status < 400;
1251
+ }
1252
+ function decodeEntities(s) {
1253
+ return s.replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#0?39;|&apos;/gi, "'").replace(/&nbsp;/gi, " ");
1254
+ }
1255
+ function extractTitleMeta(html) {
1256
+ const t = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html);
1257
+ const title = t?.[1] ? decodeEntities(t[1]).trim() || null : null;
1258
+ let metaDescription = null;
1259
+ const metaTags = html.match(/<meta\b[^>]*>/gi) ?? [];
1260
+ for (const tag of metaTags) {
1261
+ if (!/\bname\s*=\s*["']description["']/i.test(tag)) continue;
1262
+ const content = /\bcontent\s*=\s*("([^"]*)"|'([^']*)')/i.exec(tag);
1263
+ const raw = content?.[2] ?? content?.[3] ?? "";
1264
+ metaDescription = decodeEntities(raw).trim() || null;
1265
+ break;
1266
+ }
1267
+ return { title, metaDescription };
1268
+ }
1269
+ var REVERIFY_RETRY_DELAYS_MS = [15e3, 45e3];
1270
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
1271
+ function isRetryableStatus(status) {
1272
+ return status === null || status === 403 || status === 408 || status === 429 || status >= 500;
1273
+ }
1274
+ async function fetchStatusesWithCooldown(urls, verify, sleep) {
1275
+ const byUrl = /* @__PURE__ */ new Map();
1276
+ for (const url of new Set(urls)) byUrl.set(url, await verify.fetchPage(url));
1277
+ for (const delayMs of REVERIFY_RETRY_DELAYS_MS) {
1278
+ const pending = [...byUrl.entries()].filter(([, f]) => !isOkStatus(f.status) && isRetryableStatus(f.status)).map(([u]) => u);
1279
+ if (pending.length === 0) break;
1280
+ await sleep(delayMs);
1281
+ for (const url of pending) byUrl.set(url, await verify.fetchPage(url));
1282
+ }
1283
+ return byUrl;
1284
+ }
1285
+ async function reverifyRoutes(routes, verify, sleep = defaultSleep) {
1286
+ const statusIdx = [];
1287
+ const titleMetaIdx = [];
1288
+ routes.forEach((r, i) => {
1289
+ if (!isOkStatus(r.status)) statusIdx.push(i);
1290
+ else if (!(r.title ?? "").trim() || !(r.metaDescription ?? "").trim()) titleMetaIdx.push(i);
1291
+ });
1292
+ const statusFetches = await fetchStatusesWithCooldown(
1293
+ statusIdx.map((i) => routes[i].url),
1294
+ verify,
1295
+ sleep
1296
+ );
1297
+ const fetchedByIdx = /* @__PURE__ */ new Map();
1298
+ for (const i of titleMetaIdx) fetchedByIdx.set(i, await verify.fetchPage(routes[i].url));
1299
+ const out = [...routes];
1300
+ const reverified = [];
1301
+ for (const i of statusIdx) {
1302
+ const r = routes[i];
1303
+ const fetched = statusFetches.get(r.url);
1304
+ reverified.push({ url: r.url, browserStatus: r.status, fetchedStatus: fetched.status });
1305
+ const ok = isOkStatus(fetched.status);
1306
+ out[i] = {
1307
+ ...r,
1308
+ status: fetched.status,
1309
+ title: ok ? fetched.title : null,
1310
+ metaDescription: ok ? fetched.metaDescription : null
1311
+ };
1312
+ }
1313
+ for (const i of titleMetaIdx) {
1314
+ const r = routes[i];
1315
+ const fetched = fetchedByIdx.get(i);
1316
+ if (!isOkStatus(fetched.status)) continue;
1317
+ out[i] = {
1318
+ ...r,
1319
+ title: (r.title ?? "").trim() ? r.title : fetched.title,
1320
+ metaDescription: (r.metaDescription ?? "").trim() ? r.metaDescription : fetched.metaDescription
1321
+ };
1322
+ }
1323
+ return { routes: out, reverified };
1324
+ }
1325
+ function excuseChallengedEngineChecks(routes) {
1326
+ const excused = [];
1327
+ const excusable = (e) => !e.ok && typeof e.status === "number" && !isOkStatus(e.status) && isRetryableStatus(e.status);
1328
+ const out = routes.map((route) => {
1329
+ if (!isOkStatus(route.status)) return route;
1330
+ const desktop = route.desktop.map((d) => {
1331
+ if (!excusable(d)) return d;
1332
+ excused.push(
1333
+ `${route.url} [${d.engine}]: browser saw ${d.status}, route re-verified reachable (${route.status})`
1334
+ );
1335
+ return { ...d, ok: true };
1336
+ });
1337
+ const mobile = route.mobile.map((m) => {
1338
+ if (!excusable(m)) return m;
1339
+ excused.push(
1340
+ `${route.url} [${m.device}]: browser saw ${m.status}, route re-verified reachable (${route.status})`
1341
+ );
1342
+ return { ...m, ok: true };
1343
+ });
1344
+ return { ...route, desktop, mobile };
1345
+ });
1346
+ return { routes: out, excused };
1347
+ }
1348
+ async function reverifyLinks(links, verify, sleep = defaultSleep) {
1349
+ const suspects = links.filter((l) => isBroken(l.status) && isRetryableStatus(l.status));
1350
+ if (suspects.length === 0) return { links, reverified: [] };
1351
+ const fetched = await fetchStatusesWithCooldown(
1352
+ suspects.map((l) => l.url),
1353
+ verify,
1354
+ sleep
1355
+ );
1356
+ const suspectUrls = new Set(suspects.map((l) => l.url));
1357
+ const reverified = [];
1358
+ const out = links.map((l) => {
1359
+ if (!suspectUrls.has(l.url)) return l;
1360
+ const finalStatus = fetched.get(l.url).status;
1361
+ reverified.push({ url: l.url, firstStatus: l.status, fetchedStatus: finalStatus });
1362
+ return { url: l.url, status: finalStatus };
1363
+ });
1364
+ return { links: out, reverified };
1365
+ }
1366
+ var fmtEntryStatus = (s) => typeof s === "number" && !isOkStatus(s) ? ` \u2192 ${s}` : "";
1183
1367
  function summarizeBrowser(routes, links, familyCounts) {
1184
1368
  const desktopChecks = routes.flatMap((r) => r.desktop);
1185
1369
  const mobileChecks = routes.flatMap((r) => r.mobile);
1186
1370
  const desktopOk = routes.length > 0 && routes.every((r) => r.desktop.length > 0 && r.desktop.every((d) => d.ok));
1187
1371
  const mobileOk = routes.length > 0 && routes.every((r) => r.mobile.length > 0 && r.mobile.every((m) => m.ok));
1188
- const brokenLinks = links.filter((l) => isBroken(l.status)).length;
1372
+ const desktopFailures = routes.flatMap(
1373
+ (r) => r.desktop.length === 0 ? [`${r.url}: no desktop observations`] : r.desktop.filter((d) => !d.ok).map((d) => `${r.url} [${d.engine}]${fmtEntryStatus(d.status)}`)
1374
+ );
1375
+ const mobileFailures = routes.flatMap(
1376
+ (r) => r.mobile.length === 0 ? [`${r.url}: no mobile observations`] : r.mobile.filter((m) => !m.ok).map((m) => `${r.url} [${m.device}]${fmtEntryStatus(m.status)}`)
1377
+ );
1378
+ const brokenLinkUrls = links.filter((l) => isBroken(l.status)).map((l) => `${l.url} \u2192 ${l.status ?? "no response"}`);
1379
+ const brokenLinks = brokenLinkUrls.length;
1189
1380
  const linksOk = links.length > 0 && brokenLinks === 0;
1190
- const reachableOk = routes.length > 0 && routes.every((r) => r.status !== null && r.status >= 200 && r.status < 400);
1191
- const trimmedTitles = routes.map((r) => (r.title ?? "").trim());
1192
- const eachTitleMetaValid = routes.length > 0 && routes.every((r, i) => {
1193
- const t = trimmedTitles[i];
1381
+ const unreachableUrls = routes.filter((r) => !isOkStatus(r.status)).map((r) => `${r.url} \u2192 ${r.status ?? "no response"}`);
1382
+ const reachableOk = routes.length > 0 && unreachableUrls.length === 0;
1383
+ const titleMetaProblems = [];
1384
+ for (const r of routes) {
1385
+ const t = (r.title ?? "").trim();
1194
1386
  const desc = (r.metaDescription ?? "").trim();
1195
- return t.length > 0 && t.length <= 70 && desc.length > 0;
1196
- });
1197
- const noDuplicateTitles = new Set(trimmedTitles).size === trimmedTitles.length;
1198
- const titleMetaOk = eachTitleMetaValid && noDuplicateTitles;
1387
+ if (t.length === 0) titleMetaProblems.push(`${r.url}: empty title`);
1388
+ else if (t.length > 70) titleMetaProblems.push(`${r.url}: title ${t.length} chars (max 70)`);
1389
+ if (desc.length === 0) titleMetaProblems.push(`${r.url}: missing meta description`);
1390
+ }
1391
+ const byTitle = /* @__PURE__ */ new Map();
1392
+ for (const r of routes) {
1393
+ const t = (r.title ?? "").trim();
1394
+ if (t.length === 0) continue;
1395
+ byTitle.set(t, [...byTitle.get(t) ?? [], r.url]);
1396
+ }
1397
+ for (const [t, urls] of byTitle) {
1398
+ if (urls.length > 1) titleMetaProblems.push(`duplicate title "${t}": ${urls.join(" + ")}`);
1399
+ }
1400
+ const titleMetaOk = routes.length > 0 && titleMetaProblems.length === 0;
1199
1401
  const engines = [...new Set(desktopChecks.map((d) => d.engine))];
1200
1402
  const devices = [...new Set(mobileChecks.map((m) => m.device))];
1201
1403
  const families = Object.entries(familyCounts).map(([f, n]) => f === "/" ? "/" : `${f} \xD7${n}`).join(", ");
1202
- const note = `${routes.length} routes (${families}); desktop ${engines.join("/") || "\u2014"}; mobile ${devices.join("/") || "\u2014"}; ${links.length} links, ${brokenLinks} broken`;
1404
+ const firstOf = (xs, n) => xs.slice(0, n).join("; ") + (xs.length > n ? ` (+${xs.length - n} more)` : "");
1405
+ const note = `${routes.length} routes (${families}); desktop ${engines.join("/") || "\u2014"}; mobile ${devices.join("/") || "\u2014"}; ${links.length} links, ${brokenLinks} broken` + (unreachableUrls.length > 0 ? `; unreachable: ${firstOf(unreachableUrls, 3)}` : "") + (titleMetaProblems.length > 0 ? `; title/meta: ${firstOf(titleMetaProblems, 3)}` : "") + (desktopFailures.length > 0 ? `; desktop failing: ${firstOf(desktopFailures, 3)}` : "") + (mobileFailures.length > 0 ? `; mobile failing: ${firstOf(mobileFailures, 3)}` : "") + (brokenLinkUrls.length > 0 ? `; broken: ${firstOf(brokenLinkUrls, 3)}` : "");
1203
1406
  return {
1204
1407
  desktopOk,
1205
1408
  mobileOk,
1206
1409
  linksOk,
1207
1410
  reachableOk,
1208
1411
  titleMetaOk,
1412
+ unreachableUrls,
1413
+ titleMetaProblems,
1414
+ desktopFailures,
1415
+ mobileFailures,
1416
+ brokenLinkUrls,
1209
1417
  brokenLinks,
1210
1418
  routesChecked: routes.length,
1211
1419
  note
@@ -1219,24 +1427,47 @@ async function browserAudit(ctx) {
1219
1427
  }
1220
1428
  const now = ctx.now ?? /* @__PURE__ */ new Date();
1221
1429
  const discoverDeps = ctx.discoverDeps ?? defaultDiscoverDeps();
1430
+ const verifyDeps = ctx.verifyDeps ?? defaultVerifyDeps();
1222
1431
  const runner = ctx.browserRunner ?? await defaultBrowserRunner();
1223
1432
  try {
1224
1433
  const discovered = await discoverRoutes(site.deployedUrl, discoverDeps);
1225
- const routeResults = await runner.probe(discovered.routes);
1434
+ const probed = await runner.probe(discovered.routes);
1435
+ const { routes: reverifiedRoutes, reverified } = await reverifyRoutes(probed, verifyDeps);
1436
+ const { routes: routeResults, excused } = excuseChallengedEngineChecks(reverifiedRoutes);
1226
1437
  const internalLinks = [...new Set(routeResults.flatMap((r) => r.links))];
1227
- const linkResults = await runner.checkLinks(internalLinks);
1438
+ const rawLinks = await runner.checkLinks(internalLinks);
1439
+ const { links: linkResults, reverified: linkReverified } = await reverifyLinks(
1440
+ rawLinks,
1441
+ verifyDeps
1442
+ );
1228
1443
  const summary = summarizeBrowser(
1229
1444
  routeResults,
1230
1445
  linkResults,
1231
1446
  discovered.familyCounts ?? familyCountsOf(discovered.routes)
1232
1447
  );
1448
+ const cleared = reverified.filter((v) => isOkStatus(v.fetchedStatus));
1449
+ const reverifiedUrls = cleared.map(
1450
+ (v) => `${v.url}: browser saw ${v.browserStatus ?? "no response"}, plain fetch ${v.fetchedStatus}`
1451
+ );
1452
+ const clearedLinks = linkReverified.filter((v) => isOkStatus(v.fetchedStatus));
1453
+ const linkReverifiedUrls = clearedLinks.map(
1454
+ (v) => `${v.url}: first check saw ${v.firstStatus ?? "no response"}, plain re-fetch ${v.fetchedStatus}`
1455
+ );
1456
+ const note = summary.note + (cleared.length > 0 ? `; ${cleared.length} route(s) re-verified reachable by plain fetch after a browser-side challenge/flake` : "") + (excused.length > 0 ? `; ${excused.length} engine/device check(s) excused (challenge on a re-verified-reachable route)` : "") + (clearedLinks.length > 0 ? `; ${clearedLinks.length} link(s) re-verified ok by plain fetch after cooldown` : "");
1233
1457
  const status = summary.desktopOk && summary.mobileOk && summary.linksOk ? "pass" : "warn";
1234
1458
  return {
1235
1459
  audit: "browser",
1236
1460
  site: label,
1237
1461
  status,
1238
- summary: summary.note,
1239
- details: { ...summary, checkedAt: now.toISOString() }
1462
+ summary: note,
1463
+ details: {
1464
+ ...summary,
1465
+ note,
1466
+ reverifiedUrls,
1467
+ excusedEngineChecks: excused,
1468
+ linkReverifiedUrls,
1469
+ checkedAt: now.toISOString()
1470
+ }
1240
1471
  };
1241
1472
  } finally {
1242
1473
  await runner.close?.();
@@ -1259,6 +1490,25 @@ function defaultDiscoverDeps() {
1259
1490
  }
1260
1491
  };
1261
1492
  }
1493
+ function defaultVerifyDeps() {
1494
+ return {
1495
+ fetchPage: async (url) => {
1496
+ try {
1497
+ const res = await fetch(url, {
1498
+ redirect: "follow",
1499
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
1500
+ });
1501
+ const isHtml = (res.headers.get("content-type") ?? "").includes("html");
1502
+ if (!res.ok || !isHtml) {
1503
+ return { status: res.status, title: null, metaDescription: null };
1504
+ }
1505
+ return { status: res.status, ...extractTitleMeta(await res.text()) };
1506
+ } catch {
1507
+ return { status: null, title: null, metaDescription: null };
1508
+ }
1509
+ }
1510
+ };
1511
+ }
1262
1512
  var DESKTOP_VIEWPORT = { width: 1366, height: 900 };
1263
1513
  var PAGE_TIMEOUT_MS = 3e4;
1264
1514
  async function defaultBrowserRunner() {
@@ -1292,11 +1542,13 @@ async function defaultBrowserRunner() {
1292
1542
  const errors = [];
1293
1543
  page.on("pageerror", (e) => errors.push(String(e)));
1294
1544
  let ok = false;
1545
+ let navStatus = null;
1295
1546
  try {
1296
1547
  const resp = await page.goto(url, {
1297
1548
  waitUntil: "domcontentloaded",
1298
1549
  timeout: PAGE_TIMEOUT_MS
1299
1550
  });
1551
+ navStatus = resp ? resp.status() : null;
1300
1552
  const hasMain = await page.locator("main, [role=main]").first().isVisible().catch(() => false);
1301
1553
  ok = !!resp && resp.ok() && errors.length === 0 && hasMain;
1302
1554
  if (engine === "chromium") {
@@ -1320,7 +1572,7 @@ async function defaultBrowserRunner() {
1320
1572
  await ctx.close().catch(() => {
1321
1573
  });
1322
1574
  }
1323
- desktop.push({ engine, ok });
1575
+ desktop.push({ engine, ok, status: navStatus });
1324
1576
  }
1325
1577
  const mobile = [];
1326
1578
  for (let i = 0; i < mobileTargets.length; i++) {
@@ -1331,11 +1583,13 @@ async function defaultBrowserRunner() {
1331
1583
  const errors = [];
1332
1584
  page.on("pageerror", (e) => errors.push(String(e)));
1333
1585
  let ok = false;
1586
+ let navStatus = null;
1334
1587
  try {
1335
1588
  const resp = await page.goto(url, {
1336
1589
  waitUntil: "domcontentloaded",
1337
1590
  timeout: PAGE_TIMEOUT_MS
1338
1591
  });
1592
+ navStatus = resp ? resp.status() : null;
1339
1593
  const overflow = await page.evaluate("document.documentElement.scrollWidth > window.innerWidth + 2").catch(() => true);
1340
1594
  ok = !!resp && resp.ok() && errors.length === 0 && !overflow;
1341
1595
  } catch {
@@ -1344,7 +1598,7 @@ async function defaultBrowserRunner() {
1344
1598
  await ctx.close().catch(() => {
1345
1599
  });
1346
1600
  }
1347
- mobile.push({ device, ok });
1601
+ mobile.push({ device, ok, status: navStatus });
1348
1602
  }
1349
1603
  results.push({
1350
1604
  url,
@@ -1391,6 +1645,7 @@ async function defaultBrowserRunner() {
1391
1645
  }
1392
1646
 
1393
1647
  // src/audits/netlify-deploy.ts
1648
+ var NETLIFY_FETCH_TIMEOUT_MS = 15e3;
1394
1649
  var READY_STATES = /* @__PURE__ */ new Set(["ready"]);
1395
1650
  var FAILED_STATES = /* @__PURE__ */ new Set(["error", "failed", "rejected"]);
1396
1651
  function isReadyState(state) {
@@ -1415,7 +1670,8 @@ function defaultNetlifyDeployDeps(token, now, fetchImpl = fetch) {
1415
1670
  let res;
1416
1671
  try {
1417
1672
  res = await fetchImpl(url, {
1418
- headers: { Authorization: `Bearer ${token}` }
1673
+ headers: { Authorization: `Bearer ${token}` },
1674
+ signal: AbortSignal.timeout(NETLIFY_FETCH_TIMEOUT_MS)
1419
1675
  });
1420
1676
  } catch {
1421
1677
  return { ok: false };
@@ -1890,4 +2146,4 @@ export {
1890
2146
  runAudits,
1891
2147
  runAuditsAcross
1892
2148
  };
1893
- //# sourceMappingURL=chunk-M7HOQ4W7.js.map
2149
+ //# sourceMappingURL=chunk-PERP7LSS.js.map