@goflag/cli 0.2.13 → 0.2.15

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.
@@ -25,6 +25,25 @@ function fingerprint(category, ...parts) {
25
25
  return `${category}-${digest}`;
26
26
  }
27
27
 
28
+ // src/lib/core/canonicalise.ts
29
+ function canonicaliseUrl(href, base, options = {}) {
30
+ let url;
31
+ try {
32
+ url = new URL(href, base);
33
+ } catch {
34
+ return null;
35
+ }
36
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
37
+ url.hash = "";
38
+ if (!options.preserveTrailingSlash && url.pathname.length > 1 && url.pathname.endsWith("/")) {
39
+ url.pathname = url.pathname.slice(0, -1);
40
+ }
41
+ return url.toString();
42
+ }
43
+
44
+ // src/lib/core/inspect.ts
45
+ import * as cheerio3 from "cheerio";
46
+
28
47
  // src/lib/core/extract/static.ts
29
48
  import * as cheerio from "cheerio";
30
49
 
@@ -105,7 +124,9 @@ function decodeHtmlEntities(s) {
105
124
 
106
125
  // src/lib/core/extract/static.ts
107
126
  function extractStatic(html, options) {
108
- const $ = cheerio.load(html);
127
+ return extractStaticFrom(cheerio.load(html), options);
128
+ }
129
+ function extractStaticFrom($, options) {
109
130
  const base = resolveBase($, options.baseUrl);
110
131
  const raw = extractRaw($);
111
132
  const meta = parseGenericMeta(raw, base);
@@ -233,8 +254,8 @@ function parseGenericMeta(raw, base) {
233
254
  for (const l of raw.links) {
234
255
  const rel = l.rel?.toLowerCase();
235
256
  if (rel === "canonical" && l.href) {
236
- const resolved = safeResolve(l.href, base);
237
- meta.canonical = sourced(resolved, { kind: "link", rel: "canonical" }, l.href);
257
+ const resolved2 = safeResolve(l.href, base);
258
+ meta.canonical = sourced(resolved2, { kind: "link", rel: "canonical" }, l.href);
238
259
  }
239
260
  }
240
261
  return meta;
@@ -952,12 +973,98 @@ function countEntries(body) {
952
973
  return url + sm;
953
974
  }
954
975
 
976
+ // src/lib/core/links/extract.ts
977
+ import * as cheerio2 from "cheerio";
978
+ var ANCHOR_SPEC = { selector: "a[href]", attr: "href", source: "a" };
979
+ var ASSET_SPECS = [
980
+ { selector: "img[src]", attr: "src", source: "img" },
981
+ { selector: "script[src]", attr: "src", source: "script" },
982
+ { selector: "link[href]", attr: "href", source: "link" },
983
+ { selector: "iframe[src]", attr: "src", source: "iframe" }
984
+ ];
985
+ function extractLinks(html, options) {
986
+ return extractLinksFrom(cheerio2.load(html), options);
987
+ }
988
+ function extractLinksFrom($, options) {
989
+ const base = resolveBase2($, options.baseUrl);
990
+ const origin = safeOrigin(options.baseUrl);
991
+ const specs = options.includeAssets ? [ANCHOR_SPEC, ...ASSET_SPECS] : [ANCHOR_SPEC];
992
+ const out = [];
993
+ const seen = /* @__PURE__ */ new Set();
994
+ for (const spec of specs) {
995
+ $(spec.selector).each((_, el) => {
996
+ const raw = $(el).attr(spec.attr);
997
+ if (raw === void 0) return;
998
+ const rawHref = raw.trim();
999
+ if (!rawHref) return;
1000
+ const ref = buildRef($, el, rawHref, spec, base, origin);
1001
+ if (!ref) return;
1002
+ const dedupeKey = `${ref.source}|${ref.url}`;
1003
+ if (seen.has(dedupeKey)) return;
1004
+ seen.add(dedupeKey);
1005
+ out.push(ref);
1006
+ });
1007
+ }
1008
+ return out;
1009
+ }
1010
+ function buildRef($, el, rawHref, spec, base, origin) {
1011
+ const fragment = extractFragment(rawHref);
1012
+ const canonical = canonicaliseUrl(rawHref, base, { preserveTrailingSlash: true });
1013
+ const url = canonical ?? rawHref;
1014
+ const kind = canonical ? classify(canonical, origin) : "external";
1015
+ const ref = {
1016
+ rawHref,
1017
+ url,
1018
+ kind,
1019
+ source: spec.source,
1020
+ rel: spec.source === "a" || spec.source === "link" ? parseRel($(el).attr("rel")) : []
1021
+ };
1022
+ if (spec.source === "a") {
1023
+ const text = $(el).text().trim();
1024
+ if (text) ref.anchorText = text;
1025
+ }
1026
+ if (fragment) ref.fragment = fragment;
1027
+ return ref;
1028
+ }
1029
+ function extractFragment(rawHref) {
1030
+ const hash = rawHref.indexOf("#");
1031
+ if (hash === -1) return void 0;
1032
+ const frag = rawHref.slice(hash);
1033
+ return frag.length > 1 ? frag : void 0;
1034
+ }
1035
+ function parseRel(rel) {
1036
+ if (!rel) return [];
1037
+ return rel.split(/\s+/).map((t) => t.trim().toLowerCase()).filter(Boolean);
1038
+ }
1039
+ function classify(url, origin) {
1040
+ if (!origin) return "external";
1041
+ return safeOrigin(url) === origin ? "internal" : "external";
1042
+ }
1043
+ function safeOrigin(url) {
1044
+ try {
1045
+ return new URL(url).origin;
1046
+ } catch {
1047
+ return null;
1048
+ }
1049
+ }
1050
+ function resolveBase2($, pageUrl) {
1051
+ const declared = $("base[href]").first().attr("href")?.trim();
1052
+ if (declared) {
1053
+ try {
1054
+ return new URL(declared, pageUrl).toString();
1055
+ } catch {
1056
+ }
1057
+ }
1058
+ return pageUrl;
1059
+ }
1060
+
955
1061
  // src/lib/core/inspect.ts
956
1062
  async function inspect(url, options = {}) {
957
1063
  const {
958
1064
  probes: enableProbes = true,
959
1065
  mode = "auto",
960
1066
  headless: headlessOpts,
1067
+ linkRefs: wantLinkRefs = false,
961
1068
  ...fetchOptions
962
1069
  } = options;
963
1070
  const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -971,7 +1078,8 @@ async function inspect(url, options = {}) {
971
1078
  });
972
1079
  }
973
1080
  const { meta: staticMeta, body: staticBody } = await fetchStatic(url, fetchOptions);
974
- const staticParsed = extractStatic(staticBody, { baseUrl: staticMeta.finalUrl });
1081
+ const { staticParsed, linkRefs } = parseStatic(staticBody, staticMeta.finalUrl, wantLinkRefs);
1082
+ const withLinkRefs = linkRefs ? { linkRefs } : {};
975
1083
  if (mode === "static") {
976
1084
  const probes2 = await runProbes({
977
1085
  enabled: enableProbes,
@@ -985,7 +1093,8 @@ async function inspect(url, options = {}) {
985
1093
  fetch: staticMeta,
986
1094
  extractor: { mode: "static", escalated: false },
987
1095
  html: { static: staticBody },
988
- probes: probes2
1096
+ probes: probes2,
1097
+ ...withLinkRefs
989
1098
  };
990
1099
  }
991
1100
  const heuristic = looksClientRendered(staticParsed);
@@ -1002,7 +1111,8 @@ async function inspect(url, options = {}) {
1002
1111
  fetch: staticMeta,
1003
1112
  extractor: { mode: "static", escalated: false },
1004
1113
  html: { static: staticBody },
1005
- probes: probes2
1114
+ probes: probes2,
1115
+ ...withLinkRefs
1006
1116
  };
1007
1117
  }
1008
1118
  let renderedBody;
@@ -1034,7 +1144,8 @@ async function inspect(url, options = {}) {
1034
1144
  escalationBlocked: err.message
1035
1145
  },
1036
1146
  html: { static: staticBody },
1037
- probes: probes2
1147
+ probes: probes2,
1148
+ ...withLinkRefs
1038
1149
  };
1039
1150
  }
1040
1151
  const renderedParsed = extractStatic(renderedBody, { baseUrl: renderedFetch.finalUrl });
@@ -1056,9 +1167,16 @@ async function inspect(url, options = {}) {
1056
1167
  },
1057
1168
  html: { static: staticBody, rendered: renderedBody },
1058
1169
  hydration,
1059
- probes
1170
+ probes,
1171
+ ...withLinkRefs
1060
1172
  };
1061
1173
  }
1174
+ function parseStatic(html, baseUrl, wantLinkRefs) {
1175
+ const $ = cheerio3.load(html);
1176
+ const staticParsed = extractStaticFrom($, { baseUrl });
1177
+ if (!wantLinkRefs) return { staticParsed };
1178
+ return { staticParsed, linkRefs: extractLinksFrom($, { baseUrl }) };
1179
+ }
1062
1180
  async function inspectHeadlessOnly(args) {
1063
1181
  const { url, fetchedAt, enableProbes, fetchOptions, headlessOpts } = args;
1064
1182
  const result = await extractHeadless(url, {
@@ -1209,60 +1327,74 @@ async function crawl(options) {
1209
1327
  const recovered = [];
1210
1328
  let truncated = false;
1211
1329
  const seenFinal = /* @__PURE__ */ new Set();
1212
- while (queue.length > 0) {
1330
+ const inspectItem = async (item) => {
1331
+ try {
1332
+ const page = await inspect(item.url, options.inspectOptions);
1333
+ return { kind: "ok", item, page };
1334
+ } catch (err) {
1335
+ return { kind: "err", item, error: err instanceof Error ? err.message : String(err) };
1336
+ }
1337
+ };
1338
+ const pending = [];
1339
+ const maxPending = concurrency * 4;
1340
+ let active = 0;
1341
+ let requestDone = () => void 0;
1342
+ const start = (item) => {
1343
+ active += 1;
1344
+ pending.push(
1345
+ inspectItem(item).finally(() => {
1346
+ active -= 1;
1347
+ requestDone();
1348
+ })
1349
+ );
1350
+ };
1351
+ while (queue.length > 0 || pending.length > 0) {
1213
1352
  if (options.inspectOptions?.signal?.aborted) break;
1214
- const wave = queue.splice(0, concurrency);
1215
- const inspections = wave.map(async (item) => {
1216
- try {
1217
- const page = await inspect(item.url, options.inspectOptions);
1218
- return { kind: "ok", item, page };
1219
- } catch (err) {
1220
- return {
1221
- kind: "err",
1222
- item,
1223
- error: err instanceof Error ? err.message : String(err)
1224
- };
1225
- }
1353
+ while (active < concurrency && pending.length < maxPending && queue.length > 0) {
1354
+ start(queue.shift());
1355
+ }
1356
+ const oldest = pending[0];
1357
+ const finished = new Promise((resolve3) => {
1358
+ requestDone = resolve3;
1226
1359
  });
1227
- const results = await Promise.all(inspections);
1228
- for (const result of results) {
1229
- if (result.kind === "err") {
1230
- const attempt = (result.item.attempt ?? 0) + 1;
1231
- if (attempt < MAX_ATTEMPTS) {
1232
- queue.push({ ...result.item, attempt });
1233
- continue;
1234
- }
1235
- errors.push({ url: result.item.url, message: result.error });
1360
+ const oldestReady = await Promise.race([oldest.then(() => true), finished.then(() => false)]);
1361
+ if (!oldestReady) continue;
1362
+ const result = await pending.shift();
1363
+ if (result.kind === "err") {
1364
+ const attempt = (result.item.attempt ?? 0) + 1;
1365
+ if (attempt < MAX_ATTEMPTS) {
1366
+ queue.push({ ...result.item, attempt });
1236
1367
  continue;
1237
1368
  }
1238
- if (result.item.attempt) recovered.push(result.item.url);
1239
- const finalUrl = canonicaliseUrl(result.page.fetch.finalUrl) ?? result.page.fetch.finalUrl;
1240
- if (seenFinal.has(finalUrl)) continue;
1241
- seenFinal.add(finalUrl);
1242
- pages.push(result.page);
1243
- if (options.onPage) {
1244
- options.onPage(result.page, { visited: pages.length, queued: queue.length });
1245
- }
1246
- if (pages.length >= maxPages) {
1247
- truncated = true;
1248
- break;
1249
- }
1250
- if (result.item.depth >= depthLimit) continue;
1251
- const candidates = extractCandidateLinks(result.page);
1252
- for (const candidate of candidates) {
1253
- const normalised = canonicaliseUrl(candidate.href, result.page.fetch.finalUrl);
1254
- if (!normalised) continue;
1255
- if (visitedSet.has(normalised)) continue;
1256
- const parsed = new URL(normalised);
1257
- if (parsed.origin !== origin) continue;
1258
- const passesIncludeExclude = filterAllows(parsed.pathname, include, exclude);
1259
- const isHreflang = candidate.source === "hreflang";
1260
- if (!passesIncludeExclude && !(followHreflang && isHreflang)) continue;
1261
- visitedSet.add(normalised);
1262
- queue.push({ url: normalised, depth: result.item.depth + 1 });
1263
- }
1369
+ errors.push({ url: result.item.url, message: result.error });
1370
+ continue;
1371
+ }
1372
+ if (result.item.attempt) recovered.push(result.item.url);
1373
+ const finalUrl = canonicaliseUrl(result.page.fetch.finalUrl) ?? result.page.fetch.finalUrl;
1374
+ if (seenFinal.has(finalUrl)) continue;
1375
+ seenFinal.add(finalUrl);
1376
+ pages.push(result.page);
1377
+ if (options.onPage) {
1378
+ options.onPage(result.page, { visited: pages.length, queued: queue.length });
1379
+ }
1380
+ if (pages.length >= maxPages) {
1381
+ truncated = true;
1382
+ break;
1383
+ }
1384
+ if (result.item.depth >= depthLimit) continue;
1385
+ const candidates = extractCandidateLinks(result.page);
1386
+ for (const candidate of candidates) {
1387
+ const normalised = canonicaliseUrl(candidate.href, result.page.fetch.finalUrl);
1388
+ if (!normalised) continue;
1389
+ if (visitedSet.has(normalised)) continue;
1390
+ const parsed = new URL(normalised);
1391
+ if (parsed.origin !== origin) continue;
1392
+ const passesIncludeExclude = filterAllows(parsed.pathname, include, exclude);
1393
+ const isHreflang = candidate.source === "hreflang";
1394
+ if (!passesIncludeExclude && !(followHreflang && isHreflang)) continue;
1395
+ visitedSet.add(normalised);
1396
+ queue.push({ url: normalised, depth: result.item.depth + 1 });
1264
1397
  }
1265
- if (truncated) break;
1266
1398
  }
1267
1399
  return {
1268
1400
  pages,
@@ -1277,20 +1409,6 @@ function filterAllows(pathname, include, exclude) {
1277
1409
  if (include.length === 0) return true;
1278
1410
  return matchesAny(pathname, include);
1279
1411
  }
1280
- function canonicaliseUrl(href, base, options = {}) {
1281
- let url;
1282
- try {
1283
- url = new URL(href, base);
1284
- } catch {
1285
- return null;
1286
- }
1287
- if (url.protocol !== "http:" && url.protocol !== "https:") return null;
1288
- url.hash = "";
1289
- if (!options.preserveTrailingSlash && url.pathname.length > 1 && url.pathname.endsWith("/")) {
1290
- url.pathname = url.pathname.slice(0, -1);
1291
- }
1292
- return url.toString();
1293
- }
1294
1412
 
1295
1413
  // src/lib/rules/evaluate.ts
1296
1414
  function bandFor(value, bands) {
@@ -1435,14 +1553,14 @@ function normalise(value) {
1435
1553
  return value.trim().toLowerCase().split(/\s+/).sort().join(" ");
1436
1554
  }
1437
1555
  function sameAsset(href, src) {
1438
- const pathOf2 = (value) => {
1556
+ const pathOf3 = (value) => {
1439
1557
  try {
1440
1558
  return new URL(value, "https://goflag.invalid").pathname;
1441
1559
  } catch {
1442
1560
  return value.split("?")[0] ?? value;
1443
1561
  }
1444
1562
  };
1445
- return pathOf2(href) === pathOf2(src);
1563
+ return pathOf3(href) === pathOf3(src);
1446
1564
  }
1447
1565
  function hasOpenGraph(ex) {
1448
1566
  return Boolean(
@@ -2765,6 +2883,41 @@ function sortLocales(locales) {
2765
2883
  return [...xDefault, ...rest];
2766
2884
  }
2767
2885
 
2886
+ // src/lib/core/probes/not-found.ts
2887
+ import { randomBytes } from "crypto";
2888
+ async function probeNotFound(origin, options = {}) {
2889
+ const nonce = options.nonce ?? randomBytes(16).toString("hex");
2890
+ const base = `/goflag-probe-${nonce}`;
2891
+ return Promise.all([
2892
+ ask(new URL(base, origin).toString(), "bare", options),
2893
+ ask(new URL(`${base}.txt`, origin).toString(), "dotted", options)
2894
+ ]);
2895
+ }
2896
+ async function ask(url, shape, options) {
2897
+ const { signal, cleanup } = combineSignals(options.signal, options.timeoutMs ?? 5e3);
2898
+ try {
2899
+ let res = await fetch(url, { method: "HEAD", signal, redirect: "follow" });
2900
+ if (!isNotFound(res.status)) {
2901
+ res = await fetch(url, { signal, redirect: "follow" });
2902
+ await res.body?.cancel().catch(() => void 0);
2903
+ }
2904
+ return {
2905
+ url,
2906
+ shape,
2907
+ status: res.status,
2908
+ finalUrl: res.url || url,
2909
+ contentType: res.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() || void 0
2910
+ };
2911
+ } catch {
2912
+ return { url, shape, status: 0, finalUrl: url };
2913
+ } finally {
2914
+ cleanup();
2915
+ }
2916
+ }
2917
+ function isNotFound(status) {
2918
+ return status === 404 || status === 410;
2919
+ }
2920
+
2768
2921
  // src/lib/core/robots/match.ts
2769
2922
  function robotsAllows(groups, path, userAgent = "*") {
2770
2923
  const group = selectGroup(groups, userAgent);
@@ -2829,6 +2982,180 @@ function escapeRegExp(value) {
2829
2982
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2830
2983
  }
2831
2984
 
2985
+ // src/lib/rules/site-shared.ts
2986
+ var SAMPLE = 5;
2987
+ function sample(locs) {
2988
+ const shown = locs.slice(0, SAMPLE);
2989
+ const rest = locs.length - shown.length;
2990
+ return `${shown.map((l) => `\`${l}\``).join(", ")}${rest > 0 ? `, and ${rest} more` : ""}`;
2991
+ }
2992
+ function isDead(status) {
2993
+ return status === 0 || status >= 400;
2994
+ }
2995
+
2996
+ // src/lib/rules/llms-txt.ts
2997
+ var TEXT_TYPES = /* @__PURE__ */ new Set(["text/plain", "text/markdown", "text/x-markdown"]);
2998
+ function readable(site) {
2999
+ return site.llmsTxt?.found === true && !site.llmsTxt.html;
3000
+ }
3001
+ function explainedByCatchAll(site, status) {
3002
+ const dotted = site.notFound?.find((probe) => probe.shape === "dotted");
3003
+ if (!dotted) return false;
3004
+ const klass = (code) => Math.floor(code / 100);
3005
+ return (klass(status) === 2 || klass(status) === 5) && klass(dotted.status) === klass(status);
3006
+ }
3007
+ function sameOriginLinks(site) {
3008
+ const seen = /* @__PURE__ */ new Set();
3009
+ const out = [];
3010
+ for (const link of site.llmsTxt?.links ?? []) {
3011
+ if (seen.has(link.url) || !sameOrigin2(link.url, site.origin)) continue;
3012
+ seen.add(link.url);
3013
+ out.push({ url: link.url, line: link.line });
3014
+ }
3015
+ return out;
3016
+ }
3017
+ function sameOrigin2(url, origin) {
3018
+ try {
3019
+ return new URL(url).origin === new URL(origin).origin;
3020
+ } catch {
3021
+ return false;
3022
+ }
3023
+ }
3024
+ function fileUrl(site) {
3025
+ return site.llmsTxt?.url ?? `${site.origin}/llms.txt`;
3026
+ }
3027
+ var llmstxtUnreachable = {
3028
+ id: "llmstxt.unreachable",
3029
+ severity: "warning",
3030
+ summary: "`/llms.txt` must answer with the file or with 404, not with a server error",
3031
+ rigor: "guideline",
3032
+ sources: ["lighthouse-llms-txt", "llmstxt"],
3033
+ // A 5xx only. No answer at all is not evidence about the route, and every
3034
+ // 4xx — a 404 above all — is the site saying it has no file.
3035
+ appliesTo: (site) => site.llmsTxt !== void 0 && site.llmsTxt.status >= 500 && !explainedByCatchAll(site, site.llmsTxt.status),
3036
+ check: ({ site, issue }) => issue({
3037
+ pageUrl: fileUrl(site),
3038
+ message: `\`/llms.txt\` answered ${site.llmsTxt.status}. Having no llms.txt is fine \u2014 a 404 says so \u2014 but a server error says something is here and failed, and the agent that asked for it gets nothing it can use. Lighthouse fails its llms.txt audit on exactly this.`,
3039
+ origin: { kind: "computed" }
3040
+ })
3041
+ };
3042
+ var llmstxtContentType = {
3043
+ id: "llmstxt.content-type",
3044
+ severity: "warning",
3045
+ summary: "`/llms.txt` must be served as text \u2014 `text/plain` or `text/markdown` \u2014 not as a page",
3046
+ rigor: "guideline",
3047
+ sources: ["llmstxt", "ietf-rfc9110"],
3048
+ appliesTo: (site) => {
3049
+ const probe = site.llmsTxt;
3050
+ if (probe?.found !== true) return false;
3051
+ if (probe.html) return !explainedByCatchAll(site, probe.status);
3052
+ return !TEXT_TYPES.has(probe.contentType ?? "");
3053
+ },
3054
+ check: ({ site, issue }) => {
3055
+ const probe = site.llmsTxt;
3056
+ const served = probe.contentType ? `\`${probe.contentType}\`` : "no content type";
3057
+ const message = probe.html ? `\`/llms.txt\` answered ${probe.status} with an HTML page (${served}), not a markdown file. An agent that asks for the site's llms.txt is handed a page instead. If the site has no llms.txt the path should answer 404; if it has one, this is not the route serving it.` : `\`/llms.txt\` is served with ${served}. The file is markdown, read by agents and by people: \`text/plain\` or \`text/markdown\` say so, while anything else tells a client to treat it as something it is not \u2014 \`text/html\` renders it as one run-on paragraph, and a download type does not render it at all.`;
3058
+ return issue({ pageUrl: fileUrl(site), message, origin: { kind: "computed" } });
3059
+ }
3060
+ };
3061
+ var llmstxtH1Missing = {
3062
+ id: "llmstxt.h1.missing",
3063
+ severity: "warning",
3064
+ summary: "An llms.txt must have an H1 naming the site",
3065
+ rigor: "guideline",
3066
+ sources: ["llmstxt"],
3067
+ appliesTo: (site) => readable(site) && !site.llmsTxt.h1,
3068
+ check: ({ site, issue }) => issue({
3069
+ pageUrl: fileUrl(site),
3070
+ message: "`/llms.txt` has no H1 (`# Name`, with a space after the `#`). The proposal calls it the only required section \u2014 without it the file is not an llms.txt by its own definition, and a reader following the format has no name for what the rest describes.",
3071
+ origin: { kind: "computed" }
3072
+ })
3073
+ };
3074
+ function listedProbes(site) {
3075
+ const byUrl = site.llmsTxtLinks?.byUrl;
3076
+ if (!byUrl) return [];
3077
+ return sameOriginLinks(site).flatMap(({ url }) => byUrl.get(url) ?? []);
3078
+ }
3079
+ var llmstxtLinkUnreachable = {
3080
+ id: "llmstxt.link.unreachable",
3081
+ severity: "warning",
3082
+ summary: "Every same-origin URL an llms.txt lists must answer",
3083
+ rigor: "guideline",
3084
+ sources: ["llmstxt"],
3085
+ appliesTo: (site) => readable(site) && listedProbes(site).some((probe) => isDead(probe.status)),
3086
+ check: ({ site, issue }) => {
3087
+ const dead = listedProbes(site).filter((probe) => isDead(probe.status));
3088
+ const unprobed = site.llmsTxtLinks?.unprobed ?? 0;
3089
+ const floor = unprobed > 0 ? ` ${unprobed} further URL${unprobed === 1 ? " was" : "s were"} not checked \u2014 this count is a floor, not a total.` : "";
3090
+ return issue({
3091
+ pageUrl: fileUrl(site),
3092
+ message: `${dead.length} URL${dead.length === 1 ? "" : "s"} \`/llms.txt\` lists ${dead.length === 1 ? "does" : "do"} not answer: ${sample(dead.map((probe) => `${probe.url} (${probe.status === 0 ? "no response" : `HTTP ${probe.status}`})`))}. The file exists to send agents to these pages, so each dead one is a lookup that ends in an error instead of an answer.${floor}`,
3093
+ origin: { kind: "computed" }
3094
+ });
3095
+ }
3096
+ };
3097
+ var llmstxtLinkBlockedByRobots = {
3098
+ id: "llmstxt.link.blocked-by-robots",
3099
+ severity: "warning",
3100
+ summary: "An llms.txt must not list URLs that robots.txt forbids fetching",
3101
+ rigor: "guideline",
3102
+ sources: ["llmstxt", "ietf-rfc9309", "anthropic-crawlers"],
3103
+ appliesTo: (site) => readable(site) && site.robots?.found === true && robotsAllows(site.robots.groups, "/").allowed,
3104
+ check: ({ site, issue }) => {
3105
+ const robots = site.robots;
3106
+ const file = fileUrl(site);
3107
+ const blocked = [];
3108
+ for (const url of [file, ...sameOriginLinks(site).map((link) => link.url)]) {
3109
+ let path;
3110
+ try {
3111
+ const parsed = new URL(url);
3112
+ path = `${parsed.pathname}${parsed.search}`;
3113
+ } catch {
3114
+ continue;
3115
+ }
3116
+ const decision = robotsAllows(robots.groups, path);
3117
+ if (decision.allowed || !decision.rule) continue;
3118
+ const self = url === file ? ", the file itself" : "";
3119
+ blocked.push(
3120
+ `${url} (line ${decision.rule.line}: Disallow: ${decision.rule.pattern}${self})`
3121
+ );
3122
+ }
3123
+ if (blocked.length === 0) return [];
3124
+ return issue({
3125
+ pageUrl: file,
3126
+ message: `${blocked.length} URL${blocked.length === 1 ? "" : "s"} \`/llms.txt\` points agents to ${blocked.length === 1 ? "is" : "are"} disallowed by \`robots.txt\` for \`User-agent: *\`: ${sample(blocked)}. The file offers them to agents, and an agent that honours robots.txt \u2014 Anthropic documents that Claude-User does \u2014 never fetches them.`,
3127
+ origin: { kind: "computed" }
3128
+ });
3129
+ }
3130
+ };
3131
+ var LLMS_TXT_RULES = [
3132
+ llmstxtContentType,
3133
+ llmstxtH1Missing,
3134
+ llmstxtLinkBlockedByRobots,
3135
+ llmstxtLinkUnreachable,
3136
+ llmstxtUnreachable
3137
+ ];
3138
+
3139
+ // src/lib/rules/sitemap-inventory.ts
3140
+ function sitemapReadInFull(site) {
3141
+ const discovery = site.discovery;
3142
+ if (discovery?.diagnostics.found !== true) return false;
3143
+ if (discovery.truncated || discovery.diagnostics.childSitemapErrors > 0) return false;
3144
+ const read = new Set(discovery.documents.map((doc) => resolved(doc.url)));
3145
+ const base = site.robots?.url ?? `${site.origin}/robots.txt`;
3146
+ return (site.robots?.sitemaps ?? []).every((declared) => {
3147
+ const url = resolved(declared.value, base);
3148
+ return url === void 0 || read.has(url);
3149
+ });
3150
+ }
3151
+ function resolved(value, base) {
3152
+ try {
3153
+ return new URL(value, base).href;
3154
+ } catch {
3155
+ return void 0;
3156
+ }
3157
+ }
3158
+
2832
3159
  // src/lib/rules/site-rules.ts
2833
3160
  function isMultilingual(site) {
2834
3161
  return site.localeAxis.multilingual;
@@ -3037,6 +3364,69 @@ var iconsIcoMissing = {
3037
3364
  });
3038
3365
  }
3039
3366
  };
3367
+ var httpNotFound = {
3368
+ id: "http.not-found",
3369
+ severity: "error",
3370
+ summary: "A URL that does not exist must answer 404 or 410",
3371
+ rigor: "vendor-spec",
3372
+ sources: ["ietf-rfc9110", "google-soft-404", "google-http-status"],
3373
+ appliesTo: (site) => (site.notFound ?? []).some((probe) => notFoundFailure(probe.status)),
3374
+ check: ({ site, issue }) => {
3375
+ const probes = site.notFound ?? [];
3376
+ const failed = probes.filter((probe) => notFoundFailure(probe.status));
3377
+ const held = probes.filter((probe) => isNotFound(probe.status));
3378
+ const answers = failed.map((probe) => {
3379
+ const path = pathOf(probe.url);
3380
+ const via = probe.finalUrl !== probe.url ? ` after redirecting to \`${probe.finalUrl}\`` : "";
3381
+ const type = probe.contentType ? ` \`${probe.contentType}\`` : "";
3382
+ return `\`${path}\` answered ${probe.status}${type}${via}`;
3383
+ });
3384
+ const contrast = held.length ? ` (${held.map((probe) => `\`${pathOf(probe.url)}\` answered ${probe.status}`).join(", ")}, so only some unknown paths are affected)` : "";
3385
+ const soft = failed.some((probe) => notFoundFailure(probe.status) === "soft");
3386
+ const crash = failed.some((probe) => notFoundFailure(probe.status) === "error");
3387
+ const consequences = [
3388
+ soft ? "A 2xx for a page that does not exist is a soft 404: every invented URL becomes a page to crawl, and a client asking for a file the site does not have \u2014 `/llms.txt`, `/feed.xml` \u2014 gets a page it may take for that file." : "",
3389
+ crash ? "A 5xx says the server failed, not that the page is missing: every mistyped link becomes a server error, and Google slows its crawl of the whole site in proportion to the URLs that return one." : ""
3390
+ ].filter(Boolean);
3391
+ return issue({
3392
+ pageUrl: site.origin,
3393
+ message: `Asked for a path that cannot exist, the origin did not answer 404 or 410: ${answers.join("; ")}${contrast}. ${consequences.join(" ")}`,
3394
+ origin: { kind: "computed" },
3395
+ fix: {
3396
+ title: "Make the route that catches unknown paths call notFound()",
3397
+ snippet: [
3398
+ "// A dynamic first segment matches everything, `/anything.txt` included,",
3399
+ "// whenever a middleware matcher skips dotted paths. Validate it wherever",
3400
+ "// it is read \u2014 the layout, every page, every generateMetadata:",
3401
+ "const { locale } = await params;",
3402
+ "if (!routing.locales.includes(locale)) notFound();",
3403
+ "//",
3404
+ "// The layout alone is not enough in a production build: the page and its",
3405
+ "// metadata render alongside it, and whichever throws first sets the",
3406
+ "// status \u2014 a 500 from a page that formatted a date in locale `llms.txt`.",
3407
+ "//",
3408
+ "// A catch-all `[...slug]` must do the same for slugs it does not know,",
3409
+ "// and a static host's SPA fallback (`/* /index.html 200`) should be a",
3410
+ "// 404 page instead."
3411
+ ].join("\n"),
3412
+ language: "ts"
3413
+ }
3414
+ });
3415
+ }
3416
+ };
3417
+ function notFoundFailure(status) {
3418
+ if (status >= 200 && status < 300) return "soft";
3419
+ if (status >= 500) return "error";
3420
+ return void 0;
3421
+ }
3422
+ function pathOf(url) {
3423
+ try {
3424
+ const parsed = new URL(url);
3425
+ return `${parsed.pathname}${parsed.search}`;
3426
+ } catch {
3427
+ return url;
3428
+ }
3429
+ }
3040
3430
  var ROBOTS_BYTE_LIMIT = 500 * 1024;
3041
3431
  var robotstxtUnreachable = {
3042
3432
  id: "robotstxt.unreachable",
@@ -3172,12 +3562,6 @@ var robotsBlocksPage = {
3172
3562
  }
3173
3563
  };
3174
3564
  var CHANGEFREQ = /* @__PURE__ */ new Set(["always", "hourly", "daily", "weekly", "monthly", "yearly", "never"]);
3175
- var SAMPLE = 5;
3176
- function sample(locs) {
3177
- const shown = locs.slice(0, SAMPLE);
3178
- const rest = locs.length - shown.length;
3179
- return `${shown.map((l) => `\`${l}\``).join(", ")}${rest > 0 ? `, and ${rest} more` : ""}`;
3180
- }
3181
3565
  var sitemapMissing = {
3182
3566
  id: "sitemap.missing",
3183
3567
  severity: "warning",
@@ -3494,16 +3878,43 @@ function sameUrlKey(url) {
3494
3878
  }
3495
3879
  function pagesByUrl(site) {
3496
3880
  const byUrl = /* @__PURE__ */ new Map();
3497
- for (const page of site.pages) {
3881
+ for (const page of [...site.pages, ...site.variants ?? []]) {
3498
3882
  const key = sameUrlKey(page.fetch.finalUrl);
3499
3883
  if (key) byUrl.set(key, page);
3500
3884
  }
3501
3885
  return byUrl;
3502
3886
  }
3887
+ var NOINDEX = /* @__PURE__ */ new Set(["noindex", "none"]);
3888
+ var VALUED_DIRECTIVES = /* @__PURE__ */ new Set([
3889
+ "max-snippet",
3890
+ "max-image-preview",
3891
+ "max-video-preview",
3892
+ "unavailable_after"
3893
+ ]);
3894
+ function headerDirectiveNames(value) {
3895
+ if (!value) return [];
3896
+ return value.split(",").map((token) => {
3897
+ let directive = token.trim().toLowerCase();
3898
+ const colon = directive.indexOf(":");
3899
+ if (colon !== -1 && !VALUED_DIRECTIVES.has(directive.slice(0, colon).trim())) {
3900
+ directive = directive.slice(colon + 1);
3901
+ }
3902
+ return directive.split(":")[0].trim();
3903
+ });
3904
+ }
3503
3905
  function saysNoindex(page) {
3504
- if (metaRobotsTokens(page).has("noindex")) return true;
3505
- const header = page.fetch.headers["x-robots-tag"] ?? "";
3506
- return header.split(",").map((token) => token.trim().toLowerCase()).includes("noindex");
3906
+ const metas = [page.meta.robots?.value, page.meta.googlebot?.value];
3907
+ if (metas.some((content) => (content ?? "").split(",").some(isNoindex))) return true;
3908
+ return headerDirectiveNames(page.fetch.headers["x-robots-tag"]).some(isNoindex);
3909
+ }
3910
+ function isNoindex(directive) {
3911
+ return NOINDEX.has(directive.trim().toLowerCase());
3912
+ }
3913
+ function canonicalElsewhere(page, self = sameUrlKey(page.fetch.finalUrl)) {
3914
+ const canonical = page.links.canonical;
3915
+ if (!canonical) return false;
3916
+ const target = sameUrlKey(canonical);
3917
+ return target !== void 0 && target !== self;
3507
3918
  }
3508
3919
  var sitemapEntryBlockedByRobots = {
3509
3920
  id: "sitemap.entry.blocked-by-robots",
@@ -3574,17 +3985,14 @@ var sitemapEntryNonCanonical = {
3574
3985
  for (const entry of site.discovery.urls) {
3575
3986
  const key = sameUrlKey(entry.loc);
3576
3987
  const page = key ? byUrl.get(key) : void 0;
3577
- const canonical = page?.links.canonical;
3578
- if (!page || !canonical) continue;
3579
- const canonicalKey = sameUrlKey(canonical);
3580
- if (canonicalKey && canonicalKey !== key) {
3581
- wrong.push(`${entry.loc} \u2192 ${canonical}`);
3988
+ if (page && canonicalElsewhere(page)) {
3989
+ wrong.push(`${entry.loc} \u2192 ${page.links.canonical}`);
3582
3990
  }
3583
3991
  }
3584
3992
  if (wrong.length === 0) return [];
3585
3993
  return issue({
3586
3994
  pageUrl: site.discovery.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,
3587
- message: `${wrong.length} sitemap entr${wrong.length === 1 ? "y names a page whose canonical" : "ies name pages whose canonicals"} point elsewhere: ${sample(wrong)}. The sitemap is a list of what to index, so it should name the URL the site itself prefers.`,
3995
+ message: `${wrong.length} sitemap entr${wrong.length === 1 ? "y names a page whose canonical points" : "ies name pages whose canonicals point"} elsewhere: ${sample(wrong)}. The sitemap is a list of what to index, so it should name the URL the site itself prefers.`,
3588
3996
  origin: { kind: "computed" }
3589
3997
  });
3590
3998
  }
@@ -3592,26 +4000,96 @@ var sitemapEntryNonCanonical = {
3592
4000
  var sitemapOrphans = {
3593
4001
  id: "sitemap.orphans",
3594
4002
  severity: "warning",
3595
- summary: "Indexable pages the crawl found should be listed in the sitemap",
4003
+ summary: "Indexable pages the crawl found, and the canonical URLs they name, should be listed in the sitemap",
3596
4004
  rigor: "guideline",
3597
4005
  sources: ["sitemaps-protocol", "google-sitemaps"],
3598
4006
  // One finding with a count and a sample, not one per page — the same shape
3599
4007
  // translation holes take, and for the same reason: forty repeats of one
3600
4008
  // omission is noise, and the omission is a property of the sitemap.
3601
- appliesTo: (site) => site.discovery?.diagnostics.found === true && site.discovery.urls.length > 0,
4009
+ //
4010
+ // Judged only against an inventory goflag read in full — every sitemap the
4011
+ // site declares, none of them cut short (`./sitemap-inventory.ts`). A page
4012
+ // listed where goflag could not read looks exactly like one the site forgot.
4013
+ // What stopped the read is its own finding where one exists
4014
+ // (`sitemap.index.child-error`); this one waits until the list is whole.
4015
+ appliesTo: (site) => sitemapReadInFull(site) && site.discovery.urls.length > 0,
3602
4016
  check: ({ site, issue }) => {
3603
- const listed = new Set(
3604
- site.discovery.urls.map((entry) => sameUrlKey(entry.loc)).filter(Boolean)
3605
- );
3606
- const orphans = site.pages.filter((page) => !saysNoindex(page)).map((page) => ({ page, key: sameUrlKey(page.fetch.finalUrl) })).filter(({ key }) => key !== void 0 && !listed.has(key)).map(({ page }) => page.fetch.finalUrl);
3607
- if (orphans.length === 0) return [];
4017
+ const { listed, fetched, unfollowed } = sitemapReach(site);
4018
+ const orphans = [];
4019
+ const unaudited = /* @__PURE__ */ new Map();
4020
+ for (const page of site.pages) {
4021
+ if (saysNoindex(page)) continue;
4022
+ const key = sameUrlKey(page.fetch.finalUrl);
4023
+ if (key === void 0) continue;
4024
+ if (!canonicalElsewhere(page, key)) {
4025
+ if (!listed.has(key)) orphans.push(page.fetch.finalUrl);
4026
+ continue;
4027
+ }
4028
+ const target = page.links.canonical;
4029
+ const targetKey2 = sameUrlKey(target);
4030
+ if (sameOrigin3(target, site.origin) && !listed.has(targetKey2) && !fetched.has(targetKey2) && !unaudited.has(targetKey2)) {
4031
+ unaudited.set(targetKey2, { url: target, namedBy: page.fetch.finalUrl });
4032
+ }
4033
+ }
4034
+ const named = [...unaudited.values()];
4035
+ const total = orphans.length + named.length;
4036
+ if (total === 0) return [];
4037
+ const said = [];
4038
+ if (orphans.length > 0) {
4039
+ said.push(
4040
+ `${orphans.length} crawled page${orphans.length === 1 ? "" : "s"} ask${orphans.length === 1 ? "s" : ""} to be indexed and ${orphans.length === 1 ? "is" : "are"} absent from the sitemap: ${sample(orphans)}.`
4041
+ );
4042
+ }
4043
+ if (named.length > 0) {
4044
+ said.push(
4045
+ `${named.length} URL${named.length === 1 ? "" : "s"} that crawled pages name as their canonical ${named.length === 1 ? "is" : "are"} absent from ${orphans.length > 0 ? "it too" : "the sitemap"}, and ${named.length === 1 ? "was" : "were"} not audited: ${sample(named.map((t) => `${t.url} (canonical of ${t.namedBy})`))}.`
4046
+ );
4047
+ }
3608
4048
  return issue({
3609
4049
  pageUrl: site.discovery.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,
3610
- message: `${orphans.length} crawled page${orphans.length === 1 ? "" : "s"} ask${orphans.length === 1 ? "s" : ""} to be indexed and ${orphans.length === 1 ? "is" : "are"} absent from the sitemap: ${sample(orphans)}. A consumer that reads the sitemap rather than following links will never see ${orphans.length === 1 ? "it" : "them"}.`,
4050
+ message: `${said.join(" ")} A consumer that reads the sitemap rather than following links will never see ${total === 1 ? "it" : "them"}.${ceilingNote(unfollowed)}`,
3611
4051
  origin: { kind: "computed" }
3612
4052
  });
3613
4053
  }
3614
4054
  };
4055
+ function sameOrigin3(a, b) {
4056
+ try {
4057
+ return new URL(a).origin === new URL(b).origin;
4058
+ } catch {
4059
+ return false;
4060
+ }
4061
+ }
4062
+ function sitemapReach(site) {
4063
+ const fetched = /* @__PURE__ */ new Set();
4064
+ const landingOf = /* @__PURE__ */ new Map();
4065
+ for (const page of [...site.pages, ...site.variants ?? []]) {
4066
+ const final = sameUrlKey(page.fetch.finalUrl);
4067
+ const requested = sameUrlKey(page.fetch.requestedUrl);
4068
+ if (!final) continue;
4069
+ fetched.add(final);
4070
+ if (requested && requested !== final) landingOf.set(requested, final);
4071
+ }
4072
+ for (const probe of entryProbes(site)) {
4073
+ const from = sameUrlKey(probe.url);
4074
+ const to = probe.redirected ? sameUrlKey(probe.finalUrl) : void 0;
4075
+ if (from && to) landingOf.set(from, to);
4076
+ }
4077
+ const listed = /* @__PURE__ */ new Set();
4078
+ let unfollowed = 0;
4079
+ for (const entry of site.discovery.urls) {
4080
+ const key = sameUrlKey(entry.loc);
4081
+ if (!key) continue;
4082
+ listed.add(key);
4083
+ const landing = landingOf.get(key);
4084
+ if (landing) listed.add(landing);
4085
+ else if (!fetched.has(key) && !site.sitemapEntries?.byUrl.has(entry.loc)) unfollowed += 1;
4086
+ }
4087
+ return { listed, fetched, unfollowed };
4088
+ }
4089
+ function ceilingNote(unfollowed) {
4090
+ if (unfollowed === 0) return "";
4091
+ return ` ${unfollowed} sitemap entr${unfollowed === 1 ? "y was" : "ies were"} not fetched, and a page one of them redirects to would be counted here \u2014 this count is a ceiling, not a total.`;
4092
+ }
3615
4093
  function entryProbes(site) {
3616
4094
  return [...site.sitemapEntries?.byUrl.values() ?? []];
3617
4095
  }
@@ -3652,9 +4130,6 @@ var sitemapEntryRedirects = {
3652
4130
  });
3653
4131
  }
3654
4132
  };
3655
- function isDead(status) {
3656
- return status === 0 || status >= 400;
3657
- }
3658
4133
  function isRobotsFailure(status) {
3659
4134
  return status === 0 || status >= 500;
3660
4135
  }
@@ -3669,7 +4144,9 @@ function isAbsolute(value) {
3669
4144
  var SITE_RULES = [
3670
4145
  hreflangMissing,
3671
4146
  hreflangClusterIncomplete,
4147
+ httpNotFound,
3672
4148
  iconsIcoMissing,
4149
+ ...LLMS_TXT_RULES,
3673
4150
  robotsBlocksPage,
3674
4151
  robotsBlocksSite,
3675
4152
  robotstxtCrossOrigin,
@@ -3824,7 +4301,7 @@ function suggestedLocales(axis) {
3824
4301
  import { gunzipSync } from "zlib";
3825
4302
 
3826
4303
  // src/lib/core/sitemap/parse.ts
3827
- import * as cheerio2 from "cheerio";
4304
+ import * as cheerio4 from "cheerio";
3828
4305
  function alternatesOf(node) {
3829
4306
  const out = [];
3830
4307
  const seen = /* @__PURE__ */ new Set();
@@ -3852,7 +4329,7 @@ function parseSitemap(body) {
3852
4329
  }
3853
4330
  let $;
3854
4331
  try {
3855
- $ = cheerio2.load(trimmed, { xmlMode: true });
4332
+ $ = cheerio4.load(trimmed, { xmlMode: true });
3856
4333
  } catch {
3857
4334
  return { kind: "unknown", wellFormed: false, urls: [], sitemaps: [] };
3858
4335
  }
@@ -4249,6 +4726,10 @@ function normaliseContentType(raw) {
4249
4726
  const semi = raw.indexOf(";");
4250
4727
  return (semi === -1 ? raw : raw.slice(0, semi)).trim().toLowerCase() || void 0;
4251
4728
  }
4729
+ function capText(text, maxBytes) {
4730
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
4731
+ return Buffer.from(text.slice(0, maxBytes), "utf8").subarray(0, maxBytes).toString("utf8");
4732
+ }
4252
4733
  async function readCappedText(res, maxBytes) {
4253
4734
  const reader = res.body?.getReader();
4254
4735
  if (!reader) {
@@ -4381,6 +4862,34 @@ async function checkLink(url, options = {}) {
4381
4862
  durationMs: Date.now() - started
4382
4863
  };
4383
4864
  }
4865
+ function checkFromAnswer(url, answer, options = {}) {
4866
+ if (answer.status < 200 || answer.status >= 300) return void 0;
4867
+ const outcome = {
4868
+ status: answer.status,
4869
+ finalUrl: url,
4870
+ redirectChain: [],
4871
+ loop: false,
4872
+ body: capText(answer.body, options.softBodyBytes ?? DEFAULTS2.softBodyBytes),
4873
+ method: "GET"
4874
+ };
4875
+ const softNotFound = isSoftNotFound(outcome);
4876
+ return {
4877
+ url,
4878
+ finalUrl: url,
4879
+ status: outcome.status,
4880
+ verdict: classifyLink({
4881
+ status: outcome.status,
4882
+ redirected: false,
4883
+ softNotFound,
4884
+ loop: false
4885
+ }),
4886
+ method: "GET",
4887
+ redirectChain: [],
4888
+ reason: describeReason(outcome, softNotFound),
4889
+ checkedAt: answer.checkedAt,
4890
+ durationMs: answer.durationMs
4891
+ };
4892
+ }
4384
4893
  async function probeWithRetry(url, options) {
4385
4894
  const retries = options.retries ?? DEFAULTS2.retries;
4386
4895
  const sleep = options.sleep ?? defaultSleep;
@@ -4498,7 +5007,7 @@ function schemeOf(url) {
4498
5007
  }
4499
5008
  }
4500
5009
  function defaultSleep(ms) {
4501
- return new Promise((resolve2) => setTimeout(resolve2, ms));
5010
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4502
5011
  }
4503
5012
 
4504
5013
  // src/lib/core/sitemap/entries.ts
@@ -4919,6 +5428,86 @@ function isIconType(contentType) {
4919
5428
  return contentType.startsWith("image/") || contentType === "application/ico" || contentType === "application/x-ico";
4920
5429
  }
4921
5430
 
5431
+ // src/lib/core/llmstxt/parse.ts
5432
+ var H1 = /^ {0,3}#(?:[ \t]+(.*?))?[ \t]*#*[ \t]*$/;
5433
+ var FENCE = /^ {0,3}(`{3,}|~{3,})/;
5434
+ var LINK = /(!?)\[([^\]]*)\]\(\s*<?([^)\s>]+)>?(?:\s+(?:"[^"]*"|'[^']*'))?\s*\)/g;
5435
+ function parseLlmsTxt(body, fileUrl2) {
5436
+ const lines = body.replace(/^\uFEFF/, "").split(/\r\n|\r|\n/);
5437
+ const links = [];
5438
+ let h1;
5439
+ let fence;
5440
+ for (const [index, raw] of lines.entries()) {
5441
+ const line = index + 1;
5442
+ const fenceMatch = FENCE.exec(raw);
5443
+ if (fenceMatch) {
5444
+ const marker = fenceMatch[1];
5445
+ if (fence === void 0) fence = marker;
5446
+ else if (marker[0] === fence[0] && marker.length >= fence.length) fence = void 0;
5447
+ continue;
5448
+ }
5449
+ if (fence !== void 0) continue;
5450
+ if (!h1) {
5451
+ const heading = H1.exec(raw);
5452
+ const text = heading?.[1]?.trim();
5453
+ if (text) h1 = { text, line };
5454
+ }
5455
+ const prose = raw.replace(/`[^`]*`/g, "");
5456
+ for (const match of prose.matchAll(LINK)) {
5457
+ if (match[1] === "!") continue;
5458
+ const url = resolve(match[3], fileUrl2);
5459
+ if (url) links.push({ name: match[2].trim(), url, line });
5460
+ }
5461
+ }
5462
+ return { ...h1 ? { h1 } : {}, links };
5463
+ }
5464
+ function looksLikeHtml(body) {
5465
+ return /^(?:<!doctype\s+html|<html[\s>])/i.test(body.replace(/^\uFEFF/, "").trimStart());
5466
+ }
5467
+ function resolve(href, base) {
5468
+ try {
5469
+ const url = new URL(href, base);
5470
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
5471
+ url.hash = "";
5472
+ return url.toString();
5473
+ } catch {
5474
+ return void 0;
5475
+ }
5476
+ }
5477
+
5478
+ // src/lib/core/probes/llms-txt.ts
5479
+ async function probeLlmsTxt(origin, options = {}) {
5480
+ const url = new URL("/llms.txt", origin).toString();
5481
+ const { signal, cleanup } = combineSignals(options.signal, options.timeoutMs ?? 5e3);
5482
+ const nothing = { found: false, html: false, links: [] };
5483
+ try {
5484
+ const res = await fetch(url, { signal, redirect: "follow" });
5485
+ const finalUrl = res.url || url;
5486
+ const contentType = res.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() || void 0;
5487
+ if (!res.ok) {
5488
+ await res.body?.cancel().catch(() => void 0);
5489
+ return { url, status: res.status, finalUrl, contentType, ...nothing };
5490
+ }
5491
+ const body = await res.text();
5492
+ if (looksLikeHtml(body)) {
5493
+ return { url, status: res.status, finalUrl, contentType, found: true, html: true, links: [] };
5494
+ }
5495
+ return {
5496
+ url,
5497
+ status: res.status,
5498
+ finalUrl,
5499
+ contentType,
5500
+ found: true,
5501
+ html: false,
5502
+ ...parseLlmsTxt(body, finalUrl)
5503
+ };
5504
+ } catch {
5505
+ return { url, status: 0, finalUrl: url, ...nothing };
5506
+ } finally {
5507
+ cleanup();
5508
+ }
5509
+ }
5510
+
4922
5511
  // src/lib/rules/advisory.ts
4923
5512
  function resolveEvidencePath(extraction, path) {
4924
5513
  let current = extraction;
@@ -5087,7 +5676,12 @@ var hreflangSitemapMismatch = {
5087
5676
  summary: "`<head>` alternates advertise a locale the sitemap does not list",
5088
5677
  prose: "Your `<head>` advertises a translation your sitemap does not list. Is the sitemap missing an entry, or is the alternate pointing at a page you did not mean to publish?",
5089
5678
  why: "The two are derived from one intent by different code paths, so a disagreement means one of them is wrong. No specification says which: the declaration methods are equivalent, and a page can legitimately be cross-linked and kept out of the sitemap.",
5090
- appliesTo: (site) => site.localeAxis.multilingual && (site.discovery?.urls.length ?? 0) > 0,
5679
+ // A question about what the sitemap *omits*, so it waits for a sitemap read in
5680
+ // full (`./sitemap-inventory.ts`): a translation listed in a child goflag
5681
+ // could not read, or in a second declared sitemap it never opened, would be
5682
+ // asked about as missing — a question with a false premise wastes an agent's
5683
+ // turn as surely as a false finding wastes a human's.
5684
+ appliesTo: (site) => site.localeAxis.multilingual && (site.discovery?.urls.length ?? 0) > 0 && sitemapReadInFull(site),
5091
5685
  ask: (site) => {
5092
5686
  const axis = new Set(site.localeAxis.locales.map((l) => l.toLowerCase()));
5093
5687
  const bySitemap = /* @__PURE__ */ new Map();
@@ -5164,89 +5758,6 @@ function collectSiteAdvisories(site, rules = SITE_PROSE_RULES) {
5164
5758
  return out;
5165
5759
  }
5166
5760
 
5167
- // src/lib/core/links/extract.ts
5168
- import * as cheerio3 from "cheerio";
5169
- var ANCHOR_SPEC = { selector: "a[href]", attr: "href", source: "a" };
5170
- var ASSET_SPECS = [
5171
- { selector: "img[src]", attr: "src", source: "img" },
5172
- { selector: "script[src]", attr: "src", source: "script" },
5173
- { selector: "link[href]", attr: "href", source: "link" },
5174
- { selector: "iframe[src]", attr: "src", source: "iframe" }
5175
- ];
5176
- function extractLinks(html, options) {
5177
- const $ = cheerio3.load(html);
5178
- const base = resolveBase2($, options.baseUrl);
5179
- const origin = safeOrigin(options.baseUrl);
5180
- const specs = options.includeAssets ? [ANCHOR_SPEC, ...ASSET_SPECS] : [ANCHOR_SPEC];
5181
- const out = [];
5182
- const seen = /* @__PURE__ */ new Set();
5183
- for (const spec of specs) {
5184
- $(spec.selector).each((_, el) => {
5185
- const raw = $(el).attr(spec.attr);
5186
- if (raw === void 0) return;
5187
- const rawHref = raw.trim();
5188
- if (!rawHref) return;
5189
- const ref = buildRef($, el, rawHref, spec, base, origin);
5190
- if (!ref) return;
5191
- const dedupeKey = `${ref.source}|${ref.url}`;
5192
- if (seen.has(dedupeKey)) return;
5193
- seen.add(dedupeKey);
5194
- out.push(ref);
5195
- });
5196
- }
5197
- return out;
5198
- }
5199
- function buildRef($, el, rawHref, spec, base, origin) {
5200
- const fragment = extractFragment(rawHref);
5201
- const canonical = canonicaliseUrl(rawHref, base, { preserveTrailingSlash: true });
5202
- const url = canonical ?? rawHref;
5203
- const kind = canonical ? classify(canonical, origin) : "external";
5204
- const ref = {
5205
- rawHref,
5206
- url,
5207
- kind,
5208
- source: spec.source,
5209
- rel: spec.source === "a" || spec.source === "link" ? parseRel($(el).attr("rel")) : []
5210
- };
5211
- if (spec.source === "a") {
5212
- const text = $(el).text().trim();
5213
- if (text) ref.anchorText = text;
5214
- }
5215
- if (fragment) ref.fragment = fragment;
5216
- return ref;
5217
- }
5218
- function extractFragment(rawHref) {
5219
- const hash = rawHref.indexOf("#");
5220
- if (hash === -1) return void 0;
5221
- const frag = rawHref.slice(hash);
5222
- return frag.length > 1 ? frag : void 0;
5223
- }
5224
- function parseRel(rel) {
5225
- if (!rel) return [];
5226
- return rel.split(/\s+/).map((t) => t.trim().toLowerCase()).filter(Boolean);
5227
- }
5228
- function classify(url, origin) {
5229
- if (!origin) return "external";
5230
- return safeOrigin(url) === origin ? "internal" : "external";
5231
- }
5232
- function safeOrigin(url) {
5233
- try {
5234
- return new URL(url).origin;
5235
- } catch {
5236
- return null;
5237
- }
5238
- }
5239
- function resolveBase2($, pageUrl) {
5240
- const declared = $("base[href]").first().attr("href")?.trim();
5241
- if (declared) {
5242
- try {
5243
- return new URL(declared, pageUrl).toString();
5244
- } catch {
5245
- }
5246
- }
5247
- return pageUrl;
5248
- }
5249
-
5250
5761
  // src/lib/core/links/types.ts
5251
5762
  function emptyVerdictSummary() {
5252
5763
  return { ok: 0, redirect: 0, broken: 0, blocked: 0, warning: 0, skipped: 0 };
@@ -5278,8 +5789,16 @@ async function runLinkAudit(discovery, options = {}) {
5278
5789
  let pagesScanned = 0;
5279
5790
  let pagesFailed = 0;
5280
5791
  let scanDone = 0;
5792
+ const maxBytes = options.maxScanBytes ?? DEFAULTS3.maxScanBytes;
5793
+ const handedPages = [];
5794
+ const toFetch = [];
5795
+ for (const pageUrl of pageUrls) {
5796
+ const handed = options.pages?.get(pageUrl);
5797
+ if (handed) handedPages.push([pageUrl, handed]);
5798
+ else toFetch.push(pageUrl);
5799
+ }
5281
5800
  await mapWithHostCaps(
5282
- pageUrls,
5801
+ toFetch,
5283
5802
  {
5284
5803
  concurrency: options.scanConcurrency ?? DEFAULTS3.scanConcurrency,
5285
5804
  perHost: Infinity,
@@ -5291,7 +5810,7 @@ async function runLinkAudit(discovery, options = {}) {
5291
5810
  timeoutMs: options.timeoutMs ?? DEFAULTS3.timeoutMs,
5292
5811
  allowInsecureTls: options.allowInsecureTls,
5293
5812
  userAgent: options.userAgent,
5294
- maxBytes: options.maxScanBytes ?? DEFAULTS3.maxScanBytes,
5813
+ maxBytes,
5295
5814
  signal: options.signal
5296
5815
  });
5297
5816
  if (res.status === 0 || res.status >= 400 || res.body === void 0) {
@@ -5309,6 +5828,17 @@ async function runLinkAudit(discovery, options = {}) {
5309
5828
  },
5310
5829
  () => "scan"
5311
5830
  );
5831
+ for (const [pageUrl, handed] of handedPages) {
5832
+ if (options.signal?.aborted) break;
5833
+ const refs = handed.links && !options.includeAssets && Buffer.byteLength(handed.html) <= maxBytes ? handed.links : extractLinks(capText(handed.html, maxBytes), {
5834
+ baseUrl: pageUrl,
5835
+ includeAssets: options.includeAssets
5836
+ });
5837
+ for (const ref of refs) occurrences.push({ pageUrl, ref });
5838
+ pagesScanned += 1;
5839
+ scanDone += 1;
5840
+ options.onProgress?.({ phase: "scan", done: scanDone, total: pageUrls.length });
5841
+ }
5312
5842
  const uniqueUrls = [];
5313
5843
  const seen = /* @__PURE__ */ new Set();
5314
5844
  for (const occ of occurrences) {
@@ -5325,8 +5855,20 @@ async function runLinkAudit(discovery, options = {}) {
5325
5855
  }
5326
5856
  const checks = {};
5327
5857
  let checkDone = 0;
5858
+ const toProbe = [];
5859
+ for (const url of uniqueUrls) {
5860
+ const answer = options.answers?.get(url);
5861
+ const check = answer ? checkFromAnswer(url, answer) : void 0;
5862
+ if (!check) {
5863
+ toProbe.push(url);
5864
+ continue;
5865
+ }
5866
+ checks[url] = check;
5867
+ checkDone += 1;
5868
+ options.onProgress?.({ phase: "check", done: checkDone, total: uniqueUrls.length });
5869
+ }
5328
5870
  await mapWithHostCaps(
5329
- uniqueUrls,
5871
+ toProbe,
5330
5872
  {
5331
5873
  concurrency: options.checkConcurrency ?? DEFAULTS3.checkConcurrency,
5332
5874
  perHost: options.maxPerHost ?? DEFAULTS3.maxPerHost,
@@ -5400,9 +5942,9 @@ function hostOf(url) {
5400
5942
  function mapWithHostCaps(items, opts, worker, hostOf3) {
5401
5943
  const concurrency = Math.max(1, opts.concurrency);
5402
5944
  const perHost = opts.perHost;
5403
- return new Promise((resolve2) => {
5945
+ return new Promise((resolve3) => {
5404
5946
  if (items.length === 0) {
5405
- resolve2();
5947
+ resolve3();
5406
5948
  return;
5407
5949
  }
5408
5950
  const pending = items.map((item) => ({ item, host: hostOf3(item) }));
@@ -5412,7 +5954,7 @@ function mapWithHostCaps(items, opts, worker, hostOf3) {
5412
5954
  const finish = () => {
5413
5955
  if (!settled) {
5414
5956
  settled = true;
5415
- resolve2();
5957
+ resolve3();
5416
5958
  }
5417
5959
  };
5418
5960
  const pump = () => {
@@ -5578,14 +6120,14 @@ async function attachManifests(pages, options) {
5578
6120
  }
5579
6121
  }
5580
6122
  async function probeAssets(pages, options) {
5581
- const sameOrigin2 = (url) => {
6123
+ const sameOrigin4 = (url) => {
5582
6124
  try {
5583
6125
  return new URL(url).origin === new URL(options.origin).origin;
5584
6126
  } catch {
5585
6127
  return false;
5586
6128
  }
5587
6129
  };
5588
- const wantedUrl = (url) => /^https?:\/\//i.test(url) && (options.checkExternal || sameOrigin2(url));
6130
+ const wantedUrl = (url) => /^https?:\/\//i.test(url) && (options.checkExternal || sameOrigin4(url));
5589
6131
  const wanted = /* @__PURE__ */ new Set();
5590
6132
  for (const page of pages) {
5591
6133
  for (const image of page.openGraph.images) {
@@ -5639,7 +6181,8 @@ function dropCanonicalDuplicates(pages) {
5639
6181
  if (target === self) return true;
5640
6182
  return !crawled.has(target);
5641
6183
  });
5642
- return { kept, dropped: pages.length - kept.length };
6184
+ const keptSet = new Set(kept);
6185
+ return { kept, variants: pages.filter((page) => !keptSet.has(page)) };
5643
6186
  }
5644
6187
  async function runAudit(inputUrl, options = {}) {
5645
6188
  const normalized = normalizeInputUrl(inputUrl);
@@ -5673,6 +6216,14 @@ async function runAudit(inputUrl, options = {}) {
5673
6216
  signal: options.signal,
5674
6217
  timeoutMs: options.timeoutMs
5675
6218
  }).catch(() => void 0);
6219
+ const [notFound, llmsTxt] = await Promise.all([
6220
+ probeNotFound(origin, { signal: options.signal, timeoutMs: options.timeoutMs }).catch(
6221
+ () => void 0
6222
+ ),
6223
+ probeLlmsTxt(origin, { signal: options.signal, timeoutMs: options.timeoutMs }).catch(
6224
+ () => void 0
6225
+ )
6226
+ ]);
5676
6227
  const crawlResult = await crawl({
5677
6228
  entryUrl: entry,
5678
6229
  seedUrls: sitemapUrls,
@@ -5683,6 +6234,7 @@ async function runAudit(inputUrl, options = {}) {
5683
6234
  inspectOptions: {
5684
6235
  mode: options.static ? "static" : "auto",
5685
6236
  probes: false,
6237
+ linkRefs: true,
5686
6238
  allowInsecureTls: options.allowInsecureTls,
5687
6239
  timeoutMs: options.timeoutMs,
5688
6240
  signal: options.signal
@@ -5706,7 +6258,8 @@ async function runAudit(inputUrl, options = {}) {
5706
6258
  if (escalationWarning) warnings.push(escalationWarning);
5707
6259
  const okPages = pages.filter((p) => isOkStatus(p.fetch.status));
5708
6260
  const documents = okPages.filter(isHtmlPage);
5709
- const { kept: htmlPages, dropped: duplicatePages } = dropCanonicalDuplicates(documents);
6261
+ const { kept: htmlPages, variants } = dropCanonicalDuplicates(documents);
6262
+ const duplicatePages = variants.length;
5710
6263
  const unreachablePages = [
5711
6264
  ...pages.filter((p) => !isOkStatus(p.fetch.status)).map((p) => ({
5712
6265
  id: fingerprint("page", routeKey(p.fetch.finalUrl)),
@@ -5822,6 +6375,16 @@ async function runAudit(inputUrl, options = {}) {
5822
6375
  checkExternal,
5823
6376
  timeoutMs: options.timeoutMs,
5824
6377
  maxPages: effectiveMaxPages,
6378
+ pages: new Map(
6379
+ okPages.filter(isHtmlPage).map((page) => [
6380
+ page.fetch.finalUrl,
6381
+ {
6382
+ html: page.html.static,
6383
+ links: page.extractor.escalated ? void 0 : page.linkRefs
6384
+ }
6385
+ ])
6386
+ ),
6387
+ answers: crawlAnswers(okPages),
5825
6388
  signal: options.signal,
5826
6389
  onProgress: (p) => options.onProgress?.({
5827
6390
  phase: p.phase === "scan" ? "scan" : "links",
@@ -5830,18 +6393,23 @@ async function runAudit(inputUrl, options = {}) {
5830
6393
  })
5831
6394
  });
5832
6395
  warnings.push(...linkReport.diagnostics.warnings);
6396
+ const answered = {
6397
+ crawled: new Map(crawlResult.pages.map((page) => [page.fetch.finalUrl, page.fetch.status])),
6398
+ checked: new Map(Object.values(linkReport.checks).map((check) => [check.url, check])),
6399
+ maxProbes: effectiveMaxPages,
6400
+ signal: options.signal,
6401
+ timeoutMs: options.timeoutMs,
6402
+ allowInsecureTls: options.allowInsecureTls
6403
+ };
5833
6404
  const sitemapEntries = discovery ? await probeSitemapEntries(
5834
6405
  discovery.urls.map((entry2) => entry2.loc),
5835
- {
5836
- crawled: new Map(
5837
- crawlResult.pages.map((page) => [page.fetch.finalUrl, page.fetch.status])
5838
- ),
5839
- checked: new Map(Object.values(linkReport.checks).map((check) => [check.url, check])),
5840
- maxProbes: effectiveMaxPages,
5841
- signal: options.signal,
5842
- timeoutMs: options.timeoutMs,
5843
- allowInsecureTls: options.allowInsecureTls
5844
- }
6406
+ answered
6407
+ ) : void 0;
6408
+ const llmsTxtLinks = llmsTxt?.found && !llmsTxt.html ? await probeSitemapEntries(
6409
+ [...new Set(llmsTxt.links.map((link) => link.url))].filter(
6410
+ (url) => sameOriginAs(url, origin)
6411
+ ),
6412
+ answered
5845
6413
  ) : void 0;
5846
6414
  const siteContext = {
5847
6415
  origin,
@@ -5851,7 +6419,14 @@ async function runAudit(inputUrl, options = {}) {
5851
6419
  discovery,
5852
6420
  robots,
5853
6421
  favicon: favicon2,
6422
+ notFound,
6423
+ llmsTxt,
6424
+ llmsTxtLinks,
5854
6425
  sitemapEntries,
6426
+ // Set aside above, and still what their URLs serve. The sitemap rules that
6427
+ // ask what a listed URL serves need them: a listed variant is exactly what
6428
+ // `sitemap.entry.non-canonical` exists to catch.
6429
+ variants,
5855
6430
  // Same index the matrix was built from, so a rule that groups by route
5856
6431
  // and the grid that reports holes cannot disagree about which URLs are
5857
6432
  // one page.
@@ -6014,6 +6589,28 @@ function countUncrawled(sitemapUrls, pages) {
6014
6589
  for (const url of sitemapUrls) if (!crawled.has(url)) n += 1;
6015
6590
  return n;
6016
6591
  }
6592
+ function sameOriginAs(url, origin) {
6593
+ try {
6594
+ return new URL(url).origin === origin;
6595
+ } catch {
6596
+ return false;
6597
+ }
6598
+ }
6599
+ function crawlAnswers(pages) {
6600
+ const answers = /* @__PURE__ */ new Map();
6601
+ for (const page of pages) {
6602
+ if (!isOkStatus(page.fetch.status) || !isHtmlPage(page)) continue;
6603
+ if (page.extractor.escalated) continue;
6604
+ if (page.fetch.redirectCount > 0 || page.fetch.finalUrl !== page.fetch.requestedUrl) continue;
6605
+ answers.set(page.fetch.requestedUrl, {
6606
+ status: page.fetch.status,
6607
+ body: page.html.static,
6608
+ checkedAt: page.fetchedAt,
6609
+ durationMs: page.fetch.durationMs
6610
+ });
6611
+ }
6612
+ return answers;
6613
+ }
6017
6614
  function syntheticDiscovery(origin, baseUrl, pages, truncated) {
6018
6615
  return {
6019
6616
  origin,
@@ -6436,7 +7033,7 @@ function hostOf2(value) {
6436
7033
  return value;
6437
7034
  }
6438
7035
  }
6439
- function pathOf(value) {
7036
+ function pathOf2(value) {
6440
7037
  try {
6441
7038
  const { pathname, search } = new URL(value);
6442
7039
  return `${pathname}${search}`;
@@ -6465,7 +7062,7 @@ function originLabel(origin) {
6465
7062
  return "computed";
6466
7063
  }
6467
7064
  }
6468
- function resolve(...facts) {
7065
+ function resolve2(...facts) {
6469
7066
  for (const fact of facts) {
6470
7067
  if (fact && fact.value.trim() !== "") {
6471
7068
  return { value: fact.value, from: originLabel(fact.origin) };
@@ -6509,13 +7106,13 @@ function cardOf(extraction) {
6509
7106
  return {
6510
7107
  url,
6511
7108
  host: hostOf2(url),
6512
- path: pathOf(url),
6513
- ogTitle: resolve(og.title, doc.title),
6514
- ogDescription: resolve(og.description, meta.description),
6515
- documentTitle: resolve(doc.title),
6516
- metaDescription: resolve(meta.description),
6517
- siteName: resolve(og.siteName),
6518
- xTitle: resolve(twitter.title, og.title, doc.title),
7109
+ path: pathOf2(url),
7110
+ ogTitle: resolve2(og.title, doc.title),
7111
+ ogDescription: resolve2(og.description, meta.description),
7112
+ documentTitle: resolve2(doc.title),
7113
+ metaDescription: resolve2(meta.description),
7114
+ siteName: resolve2(og.siteName),
7115
+ xTitle: resolve2(twitter.title, og.title, doc.title),
6519
7116
  image: imageOf(extraction),
6520
7117
  icon: safeUrl(extraction.links.icons[0]?.href),
6521
7118
  themeColor: meta.themeColor?.value,
@@ -7018,7 +7615,7 @@ function renderPreview(report, options = {}) {
7018
7615
  }
7019
7616
  const empty = report.extractions === void 0 ? `This report carries no extractions, so there is nothing to draw. Pass <code>extractions: true</code> to <code>runAudit</code>, or use <code>goflag preview &lt;url&gt;</code>, which does.` : `The crawl reached no HTML page it could read, so there is nothing to draw. Every page was unreachable, not HTML, or a declared duplicate of another.`;
7020
7617
  const rows = extractions.map((extraction, index) => {
7021
- const path = pathOf(extraction.http.finalUrl);
7618
+ const path = pathOf2(extraction.http.finalUrl);
7022
7619
  return {
7023
7620
  index,
7024
7621
  locale: splitRoute(path).locale,
@@ -7030,7 +7627,7 @@ function renderPreview(report, options = {}) {
7030
7627
  });
7031
7628
  const byRoute = /* @__PURE__ */ new Map();
7032
7629
  extractions.forEach((extraction, index) => {
7033
- const route = splitRoute(pathOf(extraction.http.finalUrl)).route;
7630
+ const route = splitRoute(pathOf2(extraction.http.finalUrl)).route;
7034
7631
  byRoute.set(route, [...byRoute.get(route) ?? [], rows[index]]);
7035
7632
  });
7036
7633
  const body = extractions.length === 0 ? `<section class="page"><p class="none">${empty}</p></section>` : extractions.map(
@@ -7038,13 +7635,13 @@ function renderPreview(report, options = {}) {
7038
7635
  extraction,
7039
7636
  issuesByPage.get(extraction.http.finalUrl) ?? [],
7040
7637
  index,
7041
- byRoute.get(splitRoute(pathOf(extraction.http.finalUrl)).route) ?? []
7638
+ byRoute.get(splitRoute(pathOf2(extraction.http.finalUrl)).route) ?? []
7042
7639
  )
7043
7640
  ).join("\n");
7044
7641
  const nav = extractions.length > 1 ? `<nav class="nav">
7045
7642
  <p class="navnote">${extractions.length} pages, by path. A folder folds; the \u2197 beside one opens the page at that path itself.</p>
7046
7643
  ${routeList(
7047
- buildRouteTree(extractions.map((extraction) => pathOf(extraction.http.finalUrl))),
7644
+ buildRouteTree(extractions.map((extraction) => pathOf2(extraction.http.finalUrl))),
7048
7645
  extractions.length <= TREE_OPEN_MAX,
7049
7646
  "tree"
7050
7647
  )}
@@ -7362,4 +7959,4 @@ export {
7362
7959
  summarize,
7363
7960
  Logger
7364
7961
  };
7365
- //# sourceMappingURL=chunk-57NVXEJS.js.map
7962
+ //# sourceMappingURL=chunk-KHIN5TNX.js.map