seo 0.2.12 → 0.2.13

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-M-4PZ_.js";
2
+ import { n as load, t as version } from "./package-B_swz2Xd.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";
@@ -2134,6 +2134,43 @@ function extractCanonicalEvidence($, headers, baseUrl) {
2134
2134
  return evidenceFromCandidates([...htmlCandidates($, baseUrl), ...headerCandidates(headers, baseUrl)]);
2135
2135
  }
2136
2136
  //#endregion
2137
+ //#region packages/core/dist/extract/content-sketch.js
2138
+ const SHINGLE_SIZE = 5;
2139
+ const MAX_HASHES = 32;
2140
+ function words(value) {
2141
+ return value.normalize("NFKC").toLocaleLowerCase("en").match(/[\p{L}\p{N}]+/gu) ?? [];
2142
+ }
2143
+ function sampleIndexes(total, limit) {
2144
+ if (total <= limit) return Array.from({ length: total }, (_, index) => index);
2145
+ return Array.from({ length: limit }, (_, index) => Math.round(index * (total - 1) / (limit - 1)));
2146
+ }
2147
+ function digest(value) {
2148
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
2149
+ }
2150
+ function contentSketch(value) {
2151
+ const tokens = words(value);
2152
+ const hashes = sampleIndexes(Math.max(0, tokens.length - SHINGLE_SIZE + 1), MAX_HASHES).map((index) => digest(tokens.slice(index, index + SHINGLE_SIZE).join(" ")));
2153
+ return {
2154
+ version: 1,
2155
+ wordCount: tokens.length,
2156
+ shingleSize: SHINGLE_SIZE,
2157
+ sampledShingles: hashes.length,
2158
+ hashes
2159
+ };
2160
+ }
2161
+ function contentSketchCoverage(source, candidate) {
2162
+ if (!source || source.hashes.length < 4) return null;
2163
+ const targets = new Set(source.hashes);
2164
+ const matched = /* @__PURE__ */ new Set();
2165
+ const tokens = words(candidate);
2166
+ for (let index = 0; index <= tokens.length - source.shingleSize; index += 1) {
2167
+ const hash = digest(tokens.slice(index, index + source.shingleSize).join(" "));
2168
+ if (targets.has(hash)) matched.add(hash);
2169
+ if (matched.size === targets.size) break;
2170
+ }
2171
+ return Math.round(matched.size / targets.size * 1e3) / 1e3;
2172
+ }
2173
+ //#endregion
2137
2174
  //#region packages/core/dist/extract/main-content.js
2138
2175
  function readableError(error) {
2139
2176
  return error instanceof Error ? error.message : String(error);
@@ -2751,6 +2788,48 @@ const SOCIAL_PROFILE_HOSTS = [
2751
2788
  "x.com",
2752
2789
  "youtube.com"
2753
2790
  ];
2791
+ const LOGIN_PATH_PATTERN = /(?:^|\/)(?:auth|login|log-in|signin|sign-in|sso)(?:\/|$)/iu;
2792
+ const LOGIN_TITLE_PATTERN = /\b(?:authenticate|log[ -]?in|sign[ -]?in)\b/iu;
2793
+ function softAuthenticationGate($, finalUrl, title) {
2794
+ const passwordForms = $("form").toArray().filter((form) => $(form).find("input[type=\"password\" i]").length > 0);
2795
+ if (passwordForms.length === 0) return void 0;
2796
+ const pagePath = new URL(finalUrl).pathname;
2797
+ const action = $(passwordForms[0]).attr("action");
2798
+ const actionUrl = action ? httpUrl$1(action, finalUrl) : void 0;
2799
+ const actionPath = actionUrl ? new URL(actionUrl).pathname : void 0;
2800
+ const indicators = [
2801
+ ...LOGIN_PATH_PATTERN.test(pagePath) ? ["login-path"] : [],
2802
+ ...LOGIN_TITLE_PATTERN.test(title ?? "") ? ["login-title"] : [],
2803
+ ...LOGIN_PATH_PATTERN.test(actionPath ?? "") ? ["login-form-action"] : []
2804
+ ];
2805
+ if (indicators.length === 0) return void 0;
2806
+ return {
2807
+ kind: "login-form",
2808
+ indicators,
2809
+ ...actionPath ? { formActionPath: actionPath } : {}
2810
+ };
2811
+ }
2812
+ function tabbedContent($) {
2813
+ const groups = $("[role=\"tablist\" i]").length;
2814
+ const panels = $("[role=\"tabpanel\" i]").toArray();
2815
+ if (groups === 0 || panels.length === 0) return void 0;
2816
+ const panelSketches = panels.slice(0, 12).map((panel) => {
2817
+ const labelledBy = $(panel).attr("aria-labelledby");
2818
+ const labelledElement = labelledBy ? $("[id]").toArray().find((element) => $(element).attr("id") === labelledBy) : void 0;
2819
+ const label = safeText$1($(panel).attr("aria-label") ?? (labelledElement ? $(labelledElement).text() : void 0));
2820
+ return {
2821
+ ...label ? { label: label.slice(0, 80) } : {},
2822
+ sketch: contentSketch($(panel).text())
2823
+ };
2824
+ });
2825
+ return {
2826
+ groups,
2827
+ panels: panels.length,
2828
+ retainedPanels: panelSketches.length,
2829
+ truncated: panels.length > panelSketches.length,
2830
+ panelSketches
2831
+ };
2832
+ }
2754
2833
  function safeText$1(value) {
2755
2834
  const trimmed = value?.replace(/\s+/g, " ").trim();
2756
2835
  return trimmed ? trimmed : void 0;
@@ -2977,10 +3056,11 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
2977
3056
  html: sanitizedContentHtml($, fetchResult.html, fetchResult.finalUrl)
2978
3057
  }, extractor, dependencies);
2979
3058
  const { text, excerpt } = content;
3059
+ const title = safeText$1($("title").first().text());
2980
3060
  return {
2981
3061
  url: fetchResult.url,
2982
3062
  finalUrl: fetchResult.finalUrl,
2983
- title: safeText$1($("title").first().text()),
3063
+ title,
2984
3064
  metaDescription: safeText$1($("meta[name=\"description\"]").attr("content")),
2985
3065
  metaRobots,
2986
3066
  xRobotsTag: safeText$1(headerValue$4(fetchResult.headers, "x-robots-tag")),
@@ -3000,7 +3080,10 @@ async function extractPage(fetchResult, extractor = "defuddle", dependencies = {
3000
3080
  externalLinks: [...crawlerExternalLinks],
3001
3081
  internalAnchorSamples,
3002
3082
  externalAnchorSamples,
3003
- socialProfileLinks: [...socialProfileLinks]
3083
+ socialProfileLinks: [...socialProfileLinks],
3084
+ contentSketch: contentSketch(text),
3085
+ softAuthenticationGate: softAuthenticationGate($, fetchResult.finalUrl, title),
3086
+ tabbedContent: tabbedContent($)
3004
3087
  } } : {},
3005
3088
  markdownAlternates,
3006
3089
  hreflang,
@@ -7105,6 +7188,8 @@ function markdownQuality(markdown, page) {
7105
7188
  const codeFences = markdown.match(/^```/gmu)?.length ?? 0;
7106
7189
  const wordCount = bodyWordCount(markdown);
7107
7190
  const sourceWordCount = page.wordCount ?? 0;
7191
+ const evaluatedPanels = (page.tabbedContent?.panelSketches ?? []).map((panel) => contentSketchCoverage(panel.sketch, markdown)).filter((coverage) => coverage !== null);
7192
+ const retainedPanels = evaluatedPanels.filter((coverage) => coverage >= .6).length;
7108
7193
  return {
7109
7194
  frontmatterTitle: /^---[\s\S]*?^title:\s*.+$/mu.test(markdown),
7110
7195
  h1Count: markdown.match(/^#\s+\S.+$/gmu)?.length ?? 0,
@@ -7121,7 +7206,15 @@ function markdownQuality(markdown, page) {
7121
7206
  sourceWordCount,
7122
7207
  wordRetentionRatio: sourceWordCount > 0 ? Math.round(wordCount / sourceWordCount * 1e3) / 1e3 : null,
7123
7208
  introductoryCopyRetained: introProbe ? normalizedMarkdown.includes(introProbe) : null,
7124
- navigationOnly: bodyWordCount(markdown) < 25
7209
+ navigationOnly: bodyWordCount(markdown) < 25,
7210
+ contentSketchCoverage: contentSketchCoverage(page.contentSketch, markdown),
7211
+ tabbedContent: {
7212
+ detectedPanels: page.tabbedContent?.panels ?? 0,
7213
+ evaluatedPanels: evaluatedPanels.length,
7214
+ retainedPanels,
7215
+ missingPanels: Math.max(0, evaluatedPanels.length - retainedPanels),
7216
+ complete: evaluatedPanels.length === 0 ? null : retainedPanels === evaluatedPanels.length
7217
+ }
7125
7218
  };
7126
7219
  }
7127
7220
  function combinedSignal(timeoutMs, signal) {
@@ -7175,6 +7268,8 @@ async function fetchRepresentation(input) {
7175
7268
  status: response.status,
7176
7269
  contentType: response.headers.get("content-type") ?? void 0,
7177
7270
  bytes: Buffer.byteLength(body),
7271
+ characters: body.length,
7272
+ estimatedTokens: Math.ceil(body.length / 4),
7178
7273
  sha256: sha256(body),
7179
7274
  canonicalUrl: link.find((entry) => entry.rel.includes("canonical"))?.url,
7180
7275
  alternateUrl: link.find((entry) => entry.rel.includes("alternate") && entry.type === "text/markdown")?.url,
@@ -7233,11 +7328,15 @@ async function inspectMarkdownPage(input) {
7233
7328
  if (explicit) observation.explicit = explicit.observation;
7234
7329
  observation.negotiated = negotiated.observation;
7235
7330
  observation.repeated = repeated.observation;
7236
- observation.explicitMatchesNegotiated = explicit?.observation.sha256 && negotiated.observation.sha256 ? explicit.observation.sha256 === negotiated.observation.sha256 : null;
7237
- const primary = explicit ?? negotiated;
7238
- observation.repeatedHashStable = primary.observation.sha256 && repeated.observation.sha256 ? primary.observation.sha256 === repeated.observation.sha256 : null;
7239
- observation.markdownCanonicalMatchesHtml = sameDocument(primary.observation.canonicalUrl, input.page.finalUrl);
7240
- if (primary.body) observation.quality = markdownQuality(primary.body, input.page);
7331
+ const isMarkdown = (result) => result?.observation.status !== void 0 && result.observation.status >= 200 && result.observation.status < 300 && /^\s*text\/markdown\b/iu.test(result.observation.contentType ?? "");
7332
+ const explicitMarkdown = isMarkdown(explicit) ? explicit : void 0;
7333
+ const negotiatedMarkdown = isMarkdown(negotiated) ? negotiated : void 0;
7334
+ const repeatedMarkdown = isMarkdown(repeated) ? repeated : void 0;
7335
+ observation.explicitMatchesNegotiated = explicitMarkdown?.observation.sha256 && negotiatedMarkdown?.observation.sha256 ? explicitMarkdown.observation.sha256 === negotiatedMarkdown.observation.sha256 : null;
7336
+ const primary = explicitMarkdown ?? negotiatedMarkdown;
7337
+ observation.repeatedHashStable = primary?.observation.sha256 && repeatedMarkdown?.observation.sha256 ? primary.observation.sha256 === repeatedMarkdown.observation.sha256 : null;
7338
+ observation.markdownCanonicalMatchesHtml = sameDocument(primary?.observation.canonicalUrl, input.page.finalUrl);
7339
+ if (primary?.body) observation.quality = markdownQuality(primary.body, input.page);
7241
7340
  return observation;
7242
7341
  }
7243
7342
  async function fetchText(input) {
@@ -7481,17 +7580,16 @@ async function inspectRouteManifest(input) {
7481
7580
  const declared = Array.isArray(parsed.pages) ? parsed.pages : [];
7482
7581
  const declaredHtmlRoutes = declared.map((page) => page.htmlPath).filter((path) => typeof path === "string").sort();
7483
7582
  const declaredMarkdownRoutes = declared.map((page) => page.markdownPath).filter((path) => typeof path === "string").sort();
7484
- const crawledHtmlRoutes = new Set(input.pages.map((page) => new URL(page.finalUrl).pathname.replace(/\/$/u, "") || "/"));
7485
- const observedMarkdownRoutes = new Set(input.observations.flatMap((page) => page.advertisedUrls.map((value) => new URL(value).pathname)));
7486
7583
  return {
7487
7584
  url,
7488
7585
  status: result.response.status,
7489
7586
  valid: Array.isArray(parsed.pages),
7490
7587
  declaredHtmlRoutes,
7491
7588
  declaredMarkdownRoutes,
7492
- missingHtmlRoutes: declaredHtmlRoutes.filter((path) => !crawledHtmlRoutes.has(path.replace(/\/$/u, "") || "/")).sort(),
7493
- missingMarkdownRoutes: declaredMarkdownRoutes.filter((path) => !observedMarkdownRoutes.has(path)).sort(),
7494
- orphanMarkdownRoutes: [...observedMarkdownRoutes].filter((path) => !declaredMarkdownRoutes.includes(path)).sort()
7589
+ missingHtmlRoutes: [],
7590
+ missingMarkdownRoutes: [],
7591
+ orphanMarkdownRoutes: [],
7592
+ comparisonStatus: "partial"
7495
7593
  };
7496
7594
  } catch (error) {
7497
7595
  return {
@@ -7502,6 +7600,7 @@ async function inspectRouteManifest(input) {
7502
7600
  missingHtmlRoutes: [],
7503
7601
  missingMarkdownRoutes: [],
7504
7602
  orphanMarkdownRoutes: [],
7603
+ comparisonStatus: "not-applicable",
7505
7604
  error: safeError(error)
7506
7605
  };
7507
7606
  }
@@ -7545,9 +7644,7 @@ async function collectAgentDiscovery(input) {
7545
7644
  }),
7546
7645
  inspectRouteManifest({
7547
7646
  ...input,
7548
- origin,
7549
- pages,
7550
- observations
7647
+ origin
7551
7648
  }),
7552
7649
  inspectAgentEndpoints({
7553
7650
  ...input,
@@ -7652,20 +7749,13 @@ async function collectAgentDiscovery(input) {
7652
7749
  };
7653
7750
  }
7654
7751
  //#endregion
7655
- //#region packages/core/dist/analyze/crawler/agent-readiness.js
7656
- function check$1(section, input) {
7752
+ //#region packages/core/dist/analyze/crawler/agent-readiness-markdown.js
7753
+ function check$2(section, input) {
7657
7754
  return {
7658
7755
  section,
7659
7756
  ...input
7660
7757
  };
7661
7758
  }
7662
- function section$2(id, title, checks) {
7663
- return {
7664
- id,
7665
- title,
7666
- checks
7667
- };
7668
- }
7669
7759
  function failedMarkdownUrls(discovery) {
7670
7760
  return discovery.markdownAlternates.pages.filter((page) => {
7671
7761
  const negotiatedOk = page.negotiated?.status === 200 && /^\s*text\/markdown\b/iu.test(page.negotiated.contentType ?? "") && page.negotiated.varyAccept;
@@ -7676,38 +7766,12 @@ function failedMarkdownUrls(discovery) {
7676
7766
  function qualityFailureUrls(discovery) {
7677
7767
  return discovery.markdownAlternates.pages.filter((page) => {
7678
7768
  const quality = page.quality;
7679
- if (!quality) return true;
7680
- return !quality.frontmatterTitle || quality.h1Count !== 1 || !quality.codeFenceBalanced || quality.rawHtmlTags > 0 || quality.rawSvgTags > 0 || quality.rawScriptTags > 0 || quality.rawStyleTags > 0 || quality.navigationOnly || quality.repeatedLines > 0 || quality.wordRetentionRatio !== null && quality.wordRetentionRatio < .4;
7769
+ if (!quality) return false;
7770
+ return !quality.frontmatterTitle || quality.h1Count !== 1 || !quality.codeFenceBalanced || quality.rawHtmlTags > 0 || quality.rawSvgTags > 0 || quality.rawScriptTags > 0 || quality.rawStyleTags > 0 || quality.navigationOnly || quality.repeatedLines > 0 || quality.contentSketchCoverage !== null && quality.contentSketchCoverage !== void 0 && quality.contentSketchCoverage < .6 || quality.tabbedContent?.complete === false || quality.wordRetentionRatio !== null && quality.wordRetentionRatio < .4;
7681
7771
  }).map((page) => page.htmlUrl);
7682
7772
  }
7683
- function profileChecks(discovery) {
7684
- return [
7685
- "api",
7686
- "application",
7687
- "commerce"
7688
- ].map((profile) => check$1("profile", {
7689
- id: `profile-${profile}`,
7690
- status: "notApplicable",
7691
- title: `${profile[0]?.toUpperCase()}${profile.slice(1)} checks are not applicable`,
7692
- plainEnglish: discovery.profileApplicability[profile].reason,
7693
- action: "Choose this profile only when the site exposes that capability publicly.",
7694
- evidence: {
7695
- profile,
7696
- selectedProfile: discovery.profile
7697
- }
7698
- }));
7699
- }
7700
- function summariseChecks(checks) {
7701
- const count = (status) => checks.filter((item) => item.status === status).length;
7702
- return {
7703
- checks: checks.length,
7704
- passed: count("pass"),
7705
- warnings: count("warning"),
7706
- failed: count("fail"),
7707
- unknown: count("unknown"),
7708
- information: count("info"),
7709
- notApplicable: count("notApplicable")
7710
- };
7773
+ function confirmedMarkdown(page) {
7774
+ for (const response of [page.explicit, page.negotiated]) if (response?.status !== void 0 && response.status >= 200 && response.status < 300 && /^\s*text\/markdown\b/iu.test(response.contentType ?? "")) return response;
7711
7775
  }
7712
7776
  function markdownChecks(discovery) {
7713
7777
  const markdown = discovery.markdownAlternates;
@@ -7717,12 +7781,28 @@ function markdownChecks(discovery) {
7717
7781
  const explicitPages = markdown.pages.filter((page) => page.htmlAlternateUnique).length;
7718
7782
  const coverageComplete = markdown.eligibleHtmlPages > 0 && markdown.evaluatedPages === markdown.eligibleHtmlPages;
7719
7783
  const negotiationComplete = markdown.eligibleHtmlPages > 0 && negotiatedPages === markdown.eligibleHtmlPages && markdown.pages.every((page) => page.negotiated?.varyAccept) && markdown.pages.every((page) => !page.htmlAlternateUnique || page.explicitMatchesNegotiated === true);
7720
- const stable = markdown.eligibleHtmlPages > 0 && markdown.stableResponses === markdown.eligibleHtmlPages;
7784
+ const stable = markdown.evaluatedPages > 0 && markdown.stableResponses === markdown.evaluatedPages;
7721
7785
  const tokenHeaders = markdown.pages.filter((page) => {
7722
- return (page.explicit ?? page.negotiated)?.markdownTokens !== void 0 && (!page.explicit || !page.negotiated || page.explicit.markdownTokens === page.negotiated.markdownTokens);
7786
+ return confirmedMarkdown(page)?.markdownTokens !== void 0 && (!page.explicit || !page.negotiated || page.explicit.markdownTokens === page.negotiated.markdownTokens);
7723
7787
  }).length;
7788
+ const sizedPages = markdown.pages.filter((page) => {
7789
+ return confirmedMarkdown(page)?.characters !== void 0;
7790
+ });
7791
+ const largePages = sizedPages.filter((page) => {
7792
+ return (confirmedMarkdown(page)?.characters ?? 0) > 5e4;
7793
+ });
7794
+ const veryLargePages = sizedPages.filter((page) => {
7795
+ return (confirmedMarkdown(page)?.characters ?? 0) > 1e5;
7796
+ });
7797
+ const maxCharacters = Math.max(0, ...sizedPages.map((page) => confirmedMarkdown(page)?.characters ?? 0));
7798
+ const maxEstimatedTokens = Math.max(0, ...sizedPages.map((page) => confirmedMarkdown(page)?.estimatedTokens ?? 0));
7799
+ const parityPages = markdown.pages.filter((page) => page.quality?.contentSketchCoverage !== null && page.quality?.contentSketchCoverage !== void 0);
7800
+ const parityFailures = parityPages.filter((page) => (page.quality?.contentSketchCoverage ?? 0) < .6);
7801
+ const tabbedPages = markdown.pages.filter((page) => (page.quality?.tabbedContent?.detectedPanels ?? 0) > 0);
7802
+ const tabFailures = tabbedPages.filter((page) => page.quality?.tabbedContent?.complete === false);
7803
+ const tabUnknown = tabbedPages.filter((page) => page.quality?.tabbedContent?.complete === null);
7724
7804
  return [
7725
- check$1("representations", {
7805
+ check$2("representations", {
7726
7806
  id: "markdown-coverage",
7727
7807
  status: markdown.eligibleHtmlPages === 0 ? "unknown" : coverageComplete ? "pass" : "fail",
7728
7808
  title: coverageComplete ? "Every successful HTML page has a Markdown representation" : "Markdown representation coverage is incomplete",
@@ -7736,17 +7816,62 @@ function markdownChecks(discovery) {
7736
7816
  },
7737
7817
  urls: failedUrls.slice(0, 25)
7738
7818
  }),
7739
- check$1("representations", {
7819
+ check$2("representations", {
7740
7820
  id: "markdown-token-estimates",
7741
- status: markdown.evaluatedPages === 0 ? "unknown" : tokenHeaders === markdown.evaluatedPages ? "pass" : "warning",
7821
+ status: markdown.evaluatedPages === 0 ? "unknown" : tokenHeaders === markdown.evaluatedPages ? "pass" : "info",
7742
7822
  title: tokenHeaders === markdown.evaluatedPages ? "Every Markdown response includes a token estimate" : "Some Markdown responses are missing a stable token estimate",
7743
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.`,
7744
- action: "Return a stable token estimate and keep it consistent when both explicit and negotiated responses exist.",
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.",
7745
7825
  urls: markdown.pages.filter((page) => {
7746
- return (page.explicit ?? page.negotiated)?.markdownTokens === void 0 || page.explicit !== void 0 && page.negotiated !== void 0 && page.explicit.markdownTokens !== page.negotiated.markdownTokens;
7826
+ return confirmedMarkdown(page)?.markdownTokens === void 0 || page.explicit !== void 0 && page.negotiated !== void 0 && page.explicit.markdownTokens !== page.negotiated.markdownTokens;
7747
7827
  }).map((page) => page.htmlUrl).slice(0, 25)
7748
7828
  }),
7749
- check$1("representations", {
7829
+ check$2("representations", {
7830
+ id: "markdown-size",
7831
+ status: sizedPages.length === 0 ? "unknown" : largePages.length === 0 ? "pass" : "warning",
7832
+ 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
+ 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.",
7835
+ evidence: {
7836
+ measuredPages: sizedPages.length,
7837
+ reviewThresholdCharacters: 5e4,
7838
+ highRiskThresholdCharacters: 1e5,
7839
+ largePages: largePages.length,
7840
+ veryLargePages: veryLargePages.length,
7841
+ maxCharacters,
7842
+ maxEstimatedTokens,
7843
+ heuristic: true
7844
+ },
7845
+ urls: largePages.map((page) => page.htmlUrl).slice(0, 25)
7846
+ }),
7847
+ check$2("representations", {
7848
+ id: "markdown-content-parity",
7849
+ status: parityPages.length === 0 ? "unknown" : parityFailures.length === 0 ? "pass" : "warning",
7850
+ 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
+ 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.",
7853
+ evidence: {
7854
+ evaluatedPages: parityPages.length,
7855
+ failedPages: parityFailures.length,
7856
+ minimumCoverage: .6,
7857
+ sampling: "bounded-content-shingles"
7858
+ },
7859
+ urls: parityFailures.map((page) => page.htmlUrl).slice(0, 25)
7860
+ }),
7861
+ check$2("representations", {
7862
+ id: "markdown-tab-serialization",
7863
+ status: tabbedPages.length === 0 ? "notApplicable" : tabFailures.length > 0 ? "warning" : tabUnknown.length > 0 ? "unknown" : "pass",
7864
+ title: tabbedPages.length === 0 ? "No accessible tabbed content was detected" : tabFailures.length > 0 ? "Some tab panels are missing from Markdown" : tabUnknown.length > 0 ? "Some tab panels were too small to compare reliably" : "Markdown retains detected tab panels",
7865
+ plainEnglish: tabbedPages.length === 0 ? "The crawled HTML did not contain ARIA tablists with tab panels, so this check does not apply." : `${tabbedPages.length} pages contained accessible tab panels. ${tabFailures.length} did not retain every evaluated panel in Markdown, and ${tabUnknown.length} had no panel large enough for a reliable comparison.`,
7866
+ action: tabFailures.length === 0 ? tabUnknown.length > 0 ? "Review the affected page directly if the short tab labels or values are important to the document." : "Keep every panel serialized in document order even when only one tab is visible by default." : "Serialize all tab panels into the Markdown document with headings instead of exporting only the initially active panel.",
7867
+ evidence: {
7868
+ tabbedPages: tabbedPages.length,
7869
+ failedPages: tabFailures.length,
7870
+ unknownPages: tabUnknown.length
7871
+ },
7872
+ urls: [...tabFailures, ...tabUnknown].map((page) => page.htmlUrl).slice(0, 25)
7873
+ }),
7874
+ check$2("representations", {
7750
7875
  id: "markdown-negotiation",
7751
7876
  status: markdown.eligibleHtmlPages === 0 ? "unknown" : negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "pass" : "fail",
7752
7877
  title: negotiationComplete && discovery.contentNegotiation.qZeroHonoured ? "Markdown content negotiation works across the site" : "Markdown content negotiation needs attention",
@@ -7760,16 +7885,16 @@ function markdownChecks(discovery) {
7760
7885
  },
7761
7886
  urls: failedUrls.slice(0, 25)
7762
7887
  }),
7763
- check$1("representations", {
7888
+ check$2("representations", {
7764
7889
  id: "markdown-determinism",
7765
- status: markdown.eligibleHtmlPages === 0 ? "unknown" : stable ? "pass" : "fail",
7890
+ status: markdown.evaluatedPages === 0 ? "unknown" : stable ? "pass" : "fail",
7766
7891
  title: stable ? "Repeated Markdown responses are stable" : "Repeated Markdown responses changed during the audit",
7767
- plainEnglish: `${markdown.stableResponses} of ${markdown.eligibleHtmlPages} repeated Markdown responses kept the same SHA-256 digest.`,
7892
+ plainEnglish: `${markdown.stableResponses} of ${markdown.evaluatedPages} repeated Markdown responses kept the same SHA-256 digest.`,
7768
7893
  action: "Remove timestamps, random output, or other runtime rewriting that changes the same Markdown response between requests.",
7769
7894
  evidence: { stableResponses: markdown.stableResponses },
7770
7895
  urls: failedUrls.slice(0, 25)
7771
7896
  }),
7772
- check$1("representations", {
7897
+ check$2("representations", {
7773
7898
  id: "markdown-quality",
7774
7899
  status: markdown.evaluatedPages === 0 ? "unknown" : qualityUrls.length === 0 ? "pass" : "warning",
7775
7900
  title: qualityUrls.length === 0 ? "Markdown keeps the useful document structure cleanly" : "Some Markdown alternatives need an extraction review",
@@ -7779,6 +7904,50 @@ function markdownChecks(discovery) {
7779
7904
  })
7780
7905
  ];
7781
7906
  }
7907
+ //#endregion
7908
+ //#region packages/core/dist/analyze/crawler/agent-readiness.js
7909
+ function check$1(section, input) {
7910
+ return {
7911
+ section,
7912
+ ...input
7913
+ };
7914
+ }
7915
+ function section$2(id, title, checks) {
7916
+ return {
7917
+ id,
7918
+ title,
7919
+ checks
7920
+ };
7921
+ }
7922
+ function profileChecks(discovery) {
7923
+ return [
7924
+ "api",
7925
+ "application",
7926
+ "commerce"
7927
+ ].map((profile) => check$1("profile", {
7928
+ id: `profile-${profile}`,
7929
+ status: "notApplicable",
7930
+ title: `${profile[0]?.toUpperCase()}${profile.slice(1)} checks are not applicable`,
7931
+ plainEnglish: discovery.profileApplicability[profile].reason,
7932
+ action: "Choose this profile only when the site exposes that capability publicly.",
7933
+ evidence: {
7934
+ profile,
7935
+ selectedProfile: discovery.profile
7936
+ }
7937
+ }));
7938
+ }
7939
+ function summariseChecks(checks) {
7940
+ const count = (status) => checks.filter((item) => item.status === status).length;
7941
+ return {
7942
+ checks: checks.length,
7943
+ passed: count("pass"),
7944
+ warnings: count("warning"),
7945
+ failed: count("fail"),
7946
+ unknown: count("unknown"),
7947
+ information: count("info"),
7948
+ notApplicable: count("notApplicable")
7949
+ };
7950
+ }
7782
7951
  function contentSignalsCheck(discovery, robotsContentSignals) {
7783
7952
  const signals = discovery.contentSignals;
7784
7953
  const robotsDeclared = robotsContentSignals ?? [];
@@ -7879,8 +8048,8 @@ function discoveryChecks(discovery, robotsContentSignals) {
7879
8048
  check$1("discovery", {
7880
8049
  id: "route-manifest",
7881
8050
  status: !discovery.routeManifest.valid ? "info" : discovery.routeManifest.missingHtmlRoutes.length === 0 && discovery.routeManifest.missingMarkdownRoutes.length === 0 && discovery.routeManifest.orphanMarkdownRoutes.length === 0 ? "pass" : "warning",
7882
- title: discovery.routeManifest.valid ? "The public route manifest agrees with the crawl" : "No public route manifest was available",
7883
- plainEnglish: discovery.routeManifest.valid ? `The manifest declared ${discovery.routeManifest.declaredHtmlRoutes.length} HTML routes and ${discovery.routeManifest.declaredMarkdownRoutes.length} Markdown routes. Missing or orphan routes are listed in the evidence.` : "A public route manifest is optional. Without one, this crawl cannot prove that no orphan Markdown files were deployed.",
8051
+ title: discovery.routeManifest.valid ? "The public route manifest is valid" : "No public route manifest was available",
8052
+ plainEnglish: discovery.routeManifest.valid ? `The manifest declared ${discovery.routeManifest.declaredHtmlRoutes.length} HTML routes and ${discovery.routeManifest.declaredMarkdownRoutes.length} Markdown routes. This bounded crawl did not treat unvisited routes as missing.` : "A public route manifest is optional. Without one, this crawl cannot prove that no orphan Markdown files were deployed.",
7884
8053
  action: "Generate one deterministic route inventory from the build when exact HTML and Markdown parity matters.",
7885
8054
  evidence: discovery.routeManifest
7886
8055
  })
@@ -8011,25 +8180,42 @@ function accessChecks(report, discovery) {
8011
8180
  const blocked = bots.filter((bot) => bot.allowed === false);
8012
8181
  const robotsKnown = report.ai?.robotsTxt?.availability === "available" || report.ai?.robotsTxt?.availability === "absent";
8013
8182
  const protocol = discovery.protocolVariants;
8014
- return [check$1("access", {
8015
- id: "crawler-access",
8016
- status: !robotsKnown ? "unknown" : blocked.length > 0 ? "warning" : "pass",
8017
- title: blocked.length ? "robots.txt blocks selected crawler tokens" : "No selected crawler token is blocked at the start URL",
8018
- plainEnglish: robotsKnown ? `${blocked.length} of ${bots.length} selected crawler tokens were blocked by the start URL robots.txt policy.` : "The robots.txt response was not stable enough to make a crawler-access claim.",
8019
- action: "Keep blocks intentional. A crawler token being allowed here does not prove that the service will crawl, index, or cite the site.",
8020
- evidence: {
8021
- availability: report.ai?.robotsTxt?.availability,
8022
- blocked,
8023
- botAccess: bots
8024
- }
8025
- }), check$1("access", {
8026
- id: "protocol-canonicalization",
8027
- status: protocol.http.permanentRedirectToHttps === true && protocol.hstsOnStartPage === true ? "pass" : protocol.http.permanentRedirectToHttps === null ? "unknown" : "warning",
8028
- title: protocol.http.permanentRedirectToHttps === true && protocol.hstsOnStartPage === true ? "HTTP redirects permanently to HTTPS and HTTPS sends HSTS" : "Protocol canonicalization needs attention or could not be proven",
8029
- plainEnglish: `The HTTP variant ${protocol.http.permanentRedirectToHttps === true ? "redirected permanently to HTTPS" : "did not produce a confirmed permanent HTTPS redirect"}. The HTTPS start page ${protocol.hstsOnStartPage ? "sent HSTS" : "did not provide confirmed HSTS evidence"}.`,
8030
- action: "Use one permanent HTTP-to-HTTPS redirect and send HSTS only after HTTPS is stable across the site.",
8031
- evidence: protocol
8032
- })];
8183
+ const authenticationGates = report.issues.filter((issue) => issue.ruleId === "soft_authentication_gate");
8184
+ const clientRenderedPages = report.issues.filter((issue) => issue.ruleId === "client_rendered_content");
8185
+ return [
8186
+ check$1("access", {
8187
+ id: "crawler-access",
8188
+ status: !robotsKnown ? "unknown" : blocked.length > 0 ? "warning" : "pass",
8189
+ title: blocked.length ? "robots.txt blocks selected crawler tokens" : "No selected crawler token is blocked at the start URL",
8190
+ plainEnglish: robotsKnown ? `${blocked.length} of ${bots.length} selected crawler tokens were blocked by the start URL robots.txt policy.` : "The robots.txt response was not stable enough to make a crawler-access claim.",
8191
+ action: "Keep blocks intentional. A crawler token being allowed here does not prove that the service will crawl, index, or cite the site.",
8192
+ evidence: {
8193
+ availability: report.ai?.robotsTxt?.availability,
8194
+ blocked,
8195
+ botAccess: bots
8196
+ }
8197
+ }),
8198
+ check$1("access", {
8199
+ id: "public-content-access",
8200
+ status: authenticationGates.length > 0 || clientRenderedPages.length > 0 ? "warning" : "pass",
8201
+ title: authenticationGates.length > 0 || clientRenderedPages.length > 0 ? "Some public content may be difficult for constrained clients to reach" : "No login gate or empty initial document was detected",
8202
+ plainEnglish: `${authenticationGates.length} crawled pages looked like login gates and ${clientRenderedPages.length} relied on JavaScript to replace an empty or near-empty initial document. These are general site-health observations reused here because they affect agents that do not maintain sessions or run JavaScript.`,
8203
+ action: authenticationGates.length > 0 || clientRenderedPages.length > 0 ? "Make intended public content available without a session and include its important document content in the initial HTML or a complete server-generated representation." : "Keep important public content available without a session or client-side rendering requirement.",
8204
+ evidence: {
8205
+ authenticationGates: authenticationGates.length,
8206
+ clientRenderedPages: clientRenderedPages.length
8207
+ },
8208
+ urls: [...authenticationGates, ...clientRenderedPages].map((issue) => issue.url).slice(0, 25)
8209
+ }),
8210
+ check$1("access", {
8211
+ id: "protocol-canonicalization",
8212
+ status: protocol.http.permanentRedirectToHttps === true && protocol.hstsOnStartPage === true ? "pass" : protocol.http.permanentRedirectToHttps === null ? "unknown" : "warning",
8213
+ title: protocol.http.permanentRedirectToHttps === true && protocol.hstsOnStartPage === true ? "HTTP redirects permanently to HTTPS and HTTPS sends HSTS" : "Protocol canonicalization needs attention or could not be proven",
8214
+ plainEnglish: `The HTTP variant ${protocol.http.permanentRedirectToHttps === true ? "redirected permanently to HTTPS" : "did not produce a confirmed permanent HTTPS redirect"}. The HTTPS start page ${protocol.hstsOnStartPage ? "sent HSTS" : "did not provide confirmed HSTS evidence"}.`,
8215
+ action: "Use one permanent HTTP-to-HTTPS redirect and send HSTS only after HTTPS is stable across the site.",
8216
+ evidence: protocol
8217
+ })
8218
+ ];
8033
8219
  }
8034
8220
  function identityChecks(report) {
8035
8221
  const types = new Set(report.pages.flatMap((page) => page.schemaTypes ?? []));
@@ -9558,6 +9744,48 @@ const RULE_DEFINITIONS = [
9558
9744
  suggestedCommands: ["seo crawl --sitemap-url <url> --health --json"]
9559
9745
  }
9560
9746
  },
9747
+ {
9748
+ id: "soft_authentication_gate",
9749
+ title: "Page appears to be an authentication gate",
9750
+ category: "response",
9751
+ defaultSeverity: "medium",
9752
+ whyItMatters: "The URL returned a successful HTML response, but the visible document looks like a login gate rather than the intended public page. Users and crawlers may receive a 200 response without the content they expected.",
9753
+ howToFix: "Confirm whether authentication is intentional. Public URLs should return their public content without a login form. Protected URLs should use a clear 401 or 403 response where appropriate and should not appear in public sitemaps or internal crawl paths.",
9754
+ impactIfIgnored: "A login page can be mistaken for the intended content, producing misleading crawl results and weak search or agent access.",
9755
+ howToVerify: "Fetch the affected URL without a session and confirm it returns the intended public page, or a deliberate authentication status for protected content.",
9756
+ agentHints: {
9757
+ evidenceFields: ["page.softAuthenticationGate", "page.finalUrl"],
9758
+ suggestedCommands: ["seo crawl <url> --max-pages 1 --json"]
9759
+ }
9760
+ },
9761
+ {
9762
+ id: "client_rendered_content",
9763
+ title: "Important content requires JavaScript rendering",
9764
+ category: "response",
9765
+ defaultSeverity: "medium",
9766
+ whyItMatters: "The raw HTML contained very little content and the useful document appeared only after JavaScript rendering. Some crawlers, agents, previews, and constrained clients do not run the page JavaScript.",
9767
+ howToFix: "Render the important page content into the initial HTML, or publish a complete server-generated representation such as negotiated Markdown. Keep navigation, headings, canonicals, and indexability signals available without client execution.",
9768
+ impactIfIgnored: "Clients that do not execute JavaScript may see an empty shell or miss important content and links.",
9769
+ howToVerify: "Re-run the crawl and confirm the raw and rendered content observations contain the same important document content.",
9770
+ agentHints: {
9771
+ evidenceFields: ["page.fetchDiagnostics.rendering.documentDifference", "page.usedJs"],
9772
+ suggestedCommands: ["seo crawl <url> --json"]
9773
+ }
9774
+ },
9775
+ {
9776
+ id: "soft_404",
9777
+ title: "Unknown URLs return a successful response",
9778
+ category: "response",
9779
+ defaultSeverity: "high",
9780
+ whyItMatters: "Known-nonexistent URLs returned a successful response or redirected to another successful page. Search engines and users cannot reliably distinguish missing content from a real page.",
9781
+ howToFix: "Return 404 or 410 for missing URLs. If a replacement exists, redirect only the specific retired URL to its closest relevant replacement rather than sending every unknown path to a generic page.",
9782
+ impactIfIgnored: "Missing URLs can remain in crawl queues, waste crawl attention, and create confusing search or monitoring evidence.",
9783
+ howToVerify: "Request several known-nonexistent paths and confirm each returns 404 or 410 without redirecting to a generic successful page.",
9784
+ agentHints: {
9785
+ evidenceFields: ["report.siteChecks.soft404"],
9786
+ suggestedCommands: ["seo crawl <url> --json"]
9787
+ }
9788
+ },
9561
9789
  {
9562
9790
  id: "client_error",
9563
9791
  title: "Client error",
@@ -10047,6 +10275,8 @@ const RULE_RECOMMENDATIONS = {
10047
10275
  x_robots_noindex: "review",
10048
10276
  robots_blocked: "review",
10049
10277
  crawler_access_blocked: "review",
10278
+ soft_authentication_gate: "review",
10279
+ client_rendered_content: "review",
10050
10280
  orphan_page: "review",
10051
10281
  redirected_url: "review",
10052
10282
  slow_response: "review",
@@ -10137,6 +10367,28 @@ const WEAK_VALUABLE_INLINKS = 1;
10137
10367
  const QUERY_COVERAGE_MIN_IMPRESSIONS = 50;
10138
10368
  const QUERY_COVERAGE_MIN = .6;
10139
10369
  const OVERSIZED_IMAGE_CANDIDATE_LIMIT = 2e3;
10370
+ const EMPTY_INITIAL_HTML_CHARACTERS = 200;
10371
+ const SUBSTANTIVE_RENDERED_CHARACTERS = 500;
10372
+ function clientRenderedContentEvidence(page) {
10373
+ const rendering = page.fetchDiagnostics?.rendering;
10374
+ const difference = rendering?.documentDifference;
10375
+ if (rendering?.status !== "rendered" || !difference?.changed.includes("content")) return;
10376
+ const rawCharacters = difference.raw.content.characters;
10377
+ const renderedCharacters = difference.rendered.content.characters;
10378
+ if (rawCharacters >= EMPTY_INITIAL_HTML_CHARACTERS || renderedCharacters < SUBSTANTIVE_RENDERED_CHARACTERS || renderedCharacters < rawCharacters * 3) return;
10379
+ return {
10380
+ rawCharacters,
10381
+ rawWords: difference.raw.content.wordCount,
10382
+ renderedCharacters,
10383
+ renderedWords: difference.rendered.content.wordCount,
10384
+ changed: difference.changed,
10385
+ thresholds: {
10386
+ emptyInitialHtmlCharacters: EMPTY_INITIAL_HTML_CHARACTERS,
10387
+ substantiveRenderedCharacters: SUBSTANTIVE_RENDERED_CHARACTERS,
10388
+ minimumGrowthRatio: 3
10389
+ }
10390
+ };
10391
+ }
10140
10392
  function issue(ruleId, page, detail, evidence) {
10141
10393
  const rule = explainRule(ruleId);
10142
10394
  if (!rule) throw new Error(`Missing rule guidance for ${ruleId}`);
@@ -10351,6 +10603,9 @@ function auditCrawlPages(pages, opts = {}) {
10351
10603
  }));
10352
10604
  if (page.auditScope === "status") continue;
10353
10605
  if (redirected) continue;
10606
+ if (page.softAuthenticationGate) issues.push(issue("soft_authentication_gate", page, "A password form and login-specific page signals were observed.", page.softAuthenticationGate));
10607
+ const renderedContent = clientRenderedContentEvidence(page);
10608
+ if (renderedContent) issues.push(issue("client_rendered_content", page, "The raw response was an empty or near-empty shell.", renderedContent));
10354
10609
  if (isHtmlPage(page) && (page.sizeBytes ?? 0) > LARGE_HTML_BYTES) issues.push(issue("large_html", page, `${Math.round((page.sizeBytes ?? 0) / 1024)} KB`, {
10355
10610
  sizeBytes: page.sizeBytes,
10356
10611
  thresholdBytes: LARGE_HTML_BYTES
@@ -10955,7 +11210,7 @@ function createCrawlReport(input) {
10955
11210
  const requests = sortRequests(sanitizeTenantValue(input.requests ?? []));
10956
11211
  const requestEvidenceStatus = input.requestEvidenceStatus ?? (input.requests ? "available" : "unavailable");
10957
11212
  if (requestEvidenceStatus === "unavailable" && requests.length > 0) throw new Error("Unavailable request evidence cannot include request observations.");
10958
- const safeIssues = sanitizeIssues(input.issues ?? [...auditCrawlRequests(requests), ...auditCrawlPages(safePagesWithLinks, { startUrl: config.url })]);
11213
+ const safeIssues = sanitizeIssues([...input.issues ?? [...auditCrawlRequests(requests), ...auditCrawlPages(safePagesWithLinks, { startUrl: config.url })], ...input.additionalIssues ?? []]);
10959
11214
  const pages = safePagesWithLinks;
10960
11215
  return {
10961
11216
  id: input.id ?? crawlRunId(),
@@ -10984,6 +11239,7 @@ function createCrawlReport(input) {
10984
11239
  ...input.ai ? { ai: sanitizeTenantValue(input.ai) } : {},
10985
11240
  ...input.sitemapDiscovery ? { sitemapDiscovery: sanitizeTenantValue(input.sitemapDiscovery) } : {},
10986
11241
  ...input.externalLinkVerification ? { externalLinkVerification: sanitizeTenantValue(input.externalLinkVerification) } : {},
11242
+ ...input.siteChecks ? { siteChecks: sanitizeTenantValue(input.siteChecks) } : {},
10987
11243
  ...input.agentDiscovery ? { agentDiscovery: sanitizeTenantValue(input.agentDiscovery) } : {},
10988
11244
  warnings: sanitizeMessages(input.warnings).sort(compareText$13),
10989
11245
  caveats: sanitizeMessages(input.caveats).sort(compareText$13)
@@ -11544,6 +11800,75 @@ function observationFromPage(requestedUrl, page) {
11544
11800
  };
11545
11801
  }
11546
11802
  //#endregion
11803
+ //#region packages/core/dist/analyze/crawler/site-checks.js
11804
+ const PROBE_COUNT = 2;
11805
+ function probeUrls(startUrl) {
11806
+ const origin = new URL(startUrl).origin;
11807
+ const seed = createHash("sha256").update(origin).digest("hex").slice(0, 12);
11808
+ return Array.from({ length: PROBE_COUNT }, (_, index) => new URL(`/.well-known/seo-audit/not-found-${seed}-${index + 1}`, origin).toString());
11809
+ }
11810
+ function probeObservation(url, result) {
11811
+ const request = result.request;
11812
+ if (!request) return {
11813
+ url,
11814
+ error: result.warning ?? "No response evidence."
11815
+ };
11816
+ if (request.outcome === "response") return {
11817
+ url,
11818
+ status: request.status,
11819
+ finalUrl: request.finalUrl,
11820
+ redirected: request.finalUrl !== url,
11821
+ accessBlocked: Boolean(request.accessBlock)
11822
+ };
11823
+ return {
11824
+ url,
11825
+ error: request.outcome === "failure" ? request.error : `Probe skipped: ${request.reason}`
11826
+ };
11827
+ }
11828
+ async function collectCrawlSiteChecks(input) {
11829
+ const probes = [];
11830
+ for (const url of probeUrls(input.startUrl)) {
11831
+ if (input.signal?.aborted) break;
11832
+ const result = await input.fetchStatusPage(url, {
11833
+ js: "off",
11834
+ refresh: true,
11835
+ timeoutMs: input.timeoutMs,
11836
+ signal: input.signal,
11837
+ writeCache: false,
11838
+ respectRobots: input.respectRobots
11839
+ });
11840
+ probes.push(probeObservation(url, result));
11841
+ }
11842
+ const conclusive = probes.filter((probe) => probe.status !== void 0 && !probe.accessBlocked);
11843
+ const successful = conclusive.filter((probe) => (probe.status ?? 0) >= 200 && (probe.status ?? 0) < 400);
11844
+ const correctMissing = conclusive.filter((probe) => probe.status === 404 || probe.status === 410);
11845
+ const status = conclusive.length !== PROBE_COUNT ? "unknown" : successful.length === PROBE_COUNT ? "warning" : correctMissing.length === PROBE_COUNT ? "pass" : "unknown";
11846
+ const checks = { soft404: {
11847
+ status,
11848
+ probes,
11849
+ probeLimit: PROBE_COUNT,
11850
+ complete: probes.length === PROBE_COUNT
11851
+ } };
11852
+ if (status !== "warning") return {
11853
+ checks,
11854
+ issues: []
11855
+ };
11856
+ const rule = explainRule("soft_404");
11857
+ if (!rule) throw new Error("Missing rule guidance for soft_404");
11858
+ return {
11859
+ checks,
11860
+ issues: [{
11861
+ ruleId: "soft_404",
11862
+ title: rule.title,
11863
+ category: rule.category,
11864
+ severity: rule.defaultSeverity,
11865
+ url: new URL("/", input.startUrl).toString(),
11866
+ detail: `${successful.length} known-nonexistent paths returned a successful response or redirect.`,
11867
+ evidence: checks.soft404
11868
+ }]
11869
+ };
11870
+ }
11871
+ //#endregion
11547
11872
  //#region packages/core/dist/analyze/crawler/site-crawl-ai-signals.js
11548
11873
  const MAX_AUXILIARY_RESPONSE_BYTES = 2 * 1024 * 1024;
11549
11874
  const AI_BOT_USER_AGENTS = [
@@ -12263,6 +12588,9 @@ async function crawlOne(url, opts = {}) {
12263
12588
  mainContentHash: hashText(extracted.contentText),
12264
12589
  textRatio: fetched.html.length ? Math.min(1, extracted.contentText.length / fetched.html.length) : void 0,
12265
12590
  contentSample: truncate$1(extracted.contentText, 300),
12591
+ contentSketch: crawlerEvidence.contentSketch,
12592
+ softAuthenticationGate: crawlerEvidence.softAuthenticationGate,
12593
+ tabbedContent: crawlerEvidence.tabbedContent,
12266
12594
  lang: extracted.lang,
12267
12595
  hasViewport: extracted.hasViewport,
12268
12596
  isHttps: new URL(extracted.finalUrl).protocol === "https:",
@@ -13547,6 +13875,13 @@ async function crawlSite(input, dependencies) {
13547
13875
  signal,
13548
13876
  concurrency: Math.min(config.concurrency, 4)
13549
13877
  }) : void 0;
13878
+ const siteCheckResult = !healthStrategy && !cancelled && !stoppedForMemory ? await collectCrawlSiteChecks({
13879
+ startUrl: config.url,
13880
+ timeoutMs: config.timeoutMs,
13881
+ fetchStatusPage: deps.fetchStatusPage,
13882
+ respectRobots: config.respectRobots,
13883
+ signal
13884
+ }) : void 0;
13550
13885
  for (const page of pages) {
13551
13886
  if (!page.geo || !llmsTxt) continue;
13552
13887
  page.geo = {
@@ -13572,6 +13907,10 @@ async function crawlSite(input, dependencies) {
13572
13907
  } } : {},
13573
13908
  ...sitemapDiscovery ? { sitemapDiscovery } : {},
13574
13909
  ...externalLinkVerification ? { externalLinkVerification } : {},
13910
+ ...siteCheckResult ? {
13911
+ siteChecks: siteCheckResult.checks,
13912
+ additionalIssues: siteCheckResult.issues
13913
+ } : {},
13575
13914
  ...agentDiscovery ? { agentDiscovery } : {},
13576
13915
  status: reportStatus,
13577
13916
  warnings,
@@ -14452,6 +14791,8 @@ const agentRepresentationResponseSchema = z.object({
14452
14791
  status: z.number().int().optional(),
14453
14792
  contentType: z.string().optional(),
14454
14793
  bytes: z.number().int().nonnegative().optional(),
14794
+ characters: z.number().int().nonnegative().optional(),
14795
+ estimatedTokens: z.number().int().nonnegative().optional(),
14455
14796
  sha256: z.string().optional(),
14456
14797
  canonicalUrl: z.string().url().optional(),
14457
14798
  alternateUrl: z.string().url().optional(),
@@ -14509,7 +14850,15 @@ const agentDiscoverySchema = z.object({
14509
14850
  sourceWordCount: z.number().int().nonnegative(),
14510
14851
  wordRetentionRatio: z.number().nonnegative().nullable(),
14511
14852
  introductoryCopyRetained: z.boolean().nullable(),
14512
- navigationOnly: z.boolean()
14853
+ navigationOnly: z.boolean(),
14854
+ contentSketchCoverage: z.number().min(0).max(1).nullable().optional(),
14855
+ tabbedContent: z.object({
14856
+ detectedPanels: z.number().int().nonnegative(),
14857
+ evaluatedPanels: z.number().int().nonnegative(),
14858
+ retainedPanels: z.number().int().nonnegative(),
14859
+ missingPanels: z.number().int().nonnegative(),
14860
+ complete: z.boolean().nullable()
14861
+ }).optional()
14513
14862
  }).optional()
14514
14863
  }))
14515
14864
  }),
@@ -14528,6 +14877,11 @@ const agentDiscoverySchema = z.object({
14528
14877
  missingHtmlRoutes: z.array(z.string()),
14529
14878
  missingMarkdownRoutes: z.array(z.string()),
14530
14879
  orphanMarkdownRoutes: z.array(z.string()),
14880
+ comparisonStatus: z.enum([
14881
+ "complete",
14882
+ "partial",
14883
+ "not-applicable"
14884
+ ]).optional(),
14531
14885
  error: z.string().optional()
14532
14886
  }),
14533
14887
  agentSkills: z.object({
@@ -14633,6 +14987,25 @@ const agentDiscoverySchema = z.object({
14633
14987
  warnings: z.array(z.string())
14634
14988
  });
14635
14989
  //#endregion
14990
+ //#region packages/core/dist/analyze/crawler/site-checks-schema.js
14991
+ const crawlSiteChecksSchema = z.object({ soft404: z.object({
14992
+ status: z.enum([
14993
+ "pass",
14994
+ "warning",
14995
+ "unknown"
14996
+ ]),
14997
+ probes: z.array(z.object({
14998
+ url: z.string().url(),
14999
+ status: z.number().int().optional(),
15000
+ finalUrl: z.string().url().optional(),
15001
+ redirected: z.boolean().optional(),
15002
+ accessBlocked: z.boolean().optional(),
15003
+ error: z.string().optional()
15004
+ })),
15005
+ probeLimit: z.number().int().positive(),
15006
+ complete: z.boolean()
15007
+ }) });
15008
+ //#endregion
14636
15009
  //#region packages/core/dist/analyze/crawler/schemas.js
14637
15010
  const crawlerRuleSeveritySchema = z.enum([
14638
15011
  "low",
@@ -15062,6 +15435,34 @@ const crawlPageSnapshotSchema = z.object({
15062
15435
  mainContentHash: z.string().optional(),
15063
15436
  textRatio: z.number().min(0).max(1).optional(),
15064
15437
  contentSample: z.string().optional(),
15438
+ contentSketch: z.object({
15439
+ version: z.literal(1),
15440
+ wordCount: z.number().int().nonnegative(),
15441
+ shingleSize: z.number().int().positive(),
15442
+ sampledShingles: z.number().int().nonnegative(),
15443
+ hashes: z.array(z.string())
15444
+ }).optional(),
15445
+ softAuthenticationGate: z.object({
15446
+ kind: z.literal("login-form"),
15447
+ indicators: z.array(z.string()),
15448
+ formActionPath: z.string().optional()
15449
+ }).optional(),
15450
+ tabbedContent: z.object({
15451
+ groups: z.number().int().positive(),
15452
+ panels: z.number().int().positive(),
15453
+ retainedPanels: z.number().int().nonnegative(),
15454
+ truncated: z.boolean(),
15455
+ panelSketches: z.array(z.object({
15456
+ label: z.string().optional(),
15457
+ sketch: z.object({
15458
+ version: z.literal(1),
15459
+ wordCount: z.number().int().nonnegative(),
15460
+ shingleSize: z.number().int().positive(),
15461
+ sampledShingles: z.number().int().nonnegative(),
15462
+ hashes: z.array(z.string())
15463
+ })
15464
+ }))
15465
+ }).optional(),
15065
15466
  lang: z.string().optional(),
15066
15467
  hasViewport: z.boolean().optional(),
15067
15468
  isHttps: z.boolean().optional(),
@@ -15553,6 +15954,7 @@ const crawlReportBaseSchema = z.object({
15553
15954
  ai: crawlAiSignalsSchema.optional(),
15554
15955
  sitemapDiscovery: crawlSitemapDiscoverySchema.optional(),
15555
15956
  externalLinkVerification: crawlExternalLinkVerificationSchema.optional(),
15957
+ siteChecks: crawlSiteChecksSchema.optional(),
15556
15958
  agentDiscovery: agentDiscoverySchema.optional(),
15557
15959
  warnings: z.array(z.string()),
15558
15960
  caveats: z.array(z.string())