seo 0.2.13 → 0.2.14

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/mcp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createRequire as __seoCreateRequire } from 'node:module'; const require = __seoCreateRequire(import.meta.url);
2
- import { n as load, t as version } from "./package-B_swz2Xd.js";
2
+ import { n as load, t as version } from "./package-Cp8DwqCy.js";
3
3
  import { Headers, fetch as fetch$1 } from "undici";
4
4
  import { chmodSync, createReadStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { arch, homedir, platform, totalmem } from "node:os";
@@ -2778,6 +2778,22 @@ function extractStructuredData($, baseUrl, options = {}) {
2778
2778
  }
2779
2779
  //#endregion
2780
2780
  //#region packages/core/dist/extract/page-extractor.js
2781
+ function metaRefresh($, finalUrl) {
2782
+ for (const element of $("meta[http-equiv]").toArray()) {
2783
+ if ($(element).attr("http-equiv")?.trim().toLowerCase() !== "refresh") continue;
2784
+ const match = ($(element).attr("content")?.trim())?.match(/^([0-9]+(?:\.[0-9]+)?)\s*;\s*url\s*=\s*(?:"([^"]*)"|'([^']*)'|(.+))$/iu);
2785
+ if (!match) continue;
2786
+ try {
2787
+ const target = new URL((match[2] ?? match[3] ?? match[4] ?? "").trim(), finalUrl);
2788
+ if (!["http:", "https:"].includes(target.protocol)) continue;
2789
+ target.hash = "";
2790
+ return {
2791
+ targetUrl: target.toString(),
2792
+ delaySeconds: Number(match[1])
2793
+ };
2794
+ } catch {}
2795
+ }
2796
+ }
2781
2797
  const SOCIAL_PROFILE_HOSTS = [
2782
2798
  "facebook.com",
2783
2799
  "instagram.com",
@@ -3057,6 +3073,8 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
3057
3073
  }, extractor, dependencies);
3058
3074
  const { text, excerpt } = content;
3059
3075
  const title = safeText$1($("title").first().text());
3076
+ const crawlerMetaRefresh = extractor === "crawler" ? metaRefresh($, fetchResult.finalUrl) : void 0;
3077
+ if (crawlerMetaRefresh && new URL(crawlerMetaRefresh.targetUrl).origin === url.origin) crawlerInternalLinks.add(crawlerMetaRefresh.targetUrl);
3060
3078
  return {
3061
3079
  url: fetchResult.url,
3062
3080
  finalUrl: fetchResult.finalUrl,
@@ -3082,6 +3100,7 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
3082
3100
  externalAnchorSamples,
3083
3101
  socialProfileLinks: [...socialProfileLinks],
3084
3102
  contentSketch: contentSketch(text),
3103
+ metaRefresh: crawlerMetaRefresh,
3085
3104
  softAuthenticationGate: softAuthenticationGate($, fetchResult.finalUrl, title),
3086
3105
  tabbedContent: tabbedContent($)
3087
3106
  } } : {},
@@ -7763,6 +7782,9 @@ function failedMarkdownUrls(discovery) {
7763
7782
  return !negotiatedOk || !explicitOk || page.repeatedHashStable !== true;
7764
7783
  }).map((page) => page.htmlUrl);
7765
7784
  }
7785
+ function unstableMarkdownUrls(discovery) {
7786
+ return discovery.markdownAlternates.pages.filter((page) => confirmedMarkdown(page) !== void 0 && page.repeatedHashStable !== true).map((page) => page.htmlUrl);
7787
+ }
7766
7788
  function qualityFailureUrls(discovery) {
7767
7789
  return discovery.markdownAlternates.pages.filter((page) => {
7768
7790
  const quality = page.quality;
@@ -7776,7 +7798,10 @@ function confirmedMarkdown(page) {
7776
7798
  function markdownChecks(discovery) {
7777
7799
  const markdown = discovery.markdownAlternates;
7778
7800
  const failedUrls = failedMarkdownUrls(discovery);
7801
+ const unstableUrls = unstableMarkdownUrls(discovery);
7779
7802
  const qualityUrls = qualityFailureUrls(discovery);
7803
+ const hasEligibleHtml = markdown.eligibleHtmlPages > 0;
7804
+ const hasEvaluatedMarkdown = markdown.evaluatedPages > 0;
7780
7805
  const negotiatedPages = markdown.pages.filter((page) => page.negotiated?.status === 200 && /^\s*text\/markdown\b/iu.test(page.negotiated.contentType ?? "")).length;
7781
7806
  const explicitPages = markdown.pages.filter((page) => page.htmlAlternateUnique).length;
7782
7807
  const coverageComplete = markdown.eligibleHtmlPages > 0 && markdown.evaluatedPages === markdown.eligibleHtmlPages;
@@ -7805,25 +7830,28 @@ function markdownChecks(discovery) {
7805
7830
  check$2("representations", {
7806
7831
  id: "markdown-coverage",
7807
7832
  status: markdown.eligibleHtmlPages === 0 ? "unknown" : coverageComplete ? "pass" : "fail",
7808
- title: coverageComplete ? "Every successful HTML page has a Markdown representation" : "Markdown representation coverage is incomplete",
7809
- plainEnglish: `${markdown.evaluatedPages} of ${markdown.eligibleHtmlPages} successful HTML pages returned Markdown. ${explicitPages} advertised an explicit alternative and ${negotiatedPages} supported content negotiation.`,
7810
- action: coverageComplete ? "Keep the negotiated or explicit Markdown response stable as routes change." : "Return text/markdown through content negotiation or advertise one working Markdown alternative for each public HTML page.",
7833
+ title: coverageComplete ? "Every successful HTML page has a Markdown representation" : !hasEligibleHtml ? "Markdown representation coverage was not evaluated" : "Markdown representation coverage is incomplete",
7834
+ plainEnglish: hasEligibleHtml ? `${markdown.evaluatedPages} of ${markdown.eligibleHtmlPages} successful HTML pages returned Markdown. ${explicitPages} advertised an explicit alternative and ${negotiatedPages} supported content negotiation.` : "No successful HTML page was available, so Markdown representation coverage could not be evaluated.",
7835
+ action: !hasEligibleHtml ? "Resolve the crawl or page-fetch failure, then run this check again." : coverageComplete ? "Keep the negotiated or explicit Markdown response stable as routes change." : "Return text/markdown through content negotiation or advertise one working Markdown alternative for each public HTML page.",
7811
7836
  evidence: {
7812
7837
  eligibleHtmlPages: markdown.eligibleHtmlPages,
7813
7838
  advertisedPages: markdown.advertisedPages,
7814
7839
  negotiatedPages,
7815
7840
  evaluatedPages: markdown.evaluatedPages
7816
7841
  },
7817
- urls: failedUrls.slice(0, 25)
7842
+ urls: hasEligibleHtml ? failedUrls.slice(0, 25) : []
7818
7843
  }),
7819
7844
  check$2("representations", {
7820
7845
  id: "markdown-token-estimates",
7821
7846
  status: markdown.evaluatedPages === 0 ? "unknown" : tokenHeaders === markdown.evaluatedPages ? "pass" : "info",
7822
- title: tokenHeaders === markdown.evaluatedPages ? "Every Markdown response includes a token estimate" : "Some Markdown responses are missing a stable token estimate",
7823
- plainEnglish: `${tokenHeaders} of ${markdown.evaluatedPages} evaluated pages returned X-Markdown-Tokens. Paired explicit and negotiated responses agreed where both existed. The value is an estimate, not an exact model-specific token count.`,
7824
- action: tokenHeaders === markdown.evaluatedPages ? "Keep the estimate stable when both explicit and negotiated responses exist." : "No change is required. X-Markdown-Tokens is optional; local size estimates are reported separately.",
7847
+ title: !hasEvaluatedMarkdown ? "Markdown token estimates were not evaluated" : tokenHeaders === markdown.evaluatedPages ? "Every Markdown response includes a token estimate" : "Some Markdown responses are missing a stable token estimate",
7848
+ plainEnglish: hasEvaluatedMarkdown ? `${tokenHeaders} of ${markdown.evaluatedPages} evaluated pages returned X-Markdown-Tokens. Paired explicit and negotiated responses agreed where both existed. The value is an estimate, not an exact model-specific token count.` : "No confirmed Markdown response was available, so optional server token estimates could not be evaluated.",
7849
+ action: !hasEvaluatedMarkdown ? "Confirm a successful text/markdown representation, then run this check again." : tokenHeaders === markdown.evaluatedPages ? "Keep the estimate stable when both explicit and negotiated responses exist." : "No change is required. X-Markdown-Tokens is optional; local size estimates are reported separately.",
7825
7850
  urls: markdown.pages.filter((page) => {
7826
- return confirmedMarkdown(page)?.markdownTokens === void 0 || page.explicit !== void 0 && page.negotiated !== void 0 && page.explicit.markdownTokens !== page.negotiated.markdownTokens;
7851
+ const primary = confirmedMarkdown(page);
7852
+ if (!primary) return false;
7853
+ const paired = [page.explicit, page.negotiated].filter((response) => response?.status !== void 0 && response.status >= 200 && response.status < 300 && /^\s*text\/markdown\b/iu.test(response.contentType ?? ""));
7854
+ return primary?.markdownTokens === void 0 || paired.length === 2 && paired[0]?.markdownTokens !== paired[1]?.markdownTokens;
7827
7855
  }).map((page) => page.htmlUrl).slice(0, 25)
7828
7856
  }),
7829
7857
  check$2("representations", {
@@ -7831,7 +7859,7 @@ function markdownChecks(discovery) {
7831
7859
  status: sizedPages.length === 0 ? "unknown" : largePages.length === 0 ? "pass" : "warning",
7832
7860
  title: sizedPages.length === 0 ? "Markdown size was not measured" : largePages.length === 0 ? "Markdown representations stay within the review threshold" : "Some Markdown representations are large enough to review",
7833
7861
  plainEnglish: `${sizedPages.length} Markdown responses were measured locally. ${largePages.length} exceeded 50,000 characters and ${veryLargePages.length} exceeded 100,000 characters. These are processing-risk thresholds, not universal model limits.`,
7834
- action: largePages.length === 0 ? "Keep important content early and retain stable section structure as pages grow." : "Split genuinely separate topics, keep navigation and repeated chrome out of Markdown, and put the most useful content early. Do not remove necessary detail just to hit a threshold.",
7862
+ action: sizedPages.length === 0 ? "Confirm a successful text/markdown representation, then run this check again." : largePages.length === 0 ? "Keep important content early and retain stable section structure as pages grow." : "Split genuinely separate topics, keep navigation and repeated chrome out of Markdown, and put the most useful content early. Do not remove necessary detail just to hit a threshold.",
7835
7863
  evidence: {
7836
7864
  measuredPages: sizedPages.length,
7837
7865
  reviewThresholdCharacters: 5e4,
@@ -7849,7 +7877,7 @@ function markdownChecks(discovery) {
7849
7877
  status: parityPages.length === 0 ? "unknown" : parityFailures.length === 0 ? "pass" : "warning",
7850
7878
  title: parityPages.length === 0 ? "HTML and Markdown content parity was not measured" : parityFailures.length === 0 ? "Markdown retains sampled content from the HTML document" : "Some Markdown responses may omit important HTML content",
7851
7879
  plainEnglish: `${parityPages.length} pages were compared using bounded hashes sampled across the main HTML content. ${parityFailures.length} retained less than 60% of that sample. This detects likely omissions without storing full page bodies.`,
7852
- action: parityFailures.length === 0 ? "Keep the shared conversion path covered as templates and components change." : "Compare the affected HTML and Markdown around the missing sections, then fix the shared converter or component serialization.",
7880
+ action: parityPages.length === 0 ? "Confirm a successful text/markdown representation, then run this check again." : parityFailures.length === 0 ? "Keep the shared conversion path covered as templates and components change." : "Compare the affected HTML and Markdown around the missing sections, then fix the shared converter or component serialization.",
7853
7881
  evidence: {
7854
7882
  evaluatedPages: parityPages.length,
7855
7883
  failedPages: parityFailures.length,
@@ -7874,32 +7902,32 @@ function markdownChecks(discovery) {
7874
7902
  check$2("representations", {
7875
7903
  id: "markdown-negotiation",
7876
7904
  status: markdown.eligibleHtmlPages === 0 ? "unknown" : negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "pass" : "fail",
7877
- title: negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "Markdown content negotiation works across the site" : "Markdown content negotiation needs attention",
7878
- plainEnglish: `${negotiatedPages} of ${markdown.eligibleHtmlPages} pages returned negotiated Markdown. ${markdown.exactByteMatches} of ${explicitPages} paired explicit responses matched byte for byte. A request that refuses Markdown ${discovery.contentNegotiation.qZeroHonoured ? "received HTML" : "did not produce confirmed HTML evidence"}.`,
7879
- action: "Honour Accept q-values, send Vary: Accept, and keep paired explicit and negotiated responses identical when both exist.",
7905
+ title: !hasEligibleHtml ? "Markdown content negotiation was not evaluated" : negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "Markdown content negotiation works across the site" : "Markdown content negotiation needs attention",
7906
+ plainEnglish: hasEligibleHtml ? `${negotiatedPages} of ${markdown.eligibleHtmlPages} pages returned negotiated Markdown. ${markdown.exactByteMatches} of ${explicitPages} paired explicit responses matched byte for byte. A request that refuses Markdown ${discovery.contentNegotiation.qZeroHonoured ? "received HTML" : "did not produce confirmed HTML evidence"}.` : "No successful HTML page was available, so content negotiation could not be evaluated.",
7907
+ action: hasEligibleHtml ? "Honour Accept q-values, send Vary: Accept, and keep paired explicit and negotiated responses identical when both exist." : "Resolve the crawl or page-fetch failure, then run this check again.",
7880
7908
  evidence: {
7881
7909
  negotiatedPages,
7882
7910
  explicitPages,
7883
7911
  exactByteMatches: markdown.exactByteMatches,
7884
7912
  qZero: discovery.contentNegotiation
7885
7913
  },
7886
- urls: failedUrls.slice(0, 25)
7914
+ urls: hasEligibleHtml ? failedUrls.slice(0, 25) : []
7887
7915
  }),
7888
7916
  check$2("representations", {
7889
7917
  id: "markdown-determinism",
7890
7918
  status: markdown.evaluatedPages === 0 ? "unknown" : stable ? "pass" : "fail",
7891
- title: stable ? "Repeated Markdown responses are stable" : "Repeated Markdown responses changed during the audit",
7892
- plainEnglish: `${markdown.stableResponses} of ${markdown.evaluatedPages} repeated Markdown responses kept the same SHA-256 digest.`,
7893
- action: "Remove timestamps, random output, or other runtime rewriting that changes the same Markdown response between requests.",
7919
+ title: !hasEvaluatedMarkdown ? "Markdown response stability was not evaluated" : stable ? "Repeated Markdown responses are stable" : "Repeated Markdown responses changed during the audit",
7920
+ plainEnglish: hasEvaluatedMarkdown ? `${markdown.stableResponses} of ${markdown.evaluatedPages} repeated Markdown responses kept the same SHA-256 digest.` : "No confirmed Markdown response was available for a repeated stability check.",
7921
+ action: !hasEvaluatedMarkdown ? "Confirm a successful text/markdown representation, then run this check again." : stable ? "Keep repeated Markdown responses byte-stable as templates and data change." : "Remove timestamps, random output, or other runtime rewriting that changes the same Markdown response between requests.",
7894
7922
  evidence: { stableResponses: markdown.stableResponses },
7895
- urls: failedUrls.slice(0, 25)
7923
+ urls: unstableUrls.slice(0, 25)
7896
7924
  }),
7897
7925
  check$2("representations", {
7898
7926
  id: "markdown-quality",
7899
7927
  status: markdown.evaluatedPages === 0 ? "unknown" : qualityUrls.length === 0 ? "pass" : "warning",
7900
- title: qualityUrls.length === 0 ? "Markdown keeps the useful document structure cleanly" : "Some Markdown alternatives need an extraction review",
7901
- plainEnglish: qualityUrls.length === 0 ? "The evaluated alternatives retained one H1, frontmatter titles, balanced code fences, useful copy, and no leaked SVG, script, style, or layout tags." : `${qualityUrls.length} Markdown alternative${qualityUrls.length === 1 ? "" : "s"} lost important structure, looked navigation-only, or leaked presentation markup.`,
7902
- action: "Open the affected Markdown directly and fix the shared converter or component exclusion rules rather than patching generated files.",
7928
+ title: !hasEvaluatedMarkdown ? "Markdown quality was not evaluated" : qualityUrls.length === 0 ? "Markdown keeps the useful document structure cleanly" : "Some Markdown alternatives need an extraction review",
7929
+ plainEnglish: !hasEvaluatedMarkdown ? "No confirmed Markdown response was available for structure and extraction checks." : qualityUrls.length === 0 ? "The evaluated alternatives retained one H1, frontmatter titles, balanced code fences, useful copy, and no leaked SVG, script, style, or layout tags." : `${qualityUrls.length} Markdown alternative${qualityUrls.length === 1 ? "" : "s"} lost important structure, looked navigation-only, or leaked presentation markup.`,
7930
+ action: !hasEvaluatedMarkdown ? "Confirm a successful text/markdown representation, then run this check again." : qualityUrls.length === 0 ? "Keep the shared Markdown conversion path covered as templates and components change." : "Open the affected Markdown directly and fix the shared converter or component exclusion rules rather than patching generated files.",
7903
7931
  urls: qualityUrls.slice(0, 25)
7904
7932
  })
7905
7933
  ];
@@ -9819,7 +9847,8 @@ const RULE_DEFINITIONS = [
9819
9847
  evidenceFields: [
9820
9848
  "page.url",
9821
9849
  "page.finalUrl",
9822
- "page.status"
9850
+ "page.status",
9851
+ "page.metaRefresh"
9823
9852
  ],
9824
9853
  suggestedCommands: ["seo redirect-trace --url <url> --json"]
9825
9854
  }
@@ -10313,6 +10342,7 @@ function comparableUrl(value) {
10313
10342
  }
10314
10343
  }
10315
10344
  function isRedirectedPage(page) {
10345
+ if (page.metaRefresh) return true;
10316
10346
  const requested = comparableUrl(page.url);
10317
10347
  const final = comparableUrl(page.finalUrl);
10318
10348
  return Boolean(requested && final && requested !== final);
@@ -10587,11 +10617,18 @@ function auditCrawlPages(pages, opts = {}) {
10587
10617
  }
10588
10618
  if (page.status < 200) continue;
10589
10619
  const redirected = isRedirectedPage(page);
10590
- if (redirected) issues.push(issue("redirected_url", page, `Final URL: ${page.finalUrl}`, {
10591
- requestedUrl: page.url,
10592
- finalUrl: page.finalUrl,
10593
- status: page.status
10594
- }));
10620
+ if (redirected) {
10621
+ const redirectTarget = page.metaRefresh?.targetUrl ?? page.finalUrl;
10622
+ issues.push(issue("redirected_url", page, `Final URL: ${redirectTarget}`, {
10623
+ requestedUrl: page.url,
10624
+ finalUrl: redirectTarget,
10625
+ status: page.status,
10626
+ ...page.metaRefresh ? {
10627
+ redirectType: "meta-refresh",
10628
+ delaySeconds: page.metaRefresh.delaySeconds
10629
+ } : {}
10630
+ }));
10631
+ }
10595
10632
  const redirectChain = page.fetchDiagnostics?.redirectChain ?? [];
10596
10633
  if (redirectChain.length > 1) issues.push(issue("redirect_chain", page, `${redirectChain.length} hops`, {
10597
10634
  redirectChain,
@@ -11437,7 +11474,7 @@ function latestCrawlReport(site, store = crawlReportStore) {
11437
11474
  //#region packages/core/dist/analyze/crawler/crawl-caveats.js
11438
11475
  function crawlCaveats(input) {
11439
11476
  const caveats = input.cancelled ? ["Crawl cancelled before all queued URLs finished."] : input.pageLimitReached ? [`Stopped after reaching maxPages (${input.maxPages}).`] : [];
11440
- if (input.queueSafetySkippedUrls > 0) caveats.push(`Left ${input.queueSafetySkippedUrls} eligible same-origin URLs unqueued to keep this crawl bounded. Increase --max-pages to inspect more.`);
11477
+ if (input.queueSafetySkippedUrls > 0) caveats.push(input.queueSafetyInventoryCapped ? `Left at least ${input.queueSafetySkippedUrls} unique eligible same-origin URLs unqueued to keep this crawl bounded. The excluded-URL inventory reached its reporting cap. Increase --max-pages to inspect more.` : `Left ${input.queueSafetySkippedUrls} unique eligible same-origin URLs unqueued to keep this crawl bounded. Increase --max-pages to inspect more.`);
11441
11478
  if (input.originBackpressureSkippedUrls > 0) caveats.push(`Stopped ${input.originBackpressureSkippedUrls} queued URL${input.originBackpressureSkippedUrls === 1 ? "" : "s"} because the origin stayed slow. This is incomplete crawl evidence, not a failed URL check.`);
11442
11479
  if (input.memoryPressureSkippedUrls > 0) caveats.push(`Left ${input.memoryPressureSkippedUrls} eligible URL${input.memoryPressureSkippedUrls === 1 ? "" : "s"} unchecked after the local memory safety limit was reached. Start with --health or lower --max-pages.`);
11443
11480
  return caveats;
@@ -12590,6 +12627,7 @@ async function crawlOne(url, opts = {}) {
12590
12627
  contentSample: truncate$1(extracted.contentText, 300),
12591
12628
  contentSketch: crawlerEvidence.contentSketch,
12592
12629
  softAuthenticationGate: crawlerEvidence.softAuthenticationGate,
12630
+ metaRefresh: crawlerEvidence.metaRefresh,
12593
12631
  tabbedContent: crawlerEvidence.tabbedContent,
12594
12632
  lang: extracted.lang,
12595
12633
  hasViewport: extracted.hasViewport,
@@ -12640,9 +12678,9 @@ async function crawlOne(url, opts = {}) {
12640
12678
  hasQapageSchema: extracted.schemaTypes.includes("QAPage")
12641
12679
  }
12642
12680
  };
12643
- page.indexability = indexabilityReason(page);
12681
+ page.indexability = page.metaRefresh ? `Meta refresh to ${page.metaRefresh.targetUrl}` : indexabilityReason(page);
12644
12682
  page.indexable = !page.indexability;
12645
- page.declaredIndexability = hasMetaRobotsDirective(page.metaRobots, "noindex") || hasXRobotsDirective(page.xRobotsTag, "noindex") ? "noindex" : page.robotsTxt?.allowed === false ? "robots-blocked" : page.canonicalStatus === "conflicting" ? "canonical-conflict" : page.canonical && !sameDocumentUrl(page.canonical, page.finalUrl) ? "canonical-hint-other" : "indexable-candidate";
12683
+ page.declaredIndexability = page.metaRefresh ? "meta-refresh" : hasMetaRobotsDirective(page.metaRobots, "noindex") || hasXRobotsDirective(page.xRobotsTag, "noindex") ? "noindex" : page.robotsTxt?.allowed === false ? "robots-blocked" : page.canonicalStatus === "conflicting" ? "canonical-conflict" : page.canonical && !sameDocumentUrl(page.canonical, page.finalUrl) ? "canonical-hint-other" : "indexable-candidate";
12646
12684
  return {
12647
12685
  request: responseObservation(url, fetched, "complete"),
12648
12686
  page,
@@ -13436,6 +13474,9 @@ async function crawlSite(input, dependencies) {
13436
13474
  let robotsDeferredUrls = 0;
13437
13475
  let originBackpressureSkippedUrls = 0;
13438
13476
  let queueSafetySkippedUrls = 0;
13477
+ const queueSafetyDeferredUrls = /* @__PURE__ */ new Set();
13478
+ const maxTrackedQueueSafetyUrls = Math.min(5e4, Math.max(1e3, config.maxPages * 5));
13479
+ let queueSafetyInventoryCapped = false;
13439
13480
  let failedUrls = 0;
13440
13481
  let observedInternalLinks = 0;
13441
13482
  let memoryLimited = false;
@@ -13540,13 +13581,16 @@ async function crawlSite(input, dependencies) {
13540
13581
  return;
13541
13582
  }
13542
13583
  if (queue.size + visited.size + inFlight.size >= config.maxPages * 5) {
13543
- recordSkip("queue-safety-limit");
13544
- queueSafetySkippedUrls += 1;
13545
- emitStatus("url_skipped", {
13546
- url: normalized,
13547
- depth,
13548
- reason: "queue_safety_limit"
13549
- });
13584
+ if (!queueSafetyDeferredUrls.has(normalized)) if (queueSafetyDeferredUrls.size < maxTrackedQueueSafetyUrls) {
13585
+ queueSafetyDeferredUrls.add(normalized);
13586
+ queueSafetySkippedUrls = queueSafetyDeferredUrls.size;
13587
+ recordSkip("queue-safety-limit");
13588
+ emitStatus("url_skipped", {
13589
+ url: normalized,
13590
+ depth,
13591
+ reason: "queue_safety_limit"
13592
+ });
13593
+ } else queueSafetyInventoryCapped = true;
13550
13594
  return;
13551
13595
  }
13552
13596
  discovered.add(normalized);
@@ -13919,6 +13963,7 @@ async function crawlSite(input, dependencies) {
13919
13963
  pageLimitReached,
13920
13964
  maxPages: config.maxPages,
13921
13965
  queueSafetySkippedUrls,
13966
+ queueSafetyInventoryCapped,
13922
13967
  originBackpressureSkippedUrls,
13923
13968
  memoryPressureSkippedUrls
13924
13969
  }),
@@ -15401,6 +15446,7 @@ const crawlPageSnapshotSchema = z.object({
15401
15446
  "robots-blocked",
15402
15447
  "canonical-conflict",
15403
15448
  "canonical-hint-other",
15449
+ "meta-refresh",
15404
15450
  "not-html",
15405
15451
  "unknown"
15406
15452
  ]).optional(),
@@ -15447,6 +15493,10 @@ const crawlPageSnapshotSchema = z.object({
15447
15493
  indicators: z.array(z.string()),
15448
15494
  formActionPath: z.string().optional()
15449
15495
  }).optional(),
15496
+ metaRefresh: z.object({
15497
+ targetUrl: z.string().url(),
15498
+ delaySeconds: z.number().nonnegative()
15499
+ }).optional(),
15450
15500
  tabbedContent: z.object({
15451
15501
  groups: z.number().int().positive(),
15452
15502
  panels: z.number().int().positive(),