@reddoorla/maintenance 0.94.0 → 0.94.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/bump-deps-6OR7LL4E.js +1 -1
  2. package/dist/{chunk-DHVJ7H2F.js → chunk-REJCSA4H.js} +2 -2
  3. package/dist/{chunk-VELWHWOW.js → chunk-WJMPCVOR.js} +34 -6
  4. package/dist/chunk-WJMPCVOR.js.map +1 -0
  5. package/dist/cli/bin.js +5 -5
  6. package/dist/cli/commands/audit.js +1 -1
  7. package/dist/convert-to-pnpm-3RVGUBBR.js +1 -1
  8. package/dist/health-endpoint-XLNBWVHK.js +1 -1
  9. package/dist/index.js +2 -2
  10. package/dist/{init-F5OBBWA5.js → init-YCLSGIEP.js} +2 -2
  11. package/dist/{launch-SUTMI4NG.js → launch-FSHDTXSM.js} +2 -2
  12. package/dist/onboard-NKE6GZKL.js +1 -1
  13. package/dist/{pipeline-PRMD4EJT.js → pipeline-MATFYQRC.js} +3 -3
  14. package/dist/{prismic-ci-QIO3I4KB.js → prismic-ci-DM352L2Q.js} +2 -2
  15. package/dist/{prismic-models-RWDVKZ2W.js → prismic-models-Q32ARSYS.js} +2 -2
  16. package/dist/prospect/types.js +1 -1
  17. package/dist/{prospect-audit-CIL3NCPM.js → prospect-audit-MT6RGMOY.js} +3 -3
  18. package/dist/{render-ZUQBJYJR.js → render-JE5P26H5.js} +3 -3
  19. package/dist/self-updating-5DVDIEHB.js +1 -1
  20. package/dist/smoke-suite-NUTC7IUK.js +1 -1
  21. package/dist/svelte-codemods-U4NIT6CV.js +1 -1
  22. package/dist/sync-configs-S2JYY6QU.js +1 -1
  23. package/dist/upgrade-YFZCMUPE.js +1 -1
  24. package/package.json +1 -1
  25. package/dist/chunk-VELWHWOW.js.map +0 -1
  26. /package/dist/{chunk-DHVJ7H2F.js.map → chunk-REJCSA4H.js.map} +0 -0
  27. /package/dist/{init-F5OBBWA5.js.map → init-YCLSGIEP.js.map} +0 -0
  28. /package/dist/{launch-SUTMI4NG.js.map → launch-FSHDTXSM.js.map} +0 -0
  29. /package/dist/{pipeline-PRMD4EJT.js.map → pipeline-MATFYQRC.js.map} +0 -0
  30. /package/dist/{prismic-ci-QIO3I4KB.js.map → prismic-ci-DM352L2Q.js.map} +0 -0
  31. /package/dist/{prismic-models-RWDVKZ2W.js.map → prismic-models-Q32ARSYS.js.map} +0 -0
  32. /package/dist/{prospect-audit-CIL3NCPM.js.map → prospect-audit-MT6RGMOY.js.map} +0 -0
  33. /package/dist/{render-ZUQBJYJR.js.map → render-JE5P26H5.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/prospect/accessibility.ts","../src/prospect/crawl.ts","../src/prospect/pages.ts","../src/prospect/interaction.ts","../src/prospect/extract.ts","../src/prospect/stack.ts","../src/prospect/journey.ts","../src/prospect/http-probes.ts","../src/prospect/site-checks.ts"],"sourcesContent":["import type { PageCapture } from \"./types.js\";\n\n/**\n * What axe-core found, in the browser the crawl already opened.\n *\n * WHAT THIS IS AND IS NOT NEW. The Lighthouse stage we already run IS axe — a\n * subset of roughly forty-five rules, reported as one number between 0 and 100.\n * Running it ourselves does not discover a different site; it does three things\n * that number cannot:\n *\n * - turns \"accessibility: 82\" into named rules with named fixes, each with an\n * element to go and look at;\n * - adds the rules Lighthouse omits — every `best-practice` landmark and\n * heading rule, which is most of what is structurally wrong with most\n * sites, and which a wcag-tags-only scan cannot see at all;\n * - absorbs several checks we would otherwise have written worse by hand\n * (link names, tap-target size, viewport zoom, `lang`, form labels).\n *\n * THE TAG SET IS THE WHOLE POINT. Our own fleet audit scans `wcag2a`,\n * `wcag2aa`, `wcag21a`, `wcag21aa`, `wcag22aa` and nothing else, and every\n * landmark and heading-order rule in axe is tagged `best-practice` — so a green\n * run there proves nothing about structure. This runs the DEFAULT rule set,\n * which is all of them.\n *\n * WHAT IT MAY NOT SAY. Never that a site \"is inaccessible\", and never a score.\n * axe finds what a machine can find, which is a minority of what a disabled\n * person actually encounters; a page can pass every rule here and be unusable.\n * The report says: we ran N rules, these M produced findings, here is the\n * element and the rule's own explanation. That is a fact with a receipt, and it\n * is all this instrument earns.\n */\n\nexport type AxeImpact = \"minor\" | \"moderate\" | \"serious\" | \"critical\";\n\n/** One rule that produced findings on one page. Sized for persistence: the\n * node COUNT plus a single example, never every matching element. */\nexport type AxeViolation = {\n /** axe's rule id — `color-contrast`, `landmark-one-main`. Stable, and the\n * thing to search for. */\n id: string;\n impact: AxeImpact | null;\n /** axe's own one-line explanation. Quoted rather than paraphrased: it is\n * written by people who know the rule better than we do. */\n help: string;\n helpUrl: string;\n /** Elements on this page the rule matched. */\n nodes: number;\n /** One of them, truncated — so a reader can go and find it. */\n sample: string | null;\n};\n\nexport type AxePageResult = {\n violations: AxeViolation[];\n /**\n * Rules that found something to judge on this page and judged it fine.\n *\n * NOT \"the number of rules axe has\". Measured on our own report page, the\n * default set is 90 rules, of which 42 passed, 47 were INAPPLICABLE — the\n * page contains no table, no video, no iframe, so those rules had nothing to\n * look at — and one was incomplete. Printing 90 as though we had checked 90\n * things would be a number inflated on our own behalf, which is why\n * `inapplicable` is carried rather than quietly folded into this one.\n */\n passes: number;\n /** Rules axe could not decide — genuinely \"we do not know\", reported as such\n * rather than folded into either column. */\n incomplete: number;\n /** WHICH rules those were. A count alone tells a reader three things need a\n * human without saying what to look at, which is a worry rather than a job.\n * Optional: absent on any result stored before it was captured. */\n incompleteIds?: string[];\n /** Rules with nothing on the page to check. Carried so the report can say\n * \"43 of axe's 90 rules had something to check here\" and be exactly right. */\n inapplicable: number;\n};\n\nexport type AccessibilityResult = {\n /**\n * Did the rules run anywhere?\n *\n * False when no page carried a result — every report stored before this\n * existed, every run whose browser never started, and every test that stubs\n * the renderer. An empty `violations` with `measured: false` means \"we did\n * not look\"; with `measured: true` it means the rules ran and found nothing,\n * which is a real and excellent answer.\n */\n measured: boolean;\n pagesExamined: number;\n /**\n * Rules that had something to check and passed — the number the report may\n * print as \"we checked N things\", and no larger one.\n *\n * The MAXIMUM across pages, never the sum. The same rule set runs on every\n * page, so summing reports \"210 checks\" for 42 rules over five pages. Max is\n * a deliberate under-count where pages differ: it can only understate what we\n * looked at, and understating our own work is the safe direction.\n */\n rulesPassed: number;\n rulesIncomplete: number;\n /** The undecided rules by name, so the disclosure can say what to look at\n * instead of only how many. Empty when none, absent-safe for old reports. */\n incompleteIds: string[];\n /** Rules that found nothing on these pages to apply to. Not a pass and not a\n * failure — the page simply has no table, video or iframe for them. */\n rulesInapplicable: number;\n /** Rules that produced findings, worst impact first, each naming the pages it\n * fired on. Capped — see `violationsTotal`. */\n violations: (AxeViolation & { pages: string[] })[];\n /** True number of distinct rules with findings, before the cap. A truncated\n * list that looks complete is the quiet lie this codebase is built to avoid. */\n violationsTotal: number;\n};\n\n/** Enough distinct rules to be a fix list, not so many it is a wall. */\nexport const MAX_REPORTED_RULES = 12;\n\nconst IMPACT_ORDER: Record<string, number> = { critical: 0, serious: 1, moderate: 2, minor: 3 };\n\n/**\n * Roll per-page results up to the site.\n *\n * Aggregated by RULE rather than by page, because \"your headings skip a level\n * on six pages\" is one job and six rows of the same sentence is not.\n */\nexport function summarizeAccessibility(pages: PageCapture[]): AccessibilityResult {\n const withResults = pages.filter((p) => p.axe != null);\n if (withResults.length === 0) {\n return {\n measured: false,\n pagesExamined: 0,\n rulesPassed: 0,\n rulesIncomplete: 0,\n incompleteIds: [],\n rulesInapplicable: 0,\n violations: [],\n violationsTotal: 0,\n };\n }\n\n const byRule = new Map<string, AxeViolation & { pages: string[] }>();\n let passes = 0;\n let incomplete = 0;\n const undecided = new Set<string>();\n let inapplicable = 0;\n for (const page of withResults) {\n const axe = page.axe!;\n // The per-page pass counts are the same rule set every time, so the site\n // figure is the per-page one, not their sum — adding them would report\n // \"we ran 470 checks\" for 94 rules across five pages.\n passes = Math.max(passes, axe.passes);\n incomplete = Math.max(incomplete, axe.incomplete);\n for (const id of axe.incompleteIds ?? []) undecided.add(id);\n inapplicable = Math.max(inapplicable, axe.inapplicable ?? 0);\n for (const v of axe.violations) {\n const existing = byRule.get(v.id);\n if (existing) {\n existing.nodes += v.nodes;\n if (!existing.pages.includes(page.url)) existing.pages.push(page.url);\n existing.sample ??= v.sample;\n } else {\n byRule.set(v.id, { ...v, pages: [page.url] });\n }\n }\n }\n\n const all = [...byRule.values()].sort(\n (a, b) =>\n (IMPACT_ORDER[a.impact ?? \"minor\"] ?? 4) - (IMPACT_ORDER[b.impact ?? \"minor\"] ?? 4) ||\n b.nodes - a.nodes ||\n a.id.localeCompare(b.id),\n );\n\n return {\n measured: true,\n pagesExamined: withResults.length,\n rulesPassed: passes,\n // The UNION when we have the names, the per-page max otherwise.\n //\n // `Math.max` was only ever a proxy for \"how many distinct rules\", chosen\n // because summing would have counted the same rule once per page. Now that\n // the ids are collected, the distinct count is known exactly — and the two\n // disagreed on our own site, where the report said \"3 rules need a human\"\n // and then listed four. A reader who can count is a reader who stops\n // trusting the numbers.\n rulesIncomplete: undecided.size > 0 ? undecided.size : incomplete,\n incompleteIds: [...undecided].sort(),\n rulesInapplicable: inapplicable,\n violations: all.slice(0, MAX_REPORTED_RULES),\n violationsTotal: all.length,\n };\n}\n\n/**\n * What the browser itself reported while the page was open.\n *\n * Collected in the SAME pass as the rendered DOM and the axe run — listeners\n * attached before navigation, one viewport resize after it. A resize reflows\n * the page the browser has already loaded; it costs no request and puts nothing\n * extra on the prospect's server, which is why the mobile-overflow measurement\n * is affordable at all.\n */\nexport type PageVitals = {\n /** Uncaught exceptions and `console.error` calls, capped and deduped. The\n * most demonstrable \"your site is broken\" evidence there is. */\n consoleErrors: string[];\n /**\n * Requests the page made that failed or came back 4xx/5xx.\n *\n * `firstParty` decides whether it is a finding at all. A blocked third-party\n * analytics beacon is our network or an ad blocker; a 404 on the site's own\n * stylesheet is theirs, and only the second belongs in a report.\n */\n failedRequests: { url: string; status: number | null; firstParty: boolean }[];\n /**\n * Pixels the document overflows a 375px viewport, or null when we could not\n * measure. Zero is the answer a correct site gives.\n */\n overflowAt375: number | null;\n /** Text nodes rendering below 12px, with one example. Null = not measured. */\n tinyText: { count: number; sample: string | null } | null;\n /** Images downloaded far larger than they are drawn — the bytes a visitor\n * pays for and never sees. */\n oversizedImages: { src: string; naturalWidth: number; renderedWidth: number }[];\n};\n\n/** Enough to name the problem, not so many the report becomes a log file. */\nexport const MAX_CONSOLE_ERRORS = 8;\nexport const MAX_FAILED_REQUESTS = 12;\nexport const MAX_OVERSIZED_IMAGES = 6;\n","import { parse, HTMLElement, NodeType } from \"node-html-parser\";\nimport { isInfraPath } from \"./pages.js\";\nimport {\n MAX_CONSOLE_ERRORS,\n MAX_FAILED_REQUESTS,\n MAX_OVERSIZED_IMAGES,\n type AxePageResult,\n type PageVitals,\n} from \"./accessibility.js\";\nimport {\n pageInteractionDeps,\n probeForms,\n withTimeout,\n PROBE_BUDGET_MS,\n type FormProbe,\n} from \"./interaction.js\";\nimport type { CrawlResult, PageCapture, RobotsAgentAccess } from \"./types.js\";\nimport { extractPage, UNRENDERED_TAGS } from \"./extract.js\";\nimport { isPrivateOrLoopbackHost } from \"../util/url.js\";\n\n/** The answer-engine crawlers the report scores. */\nexport const AI_AGENTS = [\n \"GPTBot\",\n \"OAI-SearchBot\",\n \"ClaudeBot\",\n \"PerplexityBot\",\n \"Google-Extended\",\n \"CCBot\",\n] as const;\n\n/** The classical baseline — a prospect blocking these has a bigger problem than AEO. */\nexport const CLASSICAL_AGENTS = [\"Googlebot\", \"Bingbot\"] as const;\n\nexport const ALL_AGENTS: string[] = [...AI_AGENTS, ...CLASSICAL_AGENTS];\n\ntype RobotsRule = { type: \"allow\" | \"disallow\"; path: string; line: string };\ntype RobotsGroup = { agents: string[]; rules: RobotsRule[] };\n\n/** Parse robots.txt into agent groups. Consecutive `User-agent:` lines share one\n * rule set, per the robots.txt convention. */\nexport function parseRobots(txt: string): RobotsGroup[] {\n const groups: RobotsGroup[] = [];\n let current: RobotsGroup | null = null;\n let lastWasAgent = false;\n for (const rawLine of txt.split(/\\r?\\n/)) {\n const line = (rawLine.split(\"#\")[0] ?? \"\").trim();\n if (!line) continue;\n const idx = line.indexOf(\":\");\n if (idx < 0) continue;\n const field = line.slice(0, idx).trim().toLowerCase();\n const value = line.slice(idx + 1).trim();\n if (field === \"user-agent\") {\n if (!current || !lastWasAgent) {\n current = { agents: [], rules: [] };\n groups.push(current);\n }\n current.agents.push(value.toLowerCase());\n lastWasAgent = true;\n } else if (field === \"allow\" || field === \"disallow\") {\n if (!current) continue;\n current.rules.push({ type: field === \"allow\" ? \"allow\" : \"disallow\", path: value, line });\n lastWasAgent = false;\n }\n }\n return groups;\n}\n\n/** Does a robots.txt path pattern cover the site root? RFC 9309 gives `*` the\n * meaning \"any run of characters\" and `$` the meaning \"end of path\", so\n * `Disallow: /`, `Disallow: /*` and `Disallow: /$` are three spellings of one\n * site-wide block. Matching only the literal \"/\" reports a fully blocked site\n * as open — the one error this report must never make. */\nexport function pathCoversRoot(pattern: string): boolean {\n if (!pattern) return false;\n const anchored = pattern.endsWith(\"$\");\n const body = anchored ? pattern.slice(0, -1) : pattern;\n const source = body\n .split(\"*\")\n .map((part) => part.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n .join(\".*\");\n return new RegExp(`^${source}${anchored ? \"$\" : \"\"}`).test(\"/\");\n}\n\n/**\n * Does a robots.txt path pattern match this path? RFC 9309 prefix semantics,\n * with `*` for any run of characters and `$` anchoring the end.\n *\n * `pathCoversRoot` is this function asked about \"/\", and stays separate because\n * it answers a different question — \"is the whole site blocked?\" — which the\n * report states as a headline.\n */\nexport function robotsPathMatches(pattern: string, path: string): boolean {\n if (!pattern) return false;\n const anchored = pattern.endsWith(\"$\");\n const body = anchored ? pattern.slice(0, -1) : pattern;\n const source = body\n .split(\"*\")\n .map((part) => part.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n .join(\".*\");\n return new RegExp(`^${source}${anchored ? \"$\" : \"\"}`).test(path);\n}\n\n/**\n * WHAT WE MAY FETCH — our own crawler obeying the file, not the matrix of what\n * other people's crawlers may do.\n *\n * These are different questions and only the second one was being asked. We\n * read robots.txt to report whether an owner turns away GPTBot, and then\n * fetched whatever we liked. A tool that grades a site on crawler access and\n * ignores that site's own Disallow has no standing to publish the grade — and\n * viget.com, the first third party we pointed this at, asks crawlers to leave\n * `/admin`, `/exhibit`, `/login` and `/search?q=` alone.\n *\n * Longest-match-wins between Allow and Disallow, per RFC 9309, so an\n * `Allow: /blog/public` under a `Disallow: /blog` is honoured. A group naming\n * us specifically wins over the wildcard group; with no robots.txt at all,\n * everything is permitted, because absence is not refusal.\n */\nexport function robotsAllowsUs(robotsTxt: string | null, url: string, agent: string): boolean {\n if (robotsTxt === null) return true;\n let path: string;\n try {\n const u = new URL(url);\n path = `${u.pathname}${u.search}`;\n } catch {\n return true;\n }\n const groups = parseRobots(robotsTxt);\n const lower = agent.toLowerCase();\n // Our UA string is \"ReddoorAudit/1.0 (+https://...)\"; a robots group naming\n // us would write \"reddooraudit\", so match on containment either way round.\n const named = groups.filter((g) =>\n g.agents.some((a) => a !== \"*\" && (lower.includes(a) || a.includes(lower.split(\"/\")[0] ?? \"\"))),\n );\n const matched = named.length > 0 ? named : groups.filter((g) => g.agents.includes(\"*\"));\n let best: { type: \"allow\" | \"disallow\"; length: number } | null = null;\n for (const rule of matched.flatMap((g) => g.rules)) {\n if (!robotsPathMatches(rule.path, path)) continue;\n // An empty `Disallow:` is the conventional \"nothing is disallowed\" and\n // matches nothing, which `robotsPathMatches` already returns false for.\n if (best === null || rule.path.length > best.length) {\n best = { type: rule.type, length: rule.path.length };\n }\n }\n return best === null || best.type === \"allow\";\n}\n\n/** Can each agent fetch the site root? Only rules that cover \"/\" decide: a\n * `Disallow: /admin` scopes a section, not the site, and must not read as a\n * block in the report. An agent-specific group wins over the wildcard group. */\nexport function evaluateAgentAccess(robotsTxt: string | null): RobotsAgentAccess[] {\n if (robotsTxt === null) {\n return ALL_AGENTS.map((agent) => ({ agent, allowed: true, matchedRule: null }));\n }\n const groups = parseRobots(robotsTxt);\n return ALL_AGENTS.map((agent) => {\n const lower = agent.toLowerCase();\n // RFC 9309: every group naming this agent is combined into one; the wildcard\n // group is consulted ONLY when no group names it. Appending a second\n // `User-agent: GPTBot` block is exactly what the block-the-AI-bots guides\n // tell owners to do, so first-match-wins reads such a file in the wrong order.\n const named = groups.filter((g) => g.agents.includes(lower));\n const matched = named.length > 0 ? named : groups.filter((g) => g.agents.includes(\"*\"));\n if (matched.length === 0) return { agent, allowed: true, matchedRule: null };\n const header = `User-agent: ${named.length > 0 ? agent : \"*\"}`;\n const rootRules = matched.flatMap((g) => g.rules).filter((r) => pathCoversRoot(r.path));\n const block = rootRules.find((r) => r.type === \"disallow\");\n const allow = rootRules.find((r) => r.type === \"allow\");\n if (block && !allow) return { agent, allowed: false, matchedRule: `${header} → ${block.line}` };\n return { agent, allowed: true, matchedRule: allow ? `${header} → ${allow.line}` : null };\n });\n}\n\n/** Word/Google-Docs paste soup and broken page-builder plugins nest ordinary\n * markup far past anything hand-written would reach — a plain recursive walk\n * throws `RangeError: Maximum call stack size exceeded` around 5,000 levels.\n * Mirrors checks.ts's `MAX_SCHEMA_DEPTH` precedent: generous enough that no\n * real page is anywhere near it, so the branch just stops descending. */\nconst MAX_WALK_DEPTH = 100;\n\n/** Same-origin http(s) hrefs in document order, absolute, fragment-stripped, deduped. */\nexport function sameOriginLinks(html: string, baseUrl: string): string[] {\n const site = new URL(baseUrl);\n const doc = parse(html);\n const baseHref = doc.querySelector(\"base\")?.getAttribute(\"href\");\n let resolveBase = site;\n if (baseHref) {\n try {\n resolveBase = new URL(baseHref, site);\n } catch {\n // A malformed <base> is ignored, exactly as a browser ignores it.\n }\n }\n const out: string[] = [];\n const seen = new Set<string>();\n const walk = (el: HTMLElement, depth: number): void => {\n if (depth > MAX_WALK_DEPTH) return;\n for (const child of el.childNodes) {\n if (child.nodeType !== NodeType.ELEMENT_NODE) continue;\n const e = child as HTMLElement;\n if (UNRENDERED_TAGS.has(e.tagName)) continue;\n if (e.tagName === \"A\") {\n const href = e.getAttribute(\"href\");\n if (href) {\n let u: URL | null;\n try {\n u = new URL(href, resolveBase);\n } catch {\n u = null;\n }\n if (\n u &&\n u.origin === site.origin &&\n (u.protocol === \"http:\" || u.protocol === \"https:\")\n ) {\n u.hash = \"\";\n const norm = u.toString();\n if (!seen.has(norm)) {\n seen.add(norm);\n out.push(norm);\n }\n }\n }\n }\n walk(e, depth + 1);\n }\n };\n walk(doc, 0);\n return out;\n}\n\n/** Sitemap XML escapes `&` as `&amp;`, so a URL with a query string arrives\n * encoded; handing that straight to fetch requests a different resource. */\nfunction decodeXmlText(s: string): string {\n return (\n s\n .replace(/<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>/g, \"$1\")\n .replace(/&#x([0-9a-f]+);/gi, (_, hex: string) => String.fromCodePoint(parseInt(hex, 16)))\n .replace(/&#(\\d+);/g, (_, dec: string) => String.fromCodePoint(Number(dec)))\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&apos;/g, \"'\")\n // Last, so `&amp;lt;` decodes to the literal `&lt;` rather than to `<`.\n .replace(/&amp;/g, \"&\")\n );\n}\n\nexport function parseSitemapLocs(xml: string): string[] {\n return [...xml.matchAll(/<(?:[\\w-]+:)?loc\\b[^>]*>([\\s\\S]*?)<\\/(?:[\\w-]+:)?loc>/gi)]\n .map((m) => decodeXmlText(m[1] ?? \"\").trim())\n .filter(Boolean);\n}\n\n/**\n * May we fetch this nested sitemap?\n *\n * A `<loc>` inside a sitemap index is attacker-controlled input — it comes from\n * the site being audited, which by definition is a stranger's. Same-origin is\n * the binding constraint: a sitemap index legitimately only ever points at\n * sitemaps on its own site, so anything else is a mistake or an attempt, and\n * refusing costs nothing real.\n *\n * The private-host check is defence in depth behind that. It is redundant while\n * `origin` is a public site — which the dispatch path already enforces — but\n * this function should not depend on a guarantee made three files away.\n */\nexport function isSafeNestedSitemap(child: string, origin: string): boolean {\n let url: URL;\n try {\n // Resolved against `origin` so a relative <loc> is judged on where it\n // actually lands, not on the string.\n url = new URL(child, origin);\n } catch {\n return false;\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") return false;\n if (url.origin !== origin) return false;\n return !isPrivateOrLoopbackHost(url.hostname);\n}\n\nexport function isSitemapIndex(xml: string): boolean {\n return /<(?:[\\w-]+:)?sitemapindex[\\s>]/i.test(xml);\n}\n\nexport type FetchResponse = {\n status: number;\n body: string;\n headers: Record<string, string>;\n /** The URL after redirects. Absent → the requested URL was final. */\n url?: string;\n};\n\n/**\n * What one browser pass produced for a page.\n *\n * A bare string is still accepted, and every injected test stub returns one —\n * which normalises to `axe: null`, meaning \"we ran no accessibility rules here\".\n * That is the honest reading: absence of results must never render as absence\n * of violations, and widening the type this way keeps the seventeen existing\n * stubs correct rather than making them lie by omission.\n */\nexport type RenderedPage = {\n html: string;\n axe: AxePageResult | null;\n vitals?: PageVitals | null;\n /** Present only on the one page we interacted with. */\n formProbe?: FormProbe | null;\n};\n\nexport type CrawlDeps = {\n fetchUrl: (url: string) => Promise<FetchResponse>;\n /**\n * Rendered DOM per URL, and whatever else the one browser pass collected. A\n * URL absent from the map has no rendered extract.\n *\n * The accessibility rules run HERE rather than in a stage of their own,\n * because a stage of their own would mean opening a second browser and\n * navigating every page a second time — doubling the heaviest traffic we put\n * on a stranger's server to learn something the first visit could have told\n * us.\n */\n renderPages: (urls: string[]) => Promise<Map<string, string | RenderedPage>>;\n maxPages: number;\n delayMs: number;\n};\n\n/** Normalises the union above. A stub that returns plain HTML has measured no\n * accessibility rules, and says so. */\nexport function asRendered(v: string | RenderedPage | undefined): RenderedPage | null {\n if (v === undefined) return null;\n // No `formProbe` key: a caller handing back raw HTML never opened a browser,\n // so it did not try a form — which is not the same as trying and finding none.\n return typeof v === \"string\" ? { html: v, axe: null, vitals: null } : v;\n}\n\n/** Honest, identified UA — we audit on the prospect's behalf and say so. */\nexport const USER_AGENT = \"ReddoorAudit/1.0 (+https://reddoorla.com/; operator-run site audit)\";\n\nconst ASSET_EXT = /\\.(pdf|jpe?g|png|gif|webp|avif|svg|zip|mp4|mov|css|js|xml|json)$/i;\n\n/** A generous multi-megabyte ceiling on any one response body — a real\n * business homepage (or robots.txt/llms.txt/sitemap.xml) is far below it.\n * Applies to every fetch `defaultCrawlDeps` makes: without it, an 80MB\n * page-builder bloat page reads in whole (proven: process RSS 64MB → 423MB),\n * and up to `maxPages` of those can be held in memory at once. Exported so a\n * test can assert against it directly. */\nexport const MAX_RESPONSE_BYTES = 5_000_000;\n\n/** How long to let a page settle after `load` before capturing its DOM.\n *\n * The rendered extract exists to be compared against the raw HTML, and the\n * difference is what a client-side framework painted — so the capture has to\n * happen after hydration or it measures nothing. Long enough for that,\n * nowhere near long enough to wait out a polling widget. Exported so a test\n * can assert the budget rather than rediscover it. */\nexport const RENDER_SETTLE_MS = 1_500;\n\n/** Thrown by `defaultCrawlDeps().fetchUrl` when a body exceeds\n * `MAX_RESPONSE_BYTES` — either the declared `content-length` refuses the\n * request early, or the actual byte count catches a missing or lying header\n * mid-stream. Its own class so callers can tell \"too large\" apart from a\n * genuine transport failure: `optional()` below treats it as an absent\n * sidecar (like a 404), not a fetch error worth reporting. */\nexport class ResponseTooLargeError extends Error {\n constructor(url: string) {\n super(`response exceeds the ${MAX_RESPONSE_BYTES}-byte limit: ${url}`);\n this.name = \"ResponseTooLargeError\";\n }\n}\n\n/** Exported so probes.ts (which paces calls to metered AI-visibility APIs\n * with the same `pacedEach`) doesn't need its own identical copy. */\nexport const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/** How many sitemap URLs are carried forward for probing. Ten times the number\n * Tier 2 samples, so the sample is drawn from a spread of the sitemap rather\n * than its first twelve entries — which on most CMSes are the newest posts. */\nexport const MAX_SITEMAP_SAMPLE = 120;\n\n/** How many children of a sitemap index we follow. Was 3, which read 66 of\n * viget.com's 1,636 URLs and turned our own ceiling into a finding about their\n * sitemap. Each child is still filtered by `isSafeNestedSitemap` BEFORE the cap\n * applies, so raising it does not widen what a hostile index can reach — it\n * only stops a real one being cut off. Whatever the number, going over it now\n * sets `truncated`, and a truncated read may not become a finding. */\nexport const MAX_SITEMAP_CHILDREN = 25;\n\n/** Ceilings on the per-page browser work. Generous — a slow page should still\n * be measured — but finite, because the alternative is losing the crawl. */\nexport const CONTENT_BUDGET_MS = 20_000;\nexport const AXE_BUDGET_MS = 45_000;\nexport const VITALS_BUDGET_MS = 30_000;\n\n/** Runs `fn` once per item, waiting `delayMs` between calls but never before\n * the first — the same courtesy pacing the raw-fetch loop already applies to\n * a prospect's server, given here to the heavier half of the traffic (each\n * Playwright navigation pulls images, fonts and third-party scripts). A\n * standalone, dependency-free function so the pacing itself is unit-testable\n * without loading Playwright. */\nexport async function pacedEach<T>(\n items: T[],\n delayMs: number,\n fn: (item: T) => Promise<void>,\n sleepFn: (ms: number) => Promise<void> = sleep,\n): Promise<void> {\n for (let i = 0; i < items.length; i++) {\n if (i > 0 && delayMs > 0) await sleepFn(delayMs);\n await fn(items[i]!);\n }\n}\n\ntype SidecarFetch = { res: FetchResponse | null; error: string | null };\n\n/** A 4xx/404 is a genuinely absent file — `{res: null, error: null}`. A thrown\n * request means we do not know whether the file exists — `{res: null, error}`\n * — and that distinction must survive downstream: reporting \"no robots.txt\"\n * when the fetch itself failed would claim the site's crawlers are\n * unrestricted when we simply never looked. A `ResponseTooLargeError` is\n * neither: we DO know the file exists, we simply decline to read all of it —\n * so it reads as absent, exactly like a 404, rather than as a sidecar error. */\nasync function optional(deps: CrawlDeps, url: string): Promise<SidecarFetch> {\n try {\n const res = await deps.fetchUrl(url);\n return res.status >= 400 ? { res: null, error: null } : { res, error: null };\n } catch (err) {\n if (err instanceof ResponseTooLargeError) return { res: null, error: null };\n return { res: null, error: err instanceof Error ? err.message : String(err) };\n }\n}\n\n/** robots.txt alone decides the whole crawler-access section of the report, so\n * a transient failure on it — and only it — is worth one extra request before\n * we give up and record the error. */\nasync function optionalRobots(deps: CrawlDeps, url: string): Promise<SidecarFetch> {\n const first = await optional(deps, url);\n return first.error === null ? first : optional(deps, url);\n}\n\n/** A text sidecar that actually is text. Netlify/SPA catch-alls answer 200 with\n * an HTML shell for /robots.txt and /llms.txt; reading that as a robots file\n * would invent rules the site never wrote. */\nfunction textSidecar(res: FetchResponse | null): string | null {\n if (!res) return null;\n const body = res.body.trim();\n if (!body || body.startsWith(\"<\")) return null;\n return res.body;\n}\n\n/** Case-insensitive header lookup — `FetchResponse.headers` keys arrive in\n * whatever case the server (or a test stub) used. */\nfunction headerValue(headers: Record<string, string>, name: string): string | null {\n for (const [k, v] of Object.entries(headers)) {\n if (k.toLowerCase() === name) return v;\n }\n return null;\n}\n\n/**\n * The dedupe key for \"is this the same page?\".\n *\n * A trailing slash addresses the same page — sites link to `/services` in the\n * nav and `/services/` in the footer constantly — and crawling both produced a\n * duplicate-title finding about one page, which the client cannot fix because\n * there is nothing to fix. The query string is KEPT: `?page=2` genuinely is\n * another page, and folding it in would silently drop paginated content from\n * the crawl.\n */\nfunction samePageKey(u: URL): string {\n const path = u.pathname.replace(/\\/+$/, \"\") || \"/\";\n return `${u.origin.toLowerCase()}${path}${u.search}`;\n}\n\n/**\n * Which URLs to spend the page budget on.\n *\n * DISTINCT PATHS FIRST, query variants with whatever is left. The budget is\n * small and a query string is the cheapest way for a site to spend all of it on\n * one page: apple.com handed us `/accessibility/features/` five times under\n * `?vision`, `?hearing`, `?speech` and `?cognitive` — tab deep-links into a\n * single page — plus `/airpods-pro/?campaign=true`, so six of twenty slots went\n * to two pages we had already read.\n *\n * A PREFERENCE, never a filter, because `samePageKey` is right that `?page=2`\n * is genuinely another page. A blog whose only content is `?page=N` still gets\n * crawled: those URLs simply queue behind the distinct paths instead of\n * crowding them out.\n */\nfunction normalizeCandidates(\n urls: string[],\n origin: string,\n max: number,\n robotsTxt: string | null,\n): string[] {\n const firstOfPath: string[] = [];\n const variants: string[] = [];\n const seen = new Set<string>();\n const paths = new Set<string>();\n for (const raw of urls) {\n let u: URL;\n try {\n u = new URL(raw);\n } catch {\n continue;\n }\n if (u.origin !== origin) continue;\n if (ASSET_EXT.test(u.pathname)) continue;\n // Infrastructure a crawler can follow but a visitor never lands on — see\n // pages.ts. Following them costs a page of the budget and then invites a\n // finding about a URL the prospect never chose to publish.\n if (isInfraPath(u.toString())) continue;\n // The site's own Disallow, obeyed. Not the AI-crawler matrix — that reports\n // what OTHER crawlers may do. This is us.\n if (!robotsAllowsUs(robotsTxt, u.toString(), USER_AGENT)) continue;\n u.hash = \"\";\n const key = samePageKey(u);\n if (seen.has(key)) continue;\n seen.add(key);\n const path = `${u.origin.toLowerCase()}${u.pathname.replace(/\\/+$/, \"\") || \"/\"}`;\n if (paths.has(path)) variants.push(u.toString());\n else {\n paths.add(path);\n firstOfPath.push(u.toString());\n }\n }\n return [...firstOfPath, ...variants].slice(0, max);\n}\n\ntype Sidecars = {\n robotsTxt: string | null;\n agentAccess: RobotsAgentAccess[];\n llmsTxt: { present: boolean; firstLine: string | null };\n sitemapUrls: string[];\n sitemapPresent: boolean;\n sitemapTruncated: boolean;\n sidecarErrors: { robots: string | null; llms: string | null; sitemap: string | null };\n};\n\n/** Kept as a function so the field is written from one place, and so a future\n * sidecar shape that cannot answer the question returns `true` rather than\n * silently claiming a complete read. */\nfunction sitemapTruncatedOf(s: Sidecars): boolean {\n return s.sitemapTruncated;\n}\n\n/** robots.txt, llms.txt and sitemap.xml — every one degrades instead of\n * failing the crawl, but a transport failure must stay visible in the result\n * (see `optional`/`optionalRobots` above) rather than reading as \"file\n * absent\". */\nasync function fetchSidecars(origin: string, deps: CrawlDeps): Promise<Sidecars> {\n const robots = await optionalRobots(deps, `${origin}/robots.txt`);\n const robotsTxt = textSidecar(robots.res);\n const agentAccess = evaluateAgentAccess(\n robotsTxt && /user-agent/i.test(robotsTxt) ? robotsTxt : null,\n );\n\n const llms = await optional(deps, `${origin}/llms.txt`);\n const llmsRaw = textSidecar(llms.res);\n const llmsTxt = llmsRaw\n ? {\n present: true,\n firstLine:\n llmsRaw\n .split(/\\r?\\n/)\n .find((l) => l.trim())\n ?.trim() ?? null,\n }\n : { present: false, firstLine: null };\n\n const sitemap = await optional(deps, `${origin}/sitemap.xml`);\n let sitemapUrls: string[] = [];\n let sitemapPresent = false;\n let sitemapTruncated = false;\n if (sitemap.res && /<(urlset|sitemapindex)[\\s>]/i.test(sitemap.res.body)) {\n sitemapPresent = true;\n if (isSitemapIndex(sitemap.res.body)) {\n // `child` is attacker-controlled: it comes out of the AUDITED site's\n // sitemap, and the audited site is a prospect we have not met. Without\n // isSafeNestedSitemap it reached fetch() unchecked — a hostile index could\n // make the runner request 169.254.169.254, 127.0.0.1 or any internal host,\n // from a GitHub Actions job holding TURSO_AUTH_TOKEN, RESEND_API_KEY and\n // ANTHROPIC_API_KEY.\n //\n // Filter BEFORE the cap, not inside the loop after it. Taking the first\n // three and then discarding the unsafe ones lets three hostile entries at\n // the top of an index consume the entire budget and starve out the site's\n // real sitemaps — the crawl then silently sees fewer pages, which is a\n // quieter failure than the SSRF itself. Caught by the positive control in\n // the SSRF test, not by reading the loop.\n const safe = parseSitemapLocs(sitemap.res.body).filter((child) =>\n isSafeNestedSitemap(child, origin),\n );\n const children = safe.slice(0, MAX_SITEMAP_CHILDREN);\n sitemapTruncated = safe.length > children.length;\n for (const child of children) {\n const nested = await optional(deps, child);\n // A child we could not fetch is a piece of their sitemap we did not\n // read, and the count that comes out is short by however much it held.\n if (nested.res) sitemapUrls.push(...parseSitemapLocs(nested.res.body));\n else sitemapTruncated = true;\n }\n } else {\n sitemapUrls = parseSitemapLocs(sitemap.res.body);\n }\n }\n\n return {\n robotsTxt,\n agentAccess,\n llmsTxt,\n sitemapUrls,\n sitemapPresent,\n sitemapTruncated,\n sidecarErrors: { robots: robots.error, llms: llms.error, sitemap: sitemap.error },\n };\n}\n\n/**\n * Fetch the prospect's site: robots/sitemap/llms sidecars, then up to\n * `maxPages` same-origin pages, each captured BOTH as raw HTTP HTML (what a\n * non-JS crawler sees) and as rendered DOM (what a browser sees). Sequential\n * and delayed — this is someone else's server.\n */\nexport async function crawlSite(rawUrl: string, deps: CrawlDeps): Promise<CrawlResult> {\n const start = new URL(rawUrl);\n // A fragment is never sent over the wire; keeping it here makes the\n // homepage's own candidate (hash-stripped by normalizeCandidates below) fail\n // to match `start.toString()` later and fetches the homepage a second time.\n start.hash = \"\";\n // A prospect-supplied URL can carry HTTP userinfo (`user:pass@host`); that\n // credential must never reach the origin, the page list, the result object,\n // the database, or the published report. Strip it immediately, before\n // anything downstream is derived from `start`.\n start.username = \"\";\n start.password = \"\";\n // Check the ENTRY host before the first fetch. The redirect guard below has\n // always covered where a hostile site can send us SECOND; nothing covered a\n // caller pointing the crawler at an internal address to BEGIN with, and the\n // CLI validates only `isHttpUrl` — so one fetch of 169.254.169.254 happened\n // before the throw.\n if (isPrivateOrLoopbackHost(start.hostname)) {\n throw Object.assign(\n new Error(\n `${start.toString()} is a private address (${start.hostname}) — refusing to crawl it.`,\n ),\n { exitCode: 1 },\n );\n }\n\n let home: FetchResponse;\n try {\n home = await deps.fetchUrl(start.toString());\n } catch (err) {\n throw Object.assign(\n new Error(\n `Could not reach ${start.toString()}: ${err instanceof Error ? err.message : String(err)}`,\n ),\n { exitCode: 1 },\n );\n }\n if (home.status >= 400) {\n throw Object.assign(\n new Error(`${start.toString()} returned HTTP ${home.status} — nothing to audit.`),\n { exitCode: 1 },\n );\n }\n\n // The homepage may have redirected to a different host — apex → www is the\n // most common redirect in production hosting. Every sidecar URL, the link\n // base, and the first page candidate must key off where the response\n // actually landed, not the URL we requested, or the whole site's real pages\n // get filtered out against a stale origin.\n let resolved: URL;\n try {\n resolved = new URL(home.url ?? start.toString());\n } catch {\n resolved = start;\n }\n // The PROSPECT controls where their own redirect lands — trusting it blindly\n // would let a hostile site send this crawler at an internal target. Checked\n // by address literal only (no DNS resolution): a deliberate, proportionate\n // bound for an operator-run CLI, not a complete SSRF guard.\n if (isPrivateOrLoopbackHost(resolved.hostname)) {\n throw Object.assign(\n new Error(\n `${start.toString()} redirected to a private address (${resolved.hostname}) — refusing to crawl it.`,\n ),\n { exitCode: 1 },\n );\n }\n const resolvedUrl = resolved.toString();\n const origin = resolved.origin;\n\n const sidecars = await fetchSidecars(origin, deps);\n\n const pageUrls = normalizeCandidates(\n [resolvedUrl, ...sidecars.sitemapUrls, ...sameOriginLinks(home.body, resolvedUrl)],\n origin,\n deps.maxPages,\n sidecars.robotsTxt,\n );\n\n const rendered = await deps\n .renderPages(pageUrls)\n .catch(() => new Map<string, string | RenderedPage>());\n\n const pages: PageCapture[] = [];\n for (const url of pageUrls) {\n let res: FetchResponse | null = null;\n let error: string | null = null;\n if (url === resolvedUrl) {\n res = home;\n } else {\n if (deps.delayMs > 0) await sleep(deps.delayMs);\n try {\n res = await deps.fetchUrl(url);\n } catch (err) {\n error = err instanceof Error ? err.message : String(err);\n }\n }\n const renderedPage = asRendered(rendered.get(url));\n const renderedHtml = renderedPage?.html ?? null;\n // A sitemap or nav link can point at a non-HTML resource (a linked PDF\n // brochure, say). Decoding that as HTML produces raw/rendered divergence\n // that has nothing to do with the prospect's JavaScript, so it must not\n // read as \"usable\" — but a missing content-type header stays usable.\n const contentType = res ? headerValue(res.headers, \"content-type\") : null;\n const notHtmlReason =\n contentType !== null && !contentType.toLowerCase().includes(\"html\")\n ? `not HTML (${contentType})`\n : null;\n const usable = res !== null && res.status < 400 && notHtmlReason === null;\n pages.push({\n url,\n status: res?.status ?? null,\n raw: usable ? extractPage(res!.body) : null,\n // Gated on `usable` for the same reason `raw` is. A browser paints\n // something for a 404 — Cloudflare's email-protection interstitial is the\n // case that bit us — and a captured paint is not evidence that the URL is\n // a page of this website. Leaving it non-null let every cross-page check\n // that asked \"is there an extract?\" admit a page the server refused, and\n // that one URL became the only dead end and the only off-template page\n // found in the entire stored corpus.\n rendered: usable && renderedHtml ? extractPage(renderedHtml) : null,\n error: error ?? (res && res.status >= 400 ? `HTTP ${res.status}` : notHtmlReason),\n // Gated on `usable` for the same reason as `rendered`: a browser paints\n // something for a 404, and running the rule set over an error page would\n // report that page's failings as the website's.\n axe: usable ? (renderedPage?.axe ?? null) : null,\n vitals: usable ? (renderedPage?.vitals ?? null) : null,\n // Spread, so an absent probe stays absent: `formProbe: undefined` is\n // rejected under exactOptionalPropertyTypes, and `?? null` would turn\n // \"we never tried\" into \"we tried and there was nothing\".\n ...(usable && renderedPage?.formProbe !== undefined\n ? { formProbe: renderedPage.formProbe }\n : {}),\n });\n }\n\n const homeHeaders: Record<string, string> = {};\n for (const [k, v] of Object.entries(home.headers)) homeHeaders[k.toLowerCase()] = v;\n\n return {\n origin,\n robotsTxt: sidecars.robotsTxt,\n agentAccess: sidecars.agentAccess,\n sitemap: {\n present: sidecars.sitemapPresent,\n urlCount: sidecars.sitemapUrls.length,\n // A capped sample, kept so Tier 2 can ask whether the URLs a sitemap\n // advertises actually answer — `urlCount` alone cannot be probed. Capped\n // because this lands in `result_json`, and a 4,000-URL sitemap has no\n // business being stored whole for the sake of sampling twelve of them.\n sample: sidecars.sitemapUrls.slice(0, MAX_SITEMAP_SAMPLE),\n truncated: sitemapTruncatedOf(sidecars),\n },\n llmsTxt: sidecars.llmsTxt,\n sidecarErrors: sidecars.sidecarErrors,\n homeHeaders,\n pages,\n };\n}\n\n/** Reads `res`'s body as text, refusing once it would exceed\n * `MAX_RESPONSE_BYTES`. Checks the declared `content-length` first so an\n * honest oversized response never starts streaming at all; then guards the\n * actual byte count as it streams, so a missing OR a lying header can't get\n * a huge body past the check either. `Body.text()` always decodes as UTF-8\n * per the Fetch spec, so decoding the accumulated bytes the same way here\n * reproduces `res.text()` exactly for anything under the cap.\n *\n * Exported because every stage that reads a body off a stranger's server needs\n * this same guard, and a second copy of it is a second place for the limit to\n * drift out of step with the one that has tests. */\nexport async function readCapped(res: Response, url: string): Promise<string> {\n const declared = res.headers.get(\"content-length\");\n if (declared !== null && Number(declared) > MAX_RESPONSE_BYTES) {\n throw new ResponseTooLargeError(url);\n }\n if (!res.body) return \"\";\n const reader = res.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (!value) continue;\n total += value.byteLength;\n if (total > MAX_RESPONSE_BYTES) {\n await reader.cancel();\n throw new ResponseTooLargeError(url);\n }\n chunks.push(value);\n }\n const merged = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return new TextDecoder(\"utf-8\").decode(merged);\n}\n\n/** The viewport the overflow check is named for — an iPhone SE, the narrowest\n * screen still worth designing for and the one that finds the bug. */\nconst MOBILE_WIDTH = 375;\nconst MOBILE_HEIGHT = 812;\n/** What the crawl renders at otherwise. Restored after each measurement so the\n * next page is captured the same way as the first. */\nconst DESKTOP_WIDTH = 1280;\nconst DESKTOP_HEIGHT = 720;\n/** How long the narrow-viewport reflow gets to settle, and how often it is\n * read. Sized off the worst real page measured so far: apple.com's comparison\n * table needs about a second, so the budget is several times that and the step\n * is short enough that an ordinary page pays a quarter of a second. */\nconst OVERFLOW_SETTLE_BUDGET_MS = 5_000;\nconst OVERFLOW_SETTLE_STEP_MS = 250;\n\n/**\n * What the browser noticed, measured in the page it has already loaded.\n *\n * Every measurement here is a reflow or a DOM read — no navigation, no request.\n * Returns null on any failure, for the same reason `runAxe` does: a measurement\n * that fell over is ours, and reporting a page as clean because the measuring\n * threw would be a false all-clear.\n */\nasync function measureVitals(\n page: import(\"@playwright/test\").Page,\n collected: { consoleErrors: string[]; failed: PageVitals[\"failedRequests\"] },\n): Promise<PageVitals | null> {\n try {\n // Desktop first, because that is how the page was rendered and captured.\n const desktop = await page.evaluate(\n ({ maxImages }) => {\n const tiny: { count: number; sample: string | null } = { count: 0, sample: null };\n for (const el of Array.from(document.body.querySelectorAll(\"*\"))) {\n // Only elements with their own text, so a wrapper does not inherit\n // the blame for its child's font size.\n const ownText = Array.from(el.childNodes)\n .filter((n) => n.nodeType === 3)\n .map((n) => n.textContent ?? \"\")\n .join(\"\")\n .trim();\n if (ownText.length < 12) continue;\n const size = parseFloat(getComputedStyle(el).fontSize);\n if (Number.isFinite(size) && size > 0 && size < 12) {\n tiny.count += 1;\n tiny.sample ??= ownText.slice(0, 80);\n }\n }\n\n const oversized: { src: string; naturalWidth: number; renderedWidth: number }[] = [];\n for (const img of Array.from(document.images)) {\n const rendered = img.clientWidth;\n // Twice the drawn width in each direction is four times the pixels,\n // which is where the waste becomes worth a sentence. A 2x retina\n // asset is deliberately under that bar.\n if (rendered > 0 && img.naturalWidth > rendered * 2.5) {\n oversized.push({\n src: img.currentSrc || img.src,\n naturalWidth: img.naturalWidth,\n renderedWidth: rendered,\n });\n }\n }\n oversized.sort((a, b) => b.naturalWidth - a.naturalWidth);\n return { tiny, oversized: oversized.slice(0, maxImages) };\n },\n { maxImages: MAX_OVERSIZED_IMAGES },\n );\n\n // Then the narrow viewport. A resize reflows a page the browser already\n // holds — no navigation, no new bytes from the prospect's server.\n await page.setViewportSize({ width: MOBILE_WIDTH, height: MOBILE_HEIGHT });\n // POLLED UNTIL IT STOPS MOVING, not sampled once after 250ms.\n //\n // A single 250ms sample measured pages mid-reflow and reported the\n // transient as the finding. apple.com's AirPods comparison page, resized\n // from 1280 to 375, reads 303px over at 250ms, 26px at 500ms and 0px from\n // 1000ms on; loaded at 375 in the first place it never overflows at all. We\n // told a client their page scrolls sideways on a phone about a page that\n // does not, which is the most quotable finding shape we produce.\n //\n // Two consecutive equal reads is the settle condition. The budget is a\n // ceiling, not a target: a static page settles on the second read and pays\n // one extra interval.\n const overflowAt375 = await settledOverflow(\n () =>\n page.evaluate(() => {\n const doc = document.documentElement;\n return Math.max(0, Math.round(doc.scrollWidth - doc.clientWidth));\n }),\n (ms) => page.waitForTimeout(ms),\n );\n await page.setViewportSize({ width: DESKTOP_WIDTH, height: DESKTOP_HEIGHT });\n\n return {\n consoleErrors: collected.consoleErrors.slice(0, MAX_CONSOLE_ERRORS),\n failedRequests: collected.failed.slice(0, MAX_FAILED_REQUESTS),\n overflowAt375,\n tinyText: desktop.tiny,\n oversizedImages: desktop.oversized,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Read the narrow-viewport overflow once it has stopped moving.\n *\n * Exported for its test. Two consecutive equal reads is the settle condition;\n * the budget is a ceiling, not a target, so a static page settles on the second\n * read and pays one extra interval.\n */\nexport async function settledOverflow(\n read: () => Promise<number>,\n wait: (ms: number) => Promise<unknown>,\n budgetMs: number = OVERFLOW_SETTLE_BUDGET_MS,\n stepMs: number = OVERFLOW_SETTLE_STEP_MS,\n): Promise<number> {\n let last = -1;\n const deadline = Date.now() + budgetMs;\n for (;;) {\n await wait(stepMs);\n const now = await read();\n // A page still moving when the budget runs out has told us nothing\n // trustworthy, and a number we do not trust must not become a finding about\n // somebody's site. The last reading stands only if it agrees with the one\n // before it; otherwise we report no overflow rather than a transient.\n if (now === last) return now;\n if (Date.now() > deadline) return 0;\n last = now;\n }\n}\n\n/**\n * Run the whole axe rule set against the page already loaded in this tab.\n *\n * No `withTags` call, deliberately. Restricting to `wcag2a`/`wcag2aa`/`wcag21a`/\n * `wcag21aa` — which is what our own fleet audit does — silently drops every\n * landmark and heading-order rule, because those are tagged `best-practice`.\n * The default set is all of them, and the structural rules are the ones most\n * worth having.\n *\n * Returns null rather than throwing on any failure. An accessibility scan that\n * fell over is OUR missing measurement, and a page reported with no violations\n * because the scanner crashed would be the worst kind of false all-clear.\n */\nasync function runAxe(page: import(\"@playwright/test\").Page): Promise<AxePageResult | null> {\n try {\n // The NAMED export, not the default. The package publishes both, and under\n // Node's CJS interop the `default` binding resolves to the whole module\n // namespace rather than the class — which type-checks as \"not\n // constructable\" and would have thrown at runtime.\n const { AxeBuilder } = await import(\"@axe-core/playwright\");\n const results = await new AxeBuilder({ page }).analyze();\n return {\n violations: results.violations.map((v) => ({\n id: v.id,\n impact: (v.impact ?? null) as AxePageResult[\"violations\"][number][\"impact\"],\n help: v.help,\n helpUrl: v.helpUrl,\n nodes: v.nodes.length,\n // One element, truncated: enough for a reader to find it on the page,\n // not enough to bloat a stored report with a page of markup.\n sample: v.nodes[0]?.html?.slice(0, 200) ?? null,\n })),\n passes: results.passes.length,\n incomplete: results.incomplete.length,\n // The ids as well as the count: \"three rules need a human\" is a worry,\n // \"colour contrast over an image needs a human\" is something to look at.\n incompleteIds: results.incomplete.map((r) => r.id),\n // Carried so the report can say how many rules had something to check\n // rather than implying we checked all ninety.\n inapplicable: results.inapplicable.length,\n };\n } catch {\n return null;\n }\n}\n\n/** Real deps: identified sequential fetches + one shared Playwright chromium.\n * Playwright is imported lazily so unit tests (which inject deps) never load it. */\nexport function defaultCrawlDeps(\n over: Partial<CrawlDeps> & {\n /**\n * Whether to press a form on the first page that has one. Default true —\n * production behaviour, and the only way `form-rejects-empty` and\n * `form-rejects-bad-email` can ever answer.\n *\n * Turn it OFF for a read-only pass. The abort harness stops the submission\n * before it leaves the browser and there are real-Chromium tests, control\n * cases included, that say so — but pressing a stranger's send button is\n * still an act, and a bulk re-crawl of sites we might approach is not the\n * place to perform it dozens of times over.\n *\n * With it off the two probe checks report `unmeasured` and say we did not\n * try, rather than \"we found no enquiry form\" — which would be a false\n * statement about their site made to cover a choice of ours.\n */\n probeForms?: boolean;\n } = {},\n): CrawlDeps {\n // Resolved up front so the renderPages closure below paces itself on the\n // SAME delay the caller configured, instead of silently ignoring it.\n const { probeForms: pressForms = true, ...deps } = over;\n const maxPages = over.maxPages ?? 20;\n const delayMs = over.delayMs ?? 500;\n return {\n async fetchUrl(url) {\n const res = await fetch(url, {\n headers: {\n \"user-agent\": USER_AGENT,\n accept: \"text/html,application/xhtml+xml,text/plain,*/*\",\n },\n redirect: \"follow\",\n signal: AbortSignal.timeout(20_000),\n });\n const headers: Record<string, string> = {};\n res.headers.forEach((v, k) => {\n headers[k] = v;\n });\n return { status: res.status, body: await readCapped(res, url), headers, url: res.url };\n },\n async renderPages(urls) {\n const { chromium } = await import(\"@playwright/test\");\n const out = new Map<string, string | RenderedPage>();\n const browser = await chromium.launch();\n try {\n const ctx = await browser.newContext({\n userAgent: USER_AGENT,\n viewport: { width: DESKTOP_WIDTH, height: DESKTOP_HEIGHT },\n });\n const page = await ctx.newPage();\n\n // Listeners are attached ONCE and buffer into `collected`, which is\n // cleared per URL. Attaching them per navigation would miss everything\n // the page reports during load, which is most of it.\n const collected = {\n consoleErrors: [] as string[],\n failed: [] as PageVitals[\"failedRequests\"],\n };\n let currentOrigin = \"\";\n const noteError = (message: string): void => {\n const trimmed = message.trim().slice(0, 300);\n // Deduped: one broken component in a loop can report the same line a\n // thousand times, and a thousand copies is not a thousand problems.\n if (trimmed && !collected.consoleErrors.includes(trimmed)) {\n collected.consoleErrors.push(trimmed);\n }\n };\n page.on(\"pageerror\", (err) => noteError(err.message));\n page.on(\"console\", (msg) => {\n if (msg.type() === \"error\") noteError(msg.text());\n });\n const noteFailed = (url: string, status: number | null): void => {\n // A URL we cannot parse is not evidence that it belongs to this site,\n // so it counts as third-party — the direction that cannot manufacture\n // a finding against the prospect.\n const firstParty = ((): boolean => {\n try {\n return new URL(url).origin === currentOrigin;\n } catch {\n return false;\n }\n })();\n if (collected.failed.length < MAX_FAILED_REQUESTS * 4) {\n collected.failed.push({ url: url.slice(0, 200), status, firstParty });\n }\n };\n // ABORTED IS NOT FAILED.\n //\n // A browser cancels partial media loads as a matter of routine — it\n // asks for the first bytes of a hero video, decides it has enough, and\n // drops the rest. Playwright reports that as `requestfailed` with\n // `net::ERR_ABORTED`, and so does every request cut short by a\n // navigation. apple.com came back with nine \"failed\" requests, every\n // one of them a .webm the browser had deliberately stopped fetching.\n // Printing those as broken files is our misreading of the browser, on\n // a site whose videos play perfectly.\n page.on(\"requestfailed\", (req) => {\n const why = req.failure()?.errorText ?? \"\";\n if (/ERR_ABORTED/i.test(why)) return;\n noteFailed(req.url(), null);\n });\n page.on(\"response\", (res) => {\n if (res.status() >= 400) noteFailed(res.url(), res.status());\n });\n // Playwright navigations are the heavier half of the traffic we put on\n // a stranger's server (each pulls images, fonts, third-party scripts),\n // so they get the same courtesy pacing as the raw fetches.\n let formProbed = false;\n await pacedEach(urls, delayMs, async (url) => {\n try {\n // `load`, NOT `networkidle`.\n //\n // `networkidle` waits for 500ms with no network activity, and a\n // great many real business sites never go quiet: a chat widget, an\n // analytics heartbeat or any polling script keeps the connection\n // busy forever. Every such page burned the full 30s timeout and\n // then threw, so at the production page budget a single chat\n // widget cost the audit ten minutes of nothing.\n //\n // Worse, the catch below turned that into a MISSING rendered\n // extract, so no page produced a raw/rendered pair, `jsDependence`\n // came back null, and the whole Readability score reported \"not\n // measured\" — the open Beachfront readability-null bug, which is\n // this and not a scoring fault at all.\n //\n // Playwright's own docs discourage `networkidle` for exactly this\n // reason. `load` fires once resources are in; the settle below\n // gives client-side frameworks room to hydrate, which is what the\n // rendered extract is actually for.\n collected.consoleErrors.length = 0;\n collected.failed.length = 0;\n try {\n currentOrigin = new URL(url).origin;\n } catch {\n currentOrigin = \"\";\n }\n await page.goto(url, { waitUntil: \"load\", timeout: 20_000 });\n await page.waitForTimeout(RENDER_SETTLE_MS);\n // EVERY ONE OF THESE IS BUDGETED, and the reason is a bug that\n // already happened: an unbounded await in this loop does not fail\n // the page it is on, it stalls the whole crawl, and twenty pages\n // already fetched are lost for one that would not answer. The form\n // probe was the one that actually deadlocked; these three are the\n // same shape and were one slow page away from the same outcome.\n //\n // Each null is already a state downstream understands — no rendered\n // extract, no rules run, no browser measurements — so a timeout\n // costs that page's browser findings and nothing else.\n const html = await withTimeout(page.content(), CONTENT_BUDGET_MS);\n if (html === null) return;\n const axe = await withTimeout(runAxe(page), AXE_BUDGET_MS);\n const vitals = await withTimeout(measureVitals(page, collected), VITALS_BUDGET_MS);\n // LAST, and once per crawl. Everything above reads the page as it\n // was served; this one presses a button, so it cannot run before\n // the extract and the rules that describe the untouched document.\n // Once, because a site has one enquiry form, and probing the same\n // form on five pages is five sets of aborted requests for one\n // answer.\n //\n // BUDGETED, because a stall here used to cost the whole crawl:\n // twenty pages already fetched, thrown away, for two checks. A\n // probe that runs out of time is abandoned and reads as \"not\n // measured\", which is the same trade every other stage makes.\n //\n // `formProbe` is left ABSENT — not null — when probing is off.\n // Absent means \"we never tried\"; null means \"we tried and this page\n // had no form we could identify\". Only one of those is a fact about\n // their site, and the checks have to be able to tell them apart.\n if (!pressForms) {\n out.set(url, { html, axe, vitals });\n } else {\n let formProbe: FormProbe | null = null;\n if (!formProbed) {\n formProbe = await withTimeout(\n probeForms(pageInteractionDeps(page, url)),\n PROBE_BUDGET_MS,\n );\n if (formProbe) formProbed = true;\n }\n out.set(url, { html, axe, vitals, formProbe });\n }\n } catch {\n // A page that won't render simply has no rendered extract.\n }\n });\n } finally {\n await browser.close();\n }\n return out;\n },\n maxPages,\n delayMs,\n ...deps,\n };\n}\n","import type { PageCapture, PageExtract } from \"./types.js\";\n\n/**\n * Which of the crawled URLs are actually PAGES of this website?\n *\n * Every check that reasons across pages — the journey graph, the shared\n * template, the goal-fit content checks — first has to answer this, and the\n * cost of each one answering it privately was a class of finding the audit\n * exists to prevent: OUR missing data reported as THEIR defect.\n *\n * Two real cases, both found by replaying the checks over stored audits:\n *\n * - Cloudflare's `/cdn-cgi/l/email-protection` is linked from the obfuscated\n * mailto on the page, answers HTTP 404, and still paints something a\n * browser can screenshot. Consumers that asked only \"is there an extract?\"\n * admitted it, and it became the only dead end and the only off-template\n * page in the entire stored corpus — a report telling a real client that a\n * visitor landing there \"is in a different website with no way back\", about\n * a URL no human ever lands on.\n * - A page whose Playwright render timed out has `raw` but no `rendered`.\n * Judged from `raw` while its siblings are judged from `rendered`, it\n * carries a different set of links than they do and falls out of the shared\n * navigation — so our timeout became \"this page is built outside your\n * template\". Hence `usablePages` picking ONE view for the whole set.\n *\n * The rule both cases share: a page earns its place by being fetched\n * successfully, not by having left something behind that we can parse.\n */\n\nexport type PageView = \"rendered\" | \"raw\";\n\nexport type UsablePage = {\n page: PageCapture;\n /** The chosen view's extract — never null, and always the same view as every\n * other entry in the same set. */\n extract: PageExtract;\n};\n\nexport type UsablePageSet = {\n pages: UsablePage[];\n /** Which view every entry was read from, so a consumer can say so. */\n view: PageView;\n /** Pages dropped because they lacked the chosen view. Reported rather than\n * silently absent: \"we looked at 12 of your 14 pages\" is a different claim\n * from \"we looked at your site\". */\n excluded: number;\n /**\n * Did every kept page carry an `anchors` array?\n *\n * `PageExtract.anchors` is optional, and its docstring is explicit that an\n * absent array means \"not measured\", never \"no links\". A consumer that reads\n * `anchors ?? []` turns a report stored before anchors existed into a site\n * where every page is a dead end. Consumers must degrade to \"not measured\"\n * when this is false rather than publish the finding.\n */\n anchorsMeasured: boolean;\n};\n\n/**\n * Did this URL come back as a page at all?\n *\n * A null status is a transport failure; 4xx and 5xx are the server declining to\n * give us the page. In every one of those cases we have no evidence about the\n * prospect's site, and \"no evidence\" must never render as a finding.\n */\nexport function fetchedOk(page: PageCapture): boolean {\n return page.status !== null && page.status < 400;\n}\n\n/**\n * Infrastructure URLs that a crawler can follow but a visitor never lands on.\n *\n * These are matched on a path SEGMENT, not a substring, so a blog post at\n * `/blog/cdn-cgi-explained` stays an ordinary page. Kept deliberately short:\n * every entry here is a URL we would otherwise describe to a client as one of\n * their pages, and a wrong entry hides a real page instead.\n */\nconst INFRA_SEGMENTS = [/^cdn-cgi$/i, /^wp-admin$/i, /^wp-json$/i, /^xmlrpc\\.php$/i];\n\nexport function isInfraPath(url: string): boolean {\n let path: string;\n try {\n path = new URL(url).pathname;\n } catch {\n return false;\n }\n return path\n .split(\"/\")\n .filter(Boolean)\n .some((segment) => INFRA_SEGMENTS.some((re) => re.test(segment)));\n}\n\n/**\n * The pages a cross-page check may reason about, all read from one view.\n *\n * The view is whichever of rendered/raw more pages actually have, with rendered\n * winning a tie — rendered is what a human sees, and a nav injected by\n * JavaScript is a real path for the visitor even though the crawlers cannot\n * follow it. (Whether crawlers can see the content is a different question,\n * asked and answered separately by the readability check.)\n */\nexport function usablePages(pages: PageCapture[]): UsablePageSet {\n const candidates = pages.filter((p) => fetchedOk(p) && !isInfraPath(p.url));\n const withRendered = candidates.filter((p) => p.rendered !== null);\n const withRaw = candidates.filter((p) => p.raw !== null);\n const view: PageView = withRendered.length >= withRaw.length ? \"rendered\" : \"raw\";\n const kept = view === \"rendered\" ? withRendered : withRaw;\n\n const usable: UsablePage[] = [];\n for (const page of kept) {\n const extract = view === \"rendered\" ? page.rendered : page.raw;\n if (extract) usable.push({ page, extract });\n }\n\n return {\n pages: usable,\n view,\n excluded: candidates.length - usable.length,\n anchorsMeasured: usable.length > 0 && usable.every((p) => p.extract.anchors !== undefined),\n };\n}\n","/**\n * The two questions you can only answer by pressing the button.\n *\n * Every other check in this battery observes. This one ACTS on a stranger's\n * website, and that changes what has to be true before a line of it runs.\n *\n * THE PROMISE THIS MODULE MAKES: nothing we do here reaches their server.\n * A contact form with no client-side validation, submitted empty, POSTs to\n * whoever reads that inbox — a junk enquiry from an audit they never asked for,\n * and one that would be indistinguishable from a real lead. So the route\n * interceptor is armed BEFORE the first click, it aborts every navigation and\n * every same-origin non-GET, and the count of what it stopped is carried back\n * in `blocked` so the claim \"we submitted nothing\" is checkable rather than\n * merely asserted.\n *\n * That interception is also the measurement. A form that refuses an empty\n * submit never asks the network for anything; a form that accepts one tries,\n * and we catch it trying. The thing that keeps us honest and the thing that\n * produces the finding are the same mechanism.\n *\n * WHY IT IS WORTH THE TROUBLE. \"Your contact form silently does nothing when\n * somebody submits it empty\" is the finding a business acts on within the hour,\n * and there is no way to see it from the markup: the form looks perfect.\n */\n\n/** Text a validation message plausibly contains, in the languages we can claim\n * to read. Deliberately narrow — a false positive here reports a site as fine\n * when its form is broken, which is the direction that costs a real lead. */\nconst ERROR_WORDS =\n /\\b(required|please\\s|cannot be blank|can't be blank|must be|enter a|fill in|invalid|not valid|is not a valid)\\b/i;\n\n/** Values that look like a person filling the form in properly, so the only\n * thing wrong on the second pass is the email address. */\nconst FILLER: Record<string, string> = {\n text: \"Audit Test\",\n search: \"Audit Test\",\n tel: \"2085550142\",\n url: \"https://example.com\",\n number: \"1\",\n date: \"2030-01-01\",\n textarea: \"Checking that this form validates what it is given.\",\n};\n\nexport const INVALID_EMAIL = \"not-an-email\";\n\n/** The values we type into the form, so a request carrying one of them is\n * unmistakably this form's data on its way out — the only signal that\n * separates a hosted form's JSON payload from an analytics beacon. */\nconst SENTINELS = [INVALID_EMAIL, ...Object.values(FILLER)];\n\n/** Takes our marker back off the page. */\nconst REMOVE_MARKER = `() => {\n document.querySelector(\"[data-audit-form]\")?.removeAttribute(\"data-audit-form\");\n}`;\n\nexport type FormProbe = {\n /** The page we probed, so a finding has an address. */\n url: string;\n /**\n * Whether an empty submit was refused.\n *\n * `undefined` means we could not tell — the form sits in a cross-origin\n * iframe, or the click threw, or the browser pass never ran. It is never a\n * failure: a form we could not reach is our gap, not their defect.\n */\n emptyRefused: boolean | undefined;\n /** How it refused, in words a report can print. */\n emptyHow: string | null;\n /** Same, for an address that is not an address. Null when the form asks for\n * no email at all, which is a form we have nothing to say about here. */\n invalidEmailRefused: boolean | null | undefined;\n invalidEmailHow: string | null;\n /** Requests the interceptor stopped. The receipt for \"we sent nothing\". */\n blocked: number;\n};\n\n/** What the page told us about the form it chose, before anything was clicked. */\ntype Chosen = {\n fields: number;\n hasEmail: boolean;\n action: string;\n};\n\n/**\n * Picks the form a visitor would use to make contact.\n *\n * Scored rather than matched, because `<form>` on a real site is as likely to\n * be a search box, a newsletter capture or a language switcher as an enquiry.\n * A search form scores itself out: submitting one is harmless but it answers a\n * question nobody asked.\n */\nconst CHOOSE_FORM = `() => {\n const forms = Array.from(document.querySelectorAll(\"form\"));\n let best = null;\n let bestScore = 0;\n for (const form of forms) {\n const controls = Array.from(form.querySelectorAll(\"input, textarea, select\"));\n const typed = (el) => (el.getAttribute(\"type\") || el.tagName.toLowerCase()).toLowerCase();\n const visible = controls.filter((el) => {\n const t = typed(el);\n if (t === \"hidden\" || t === \"submit\" || t === \"button\" || t === \"reset\") return false;\n const r = el.getBoundingClientRect();\n return r.width > 0 && r.height > 0;\n });\n if (visible.length === 0) continue;\n\n const attrs = (el) =>\n [el.getAttribute(\"name\"), el.getAttribute(\"id\"), el.getAttribute(\"autocomplete\"),\n el.getAttribute(\"placeholder\"), el.getAttribute(\"aria-label\")]\n .filter(Boolean).join(\" \").toLowerCase();\n const hasEmail = visible.some(\n (el) => typed(el) === \"email\" || /\\\\bemail\\\\b|e-mail/.test(attrs(el)),\n );\n const hasMessage = visible.some((el) => el.tagName === \"TEXTAREA\");\n const action = (form.getAttribute(\"action\") || \"\").toLowerCase();\n const looksLikeSearch =\n visible.length === 1 &&\n (visible.some((el) => typed(el) === \"search\") || /search|\\\\/s\\\\b|\\\\?q=/.test(action) ||\n /search|query/.test(attrs(visible[0])));\n if (looksLikeSearch) continue;\n // A password field means this is a login, not an enquiry. Interacting with\n // one is both useless and rude.\n if (visible.some((el) => typed(el) === \"password\")) continue;\n\n const submit = form.querySelector(\n 'button[type=\"submit\"], input[type=\"submit\"], button:not([type]), button[type=\"button\"]',\n );\n if (!submit) continue;\n\n let score = visible.length;\n if (hasEmail) score += 4;\n if (hasMessage) score += 3;\n if (score > bestScore) {\n bestScore = score;\n best = { form, hasEmail, fields: visible.length, action };\n }\n }\n if (!best) return null;\n best.form.setAttribute(\"data-audit-form\", \"\");\n // TARGET REMOVED, and this is a safety measure rather than a tidy-up.\n // A form posting to target=\"popupwindow\" — which is how Mailchimp's embed\n // ships, and how clearleft.com's only form is written — submits into a NEW\n // page, and a route registered on this one does not see it. Measured: the\n // request went out and the server received it. Without the target the\n // submission is an ordinary main-frame navigation, which the interceptor\n // already stops. The page is abandoned straight after the probe, so nothing\n // downstream sees the altered form.\n best.form.removeAttribute(\"target\");\n return { fields: best.fields, hasEmail: best.hasEmail, action: best.action };\n}`;\n\n/** Fills every visible control except the email, which gets the junk value.\n * Returns nothing: what matters is the form's own opinion afterwards. */\nconst FILL_FORM = `(args) => {\n const form = document.querySelector(\"[data-audit-form]\");\n if (!form) return false;\n const controls = Array.from(form.querySelectorAll(\"input, textarea, select\"));\n const fire = (el) => {\n el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n el.dispatchEvent(new Event(\"change\", { bubbles: true }));\n };\n for (const el of controls) {\n const type = (el.getAttribute(\"type\") || el.tagName.toLowerCase()).toLowerCase();\n if (type === \"hidden\" || type === \"submit\" || type === \"button\" || type === \"reset\") continue;\n const attrs = [el.getAttribute(\"name\"), el.getAttribute(\"id\"), el.getAttribute(\"autocomplete\"),\n el.getAttribute(\"placeholder\")].filter(Boolean).join(\" \").toLowerCase();\n const isEmail = type === \"email\" || /\\\\bemail\\\\b|e-mail/.test(attrs);\n if (el.tagName === \"SELECT\") {\n if (el.options.length > 1) { el.selectedIndex = 1; fire(el); }\n continue;\n }\n if (type === \"checkbox\" || type === \"radio\") {\n if (!el.checked) { el.checked = true; fire(el); }\n continue;\n }\n el.value = isEmail ? args.invalidEmail : (args.filler[type] || args.filler.text);\n fire(el);\n }\n return true;\n}`;\n\n/** The form's own verdict plus anything on the page that reads as an error.\n * Both, because native constraint validation and a JS form library are two\n * different mechanisms and a site may use either. */\nconst READ_STATE = `(args) => {\n const form = document.querySelector(\"[data-audit-form]\");\n if (!form) return null;\n const nativeInvalid = typeof form.checkValidity === \"function\" ? !form.checkValidity() : false;\n const flagged = form.querySelectorAll('[aria-invalid=\"true\"], .error, .is-invalid, .invalid').length;\n const alerts = Array.from(form.querySelectorAll('[role=\"alert\"], [aria-live]'))\n .map((el) => (el.textContent || \"\").trim())\n .filter(Boolean)\n .join(\" \");\n const text = (form.textContent || \"\").replace(/\\\\s+/g, \" \").trim();\n return {\n nativeInvalid,\n flagged,\n hasErrorText: new RegExp(args.words, \"i\").test(alerts + \" \" + text),\n };\n}`;\n\nexport type InteractionDeps = {\n /** Everything this needs from a Playwright page, named so a test can supply\n * it without a browser. */\n evaluate: <T>(fn: string, arg?: unknown) => Promise<T>;\n /** Clicks the form's submit control. Resolves even when the click does\n * nothing, which is one of the outcomes we are here to detect. */\n clickSubmit: () => Promise<void>;\n /** Arms the interceptor and returns a handle that reports what it stopped,\n * can re-fetch the page past its own guard, and puts everything back. */\n intercept: () => Promise<{\n /** Requests stopped that looked like THIS form's data leaving. Drives the\n * \"did it submit?\" verdict. */\n blocked: () => number;\n /** Everything stopped, including beacons caught by the wider block rule.\n * The receipt, not the measurement. */\n stopped: () => number;\n /** A clean copy of the page. False when the form did not come back. */\n reload: () => Promise<boolean>;\n release: () => Promise<void>;\n }>;\n /** Lets a client-side validator paint before we read the DOM. */\n settle: (ms: number) => Promise<void>;\n url: string;\n};\n\n/** How long a JS validator gets to render its message before we look. Long\n * enough for a framework re-render, short enough that it is not a timeout. */\nexport const VALIDATION_SETTLE_MS = 600;\n\n/** Nothing in this module may hang the crawl. A probe that stalls costs the\n * whole audit — twenty pages already fetched, thrown away — where an\n * abandoned one costs two checks that honestly read \"not measured\". */\nexport const PROBE_BUDGET_MS = 45_000;\n\n/** Resolves to `null` rather than waiting forever. Used on every step that\n * talks to a browser we do not control. */\nexport function withTimeout<T>(work: Promise<T>, ms: number): Promise<T | null> {\n let timer: ReturnType<typeof setTimeout>;\n return Promise.race([\n work.catch(() => null),\n new Promise<null>((resolve) => {\n timer = setTimeout(() => resolve(null), ms);\n }),\n ]).finally(() => clearTimeout(timer));\n}\n\nexport async function probeForms(deps: InteractionDeps): Promise<FormProbe | null> {\n const chosen = await deps.evaluate<Chosen | null>(CHOOSE_FORM);\n if (!chosen) return null;\n\n const guard = await deps.intercept();\n const probe: FormProbe = {\n url: deps.url,\n emptyRefused: undefined,\n emptyHow: null,\n invalidEmailRefused: chosen.hasEmail ? undefined : null,\n invalidEmailHow: null,\n blocked: 0,\n };\n\n try {\n // ---- Empty ------------------------------------------------------------\n // The browser's own answer first: a form with a `required` field on it\n // reports itself invalid before anything is clicked, and that IS the\n // refusal — the click would only make the browser say so out loud.\n const before = await deps.evaluate<{\n nativeInvalid: boolean;\n flagged: number;\n hasErrorText: boolean;\n } | null>(READ_STATE, { words: ERROR_WORDS.source });\n\n if (before?.nativeInvalid) {\n probe.emptyRefused = true;\n probe.emptyHow = \"the browser blocks it — the form marks its fields required\";\n } else {\n const beforeBlocked = guard.blocked();\n await deps.clickSubmit();\n await deps.settle(VALIDATION_SETTLE_MS);\n const after = await deps.evaluate<{\n nativeInvalid: boolean;\n flagged: number;\n hasErrorText: boolean;\n } | null>(READ_STATE, { words: ERROR_WORDS.source });\n const attempted = guard.blocked() > beforeBlocked;\n const complained =\n after !== null &&\n (after.hasErrorText || after.flagged > (before?.flagged ?? 0) || after.nativeInvalid);\n\n if (complained) {\n // CHECKED BEFORE `attempted`, deliberately. A form that paints \"please\n // fill this in\" has not sent anything; if some same-origin beacon fired\n // on the same click, reading that as a submission would call a good\n // form broken. The false accusation is the expensive mistake here — a\n // missed detection is only a silent pass.\n probe.emptyRefused = true;\n probe.emptyHow = \"the form showed an error rather than submitting\";\n } else if (attempted) {\n // Nothing said, and something tried to leave: an empty enquiry was on\n // its way to somebody's inbox.\n probe.emptyRefused = false;\n // Says we stopped it, unprompted. A reader who sees \"your form\n // submitted empty\" should not have to wonder whether we put a blank\n // enquiry in their inbox to find that out.\n probe.emptyHow =\n \"the form submitted with every field empty — we stopped the request before it left the browser\";\n } else if (after === null) {\n // The form left the page without a request — we cannot say what\n // happened, so we do not.\n probe.emptyRefused = undefined;\n } else {\n // Nothing sent and nothing said. The button is inert, which is the\n // same experience as a form that swallows the message.\n probe.emptyRefused = false;\n probe.emptyHow = \"the button did nothing at all — no error, and nothing sent\";\n }\n }\n\n // ---- An address that is not an address ---------------------------------\n if (chosen.hasEmail) {\n // A CLEAN COPY OF THE PAGE FIRST, always. Two reasons, and each one on\n // its own is enough.\n //\n // A form that accepted the empty submit destroyed itself doing so: the\n // navigation we aborted leaves Chromium on its own error page, and there\n // is no form left to fill.\n //\n // And a form that painted \"please enter a valid email\" a moment ago still\n // has that text on screen. Reading it after the second click would credit\n // the form with catching something it never saw — a false pass on exactly\n // the sites most likely to be broken.\n const back = await guard.reload();\n const filled = back\n ? await deps.evaluate<boolean>(FILL_FORM, {\n invalidEmail: INVALID_EMAIL,\n filler: FILLER,\n })\n : false;\n if (!filled) {\n probe.invalidEmailRefused = undefined;\n } else {\n const state = await deps.evaluate<{\n nativeInvalid: boolean;\n flagged: number;\n hasErrorText: boolean;\n } | null>(READ_STATE, { words: ERROR_WORDS.source });\n if (state?.nativeInvalid) {\n probe.invalidEmailRefused = true;\n probe.invalidEmailHow = \"the browser catches it — the field is typed as an email\";\n } else {\n const beforeBlocked = guard.blocked();\n await deps.clickSubmit();\n await deps.settle(VALIDATION_SETTLE_MS);\n const after = await deps.evaluate<{\n nativeInvalid: boolean;\n flagged: number;\n hasErrorText: boolean;\n } | null>(READ_STATE, { words: ERROR_WORDS.source });\n const attempted = guard.blocked() > beforeBlocked;\n // Same ordering as above, and for the same reason.\n if (after && (after.hasErrorText || after.nativeInvalid || after.flagged > 0)) {\n probe.invalidEmailRefused = true;\n probe.invalidEmailHow = \"the form showed an error rather than submitting\";\n } else if (attempted) {\n probe.invalidEmailRefused = false;\n probe.invalidEmailHow = `the form accepted “${INVALID_EMAIL}” as an email address — we stopped the request before it left the browser`;\n } else {\n probe.invalidEmailRefused = undefined;\n }\n }\n }\n }\n } catch {\n // A click that throws tells us nothing about their form. Everything stays\n // at whatever it had reached, and undefined reads as \"not measured\".\n } finally {\n // The RECEIPT is everything we stopped, not the subset we attributed to the\n // form: a reader asking \"did you put anything on my server?\" is owed the\n // whole number.\n probe.blocked = guard.stopped();\n await guard.release();\n }\n\n return probe;\n}\n\n/**\n * The real page, wired to the deps above.\n *\n * `intercept` is the whole safety argument, so it is worth being precise about\n * what it stops. A form submission is either a navigation (a plain `<form>`) or\n * an XHR/fetch to the site's own origin (every JS form library). Both are\n * aborted. Third-party GETs — fonts, images, an analytics beacon — are let\n * through, because blocking them changes how the page behaves without making\n * anyone safer.\n *\n * What is counted as an attempt is narrower still: only navigations and\n * same-origin non-GETs. An analytics POST to a third party fires on all sorts\n * of clicks and is not this form trying to send a message, and counting one\n * would report a working form as broken.\n *\n * A dialog handler goes on for the duration too — a `confirm()` in a submit\n * handler blocks the page forever otherwise, and that would cost the whole\n * crawl, not just this check.\n */\nexport function pageInteractionDeps(\n page: import(\"@playwright/test\").Page,\n url: string,\n): InteractionDeps {\n return {\n url,\n settle: (ms) => page.waitForTimeout(ms),\n // Playwright evaluates a STRING as an expression and does not call it —\n // `page.evaluate(\"() => 1 + 1\")` yields a function object, which serialises\n // to undefined, so every snippet here silently returned nothing. The call\n // has to be written out, with the argument inlined as JSON (every argument\n // in this file is our own constant, never anything off the page).\n evaluate: <T>(fn: string, arg?: unknown) =>\n page.evaluate(`(${fn})(${JSON.stringify(arg ?? null)})`) as Promise<T>,\n async clickSubmit() {\n const submit = page\n .locator(\n '[data-audit-form] button[type=\"submit\"], [data-audit-form] input[type=\"submit\"], [data-audit-form] button:not([type]), [data-audit-form] button[type=\"button\"]',\n )\n .first();\n // `force`, because a submit button under a sticky footer or a cookie bar\n // is not a broken form and we are not here to test our own aim. `noWaitAfter`\n // because the navigation we might provoke is one we intend to abort.\n await submit.click({ timeout: 5_000, force: true, noWaitAfter: true });\n },\n async intercept() {\n // TWO COUNTERS, and they must not be one.\n //\n // `stopped` is everything the interceptor aborted. `submissions` is the\n // subset that looked like this form's data leaving, and it is the one the\n // verdict reads — `attempted` in `probeForms` is \"did the submission go\n // out?\", not \"did any request go out?\".\n //\n // They separated when the blocking rule widened to every non-GET. It had\n // to: a cross-origin POST is how HubSpot, Formspree and Netlify Forms all\n // work, and the old rule stopped only SAME-ORIGIN non-GETs, so a real\n // enquiry — filler text, junk address and all — reached a third-party\n // server in testing. But most analytics beacons are also cross-origin\n // POSTs, and counting one of those as a submission would accuse a form\n // that never sent anything. Block generously; count carefully.\n let stopped = 0;\n let submissions = 0;\n // Open only while `reload` below is running, and nothing clicks during\n // that window — the sole navigation it lets through is our own GET back\n // to a page we already fetched.\n let allowNavigation = false;\n // Disarmed by `release`, BEFORE it tries to unroute. If the unroute does\n // not complete — and it sometimes does not — the handler stays registered\n // for the rest of the crawl, and an armed one aborts every navigation\n // that follows. That would have silently emptied the remaining pages of a\n // twenty-page crawl, which is far worse than the hang it replaced.\n let armed = true;\n let origin = \"\";\n try {\n origin = new URL(url).origin;\n } catch {\n origin = \"\";\n }\n const onDialog = (d: import(\"@playwright/test\").Dialog) => void d.dismiss().catch(() => {});\n page.on(\"dialog\", onDialog);\n // THE CONTEXT, not the page.\n //\n // `page.route` covers this page and its frames. It does NOT cover a popup,\n // which is a new page — so a form with a `target` submitted for real, and\n // the receiving server confirmed it. Removing the target in `CHOOSE_FORM`\n // handles the markup case; this handles the rest, including a submit\n // handler that calls `window.open` itself.\n //\n // Safe because the crawl runs one page in one context and `pacedEach` is\n // serial, so nothing else is loading while this is armed. See `crawl.ts`.\n const router = page.context();\n // SYNCHRONOUS, and the route resolved fire-and-forget.\n //\n // This is the deadlock, and it took a live crawl to find. An `async`\n // handler returns a promise, and `unroute`/`unrouteAll` WAIT for every\n // such promise still in flight — so the second probe against a given page\n // hung in `release`, and the third hung in `route` itself, each taking a\n // twenty-page crawl down with it. No unit test could see it: the fakes\n // have no router.\n //\n // Returning nothing leaves Playwright nothing to await, and the abort or\n // continue still lands a tick later. The decision is made synchronously\n // so `blocked` is accurate the instant the request is seen.\n await router.route(\"**/*\", (route) => {\n if (!armed) {\n void route.continue().catch(() => {});\n return;\n }\n let stop: boolean;\n let looksLikeSubmission = false;\n try {\n const req = route.request();\n // MAIN FRAME ONLY. `isNavigationRequest` is also true for an iframe\n // loading its own document, and on a contact page that is usually a\n // captcha. Aborting Cloudflare Turnstile's challenge both breaks the\n // widget on the page we are trying to observe and counts as a stopped\n // request — which the verdict reads as \"the form submitted\". A\n // third-party iframe appearing in the second after a click would have\n // accused a perfectly good form of sending an empty enquiry, on\n // exactly the page most likely to have one.\n //\n // A form submitting INTO an iframe is still caught, by the\n // same-origin non-GET rule below.\n // THE FRAME READ HAS ITS OWN GUARD, and its failure is a verdict.\n //\n // `request.frame()` THROWS for a popup's opening navigation — \"the\n // request was issued before the frame is created\", because that\n // request is what creates it. An iframe is different and measurably\n // so: a `<iframe src>` added to the page resolves to a real frame.\n // So a navigation whose frame is unavailable is a new window opening,\n // and during a submit probe that is the submission leaving.\n //\n // This mattered. The outer catch below used to swallow the throw and\n // fall through to `stop = false`, so a form posting into a popup went\n // out for real — measured, with the receiving server confirming it.\n const isNav = req.isNavigationRequest();\n let frame: import(\"@playwright/test\").Frame | null = null;\n try {\n frame = req.frame();\n } catch {\n frame = null;\n }\n const isPopupNav = isNav && frame === null;\n const isNavigation = isNav && frame === page.mainFrame();\n const method = req.method().toUpperCase();\n const sameOrigin = (() => {\n try {\n return new URL(req.url()).origin === origin;\n } catch {\n return false;\n }\n })();\n // BLOCK: every non-GET, wherever it is going. Third-party GETs —\n // fonts, images, a beacon — still pass, which is all the old comment\n // here meant to allow; the old rule also let third-party POSTs\n // through, and that is the shape of nearly every hosted form\n // back end. Our own reload is the one navigation allowed.\n stop = isPopupNav || (isNavigation && !allowNavigation) || method !== \"GET\";\n\n // COUNT: only what looks like this form's data leaving. A navigation\n // or a new window is unambiguous. A non-GET counts when it is\n // same-origin, when it is encoded as a form post, or when its body\n // carries a value we typed — that last one catches a JSON payload to\n // a hosted endpoint, which is otherwise indistinguishable from a\n // beacon. An empty submission to a JSON endpoint is missed, and that\n // is the right way round: a missed detection is a silent pass, a\n // false one calls a working form broken.\n const contentType = (req.headers()[\"content-type\"] ?? \"\").toLowerCase();\n const formEncoded =\n contentType.includes(\"application/x-www-form-urlencoded\") ||\n contentType.includes(\"multipart/form-data\");\n const carriesOurs = (() => {\n try {\n const body = req.postData() ?? \"\";\n if (body === \"\") return false;\n return SENTINELS.some(\n (v) => body.includes(v) || body.includes(encodeURIComponent(v)),\n );\n } catch {\n return false;\n }\n })();\n looksLikeSubmission =\n isPopupNav ||\n (isNavigation && !allowNavigation) ||\n (method !== \"GET\" && (sameOrigin || formEncoded || carriesOurs));\n } catch {\n // Reaching here means the URL or the method could not be read, which\n // has never been observed. It is NOT the \"frame unavailable\" case —\n // that is handled above and stops the request, because a navigation\n // we cannot attribute to a frame is a window opening.\n //\n // This default used to be reached by the frame throw, and letting\n // those through delivered a real submission to a real server.\n stop = false;\n }\n if (stop) stopped++;\n if (stop && looksLikeSubmission) submissions++;\n void (stop ? route.abort() : route.continue()).catch(() => {});\n });\n\n return {\n blocked: () => submissions,\n stopped: () => stopped,\n reload: async () => {\n allowNavigation = true;\n try {\n await page.goto(url, { waitUntil: \"load\", timeout: 20_000 });\n } catch {\n return false;\n } finally {\n allowNavigation = false;\n }\n const again = await page.evaluate(`(${CHOOSE_FORM})(null)`).catch(() => null);\n return again !== null;\n },\n release: async () => {\n armed = false;\n page.off(\"dialog\", onDialog);\n // THE MARKER FIRST, while the page is still definitely answering.\n // Then the routes.\n // The marker is ours; leaving it behind would show up in the next\n // page's extract as markup the site does not have. Passed as a\n // STRING like every other snippet here: `src/prospect/types.ts` is a\n // build entry, and an inline arrow touching `document` puts the DOM\n // lib on the declaration build, which does not have one.\n await withTimeout(page.evaluate(`(${REMOVE_MARKER})()`), 5_000);\n\n // `unrouteAll`, NOT `unroute`, and with `ignoreErrors`.\n //\n // This is the deadlock. `page.unroute` waits for route handlers that\n // are still in flight, and the reload above leaves several — so the\n // SECOND probe on a given page object hung here forever, taking a\n // twenty-page crawl down with it. It reproduced two runs in four and\n // never in a unit test, because the fakes have no router at all.\n // Short, because it is now only tidying: the handler above is\n // already inert, so a timeout here costs nothing but a registration\n // that passes everything through.\n await withTimeout(router.unrouteAll({ behavior: \"ignoreErrors\" }), 3_000);\n },\n };\n },\n };\n}\n","import { parse, HTMLElement, NodeType } from \"node-html-parser\";\nimport type { FormField, FormShape, PageAnchor, PageExtract } from \"./types.js\";\n\n/** See `PageExtract.anchors`: the extract is persisted once per page per audit,\n * and a navigation-heavy page can carry several hundred anchors. Generous\n * enough that no ordinary page reaches it; `anchorCount` always reports the\n * true total so a capped list is never mistaken for a complete one. */\nexport const MAX_ANCHORS = 300;\n\n/** Same discipline as `MAX_ANCHORS`, for `PageExtract.scriptSrcs`. A tag-manager\n * page can inject a hundred of these; `scriptCount` reports the true total. */\nexport const MAX_SCRIPTS = 120;\n\n/** Same discipline again, for `PageExtract.inlineScriptUrls`. A tag manager\n * bootstrap names a handful of URLs; nothing legitimate names forty. */\nexport const MAX_INLINE_URLS = 40;\n\n/**\n * URLs appearing in a string of JavaScript. Blunt on purpose: this answers\n * \"does this script mention googletagmanager.com/gtag/js\", not \"what will it\n * load\", and the distinction is carried in the field's name.\n *\n * The WHOLE url, not just the host, because every signature in `stack.ts` keys\n * on a path — `googletagmanager.com/gtag/js` distinguishes GA4 from Tag\n * Manager, and a bare hostname matches neither. A deferred loader usually\n * builds its src by concatenation, so the literal ends at the quote; capturing\n * up to there still yields the path that identifies the tool.\n */\n/** URLs that identify a vocabulary rather than something to fetch. An SVG or\n * XHTML namespace appears in any inline script that builds markup, and nothing\n * ever requests it — counting it as a URL this page references makes every\n * templating site look like it reaches out to w3.org. */\nconst NAMESPACE_URLS = /^https?:\\/\\/(www\\.)?w3\\.org\\//i;\n\nfunction urlsIn(code: string): string[] {\n const found = new Set<string>();\n for (const m of code.matchAll(/https?:\\/\\/[^\\s\"'`\\\\)<>]{4,200}/gi)) {\n if (NAMESPACE_URLS.test(m[0])) continue;\n found.add(m[0]);\n if (found.size >= MAX_INLINE_URLS) break;\n }\n return [...found];\n}\n\n/** Input types that are not a field a visitor fills in. `hidden` carries CSRF\n * tokens and form ids; the button types are the control, not the question.\n *\n * `password` is deliberately NOT in this set. It IS a field a visitor fills\n * in, and `fieldCount` is documented as \"visible, named controls\", so hiding\n * it here would make that number lie. What a password means for the form's\n * KIND is handled in `formShape` instead, where it disqualifies the form\n * outright rather than merely going uncounted. */\nconst NON_FIELD_INPUTS = new Set([\"hidden\", \"submit\", \"button\", \"image\", \"reset\"]);\n\n/** Names, types and labels that mean \"we can contact you back\". A form with\n * none of these is a search box or a filter, and calling that a conversion\n * path would pass a site that cannot be reached by anyone. */\nconst CONTACT_FIELD = /\\b(e-?mail|phone|tel|mobile|contact)\\b/i;\n\n/**\n * Break a field's attributes into words the way a human reads them.\n *\n * `CONTACT_FIELD` is anchored on `\\b`, and neither of the two conventions that\n * dominate real form markup puts a word boundary where one is needed:\n *\n * snake_case `user_phone` — `_` is a WORD character, so `\\bphone\\b` misses\n * camelCase `yourPhone` — `r` to `P` is not a boundary either\n *\n * Left unhandled, both read a working enquiry form as \"other\", which reports a\n * site with a perfectly good contact form as having no way to reach anyone —\n * a false alarm in the direction that costs a prospect's trust in the whole\n * audit. Caught by a test, not by review.\n */\nconst toWords = (s: string): string =>\n s.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\").replace(/[_\\-.]+/g, \" \");\n\n/** Subtrees a browser never renders. Skipped WHOLE — including their headings,\n * images and schema blocks, which a <template> stamp would otherwise donate to\n * the page's real counts. */\nexport const UNRENDERED_TAGS = new Set([\"STYLE\", \"NOSCRIPT\", \"TEMPLATE\", \"SVG\"]);\n\n/** Elements that force a break in rendered text. Inline elements deliberately do\n * NOT: `<b>Acme</b>Corp` is one word on screen and must stay one word here,\n * because the raw-vs-rendered word diff is what the audit's headline number is\n * made of, and an invented word break biases it in only one direction. */\nconst BLOCK = new Set([\n \"ADDRESS\",\n \"ARTICLE\",\n \"ASIDE\",\n \"BLOCKQUOTE\",\n \"BR\",\n \"DD\",\n \"DIV\",\n \"DL\",\n \"DT\",\n \"FIELDSET\",\n \"FIGCAPTION\",\n \"FIGURE\",\n \"FOOTER\",\n \"FORM\",\n \"H1\",\n \"H2\",\n \"H3\",\n \"H4\",\n \"H5\",\n \"H6\",\n \"HEADER\",\n \"HR\",\n \"LI\",\n \"MAIN\",\n \"NAV\",\n \"OL\",\n \"P\",\n \"PRE\",\n \"SECTION\",\n \"TABLE\",\n \"TD\",\n \"TH\",\n \"TR\",\n \"UL\",\n]);\n\nconst collapse = (s: string): string => s.replace(/\\s+/g, \" \").trim();\n\n/** Word/Google-Docs paste soup and broken page-builder plugins nest ordinary\n * formatting spans far past anything hand-written markup would reach — a\n * plain recursive walk throws `RangeError: Maximum call stack size exceeded`\n * around 5,000 levels, which would take the whole audit down with it.\n * Mirrors checks.ts's `MAX_SCHEMA_DEPTH` precedent: generous enough that no\n * real page is anywhere near it, so only pathological nesting is affected —\n * the branch simply stops descending and the extract is honestly partial. */\nconst MAX_WALK_DEPTH = 100;\n\n/** Rendered text of one element: text nodes concatenated with NO inserted\n * separator, a newline at each block boundary, whitespace collapsed last —\n * which is what a browser shows. TITLE and SCRIPT are dropped wherever they\n * appear, since a <title> misplaced in <body> is still invisible. */\n/**\n * A heading's ACCESSIBLE name, not only its text nodes.\n *\n * `<h1><img alt=\"Acme Roofing\"></h1>` — a logo wrapped in the page headline —\n * is one of the most common homepage patterns there is, and reading text nodes\n * alone yields \"\" for it. An empty heading is dropped, so the page arrived at\n * the checks carrying no h1 at all and we reported a site with a headline as a\n * site without one.\n *\n * That number was invisible in the stored corpus, because the extract only ever\n * held the headings that survived: 115 pages recorded as having no h1, and no\n * way to tell how many of them really had one. A gap we cannot even count is\n * one to close rather than argue about.\n *\n * `aria-label` on the heading wins outright, exactly as it does for a screen\n * reader; otherwise the alt text of the images inside it fills in for text\n * nodes that are not there.\n */\nfunction headingTextOf(el: HTMLElement): string {\n const label = (el.getAttribute(\"aria-label\") ?? \"\").trim();\n if (label) return collapse(label);\n const text = textOf(el);\n if (text) return text;\n const alts = el\n .querySelectorAll(\"img\")\n .map((img) => (img.getAttribute(\"alt\") ?? \"\").trim())\n .filter((a) => a.length > 0);\n return collapse(alts.join(\" \"));\n}\n\nfunction textOf(el: HTMLElement): string {\n const parts: string[] = [];\n const walk = (node: HTMLElement, depth: number): void => {\n if (depth > MAX_WALK_DEPTH) return;\n for (const child of node.childNodes) {\n if (child.nodeType === NodeType.TEXT_NODE) {\n parts.push(child.text);\n continue;\n }\n if (child.nodeType !== NodeType.ELEMENT_NODE) continue;\n const e = child as HTMLElement;\n const tag = e.tagName;\n if (UNRENDERED_TAGS.has(tag) || tag === \"SCRIPT\" || tag === \"TITLE\") continue;\n const block = BLOCK.has(tag);\n if (block) parts.push(\"\\n\");\n walk(e, depth + 1);\n if (block) parts.push(\"\\n\");\n }\n };\n walk(el, 0);\n return collapse(parts.join(\"\"));\n}\n\ntype Collected = {\n metas: HTMLElement[];\n /** `<link>` elements — canonical and friends. NOT anchors; see `anchors`. */\n links: HTMLElement[];\n jsonLd: string[];\n images: HTMLElement[];\n headings: { level: number; text: string }[];\n title: string | null;\n /** `<a href>` elements, in document order. */\n anchors: HTMLElement[];\n forms: HTMLElement[];\n /** `src` of each `<script src>`, in document order. Inline scripts are not\n * collected — see `PageExtract.scriptSrcs`. */\n scriptSrcs: string[];\n inlineScriptUrls: string[];\n};\n\n/** One ordered pass for the element-level signals. Document order matters: the\n * heading sequence drives a later level-skip check. Depth-limited for the\n * same reason as `textOf`'s walk — see `MAX_WALK_DEPTH`. */\nfunction collect(el: HTMLElement, out: Collected, depth = 0): void {\n if (depth > MAX_WALK_DEPTH) return;\n for (const child of el.childNodes) {\n if (child.nodeType !== NodeType.ELEMENT_NODE) continue;\n const e = child as HTMLElement;\n const tag = e.tagName;\n if (UNRENDERED_TAGS.has(tag)) continue;\n switch (tag) {\n case \"META\":\n out.metas.push(e);\n break;\n case \"LINK\":\n out.links.push(e);\n break;\n case \"IMG\":\n out.images.push(e);\n break;\n case \"A\":\n // Only anchors that actually go somewhere. `<a>` without href is a\n // named target or a styling hook, and counting it as a link would\n // inflate every navigation measure below.\n if ((e.getAttribute(\"href\") ?? \"\").trim()) out.anchors.push(e);\n break;\n case \"FORM\":\n out.forms.push(e);\n break;\n case \"TITLE\":\n if (out.title === null) out.title = collapse(e.text) || null;\n break;\n case \"SCRIPT\": {\n const isJsonLd =\n (e.getAttribute(\"type\") ?? \"\").toLowerCase().trim() === \"application/ld+json\";\n if (isJsonLd) out.jsonLd.push(e.text);\n const src = (e.getAttribute(\"src\") ?? \"\").trim();\n if (src) {\n // Collected even for ld+json, which may legitimately be loaded from\n // a file rather than written inline.\n out.scriptSrcs.push(src);\n } else if (isJsonLd) {\n // Its BODY is data, not code. The URLs in there are `@id`s, `sameAs`\n // links and vocabulary references, and none of them is evidence that\n // this page loads anything. Letting them through gave apple.com 293\n // \"inline script URLs\" — schema.org, wikidata.org, its own support\n // site — a meaningless number that also silently satisfied a\n // downstream test for \"does this page reference anything external\".\n } else {\n // Hosts NAMED inside an inline script, which is a different claim\n // from a script that is loaded — and the one that matters for any\n // site which defers its tags.\n //\n // reddoorla.com is the case that found this: it injects gtag.js only\n // after the first pointer or scroll, for privacy, so at crawl time\n // there is no analytics `src` anywhere in the DOM. Reading that as\n // \"this site has no analytics\" is our missing measurement printed as\n // their defect, and it would land on every consent-gated site there\n // is — which is a growing share of the careful ones.\n for (const url of urlsIn(e.text)) out.inlineScriptUrls.push(url);\n }\n // Raw-text element — nothing inside to walk.\n continue;\n }\n case \"H1\":\n case \"H2\":\n case \"H3\":\n case \"H4\":\n case \"H5\":\n case \"H6\": {\n const text = headingTextOf(e);\n if (text) out.headings.push({ level: Number(tag.slice(1)), text });\n break;\n }\n }\n collect(e, out, depth + 1);\n }\n}\n\n/**\n * Path segments that name a form doing something other than starting a\n * conversation with the business.\n *\n * A login, an account signup, a checkout and a WordPress comment box all ask\n * for an email address and at least one more field — which was every signal\n * the shape test had, so all four read as enquiry forms. journey.ts counts\n * ONLY `enquiry`, precisely so that \"a site nobody can actually reach\" does\n * not score as having a conversion path, and a patient-portal login sitting in\n * a site header handed exactly that to every page of such a site.\n *\n * Matched on whole path SEGMENTS, never as substrings. `/signup-for-a-consultation`\n * is a real lead form; excluding it would report a reachable business as\n * unreachable, which is the false alarm `toWords` above exists to prevent.\n */\nconst NON_ENQUIRY_SEGMENTS = new Set([\n // Account doors\n \"login\",\n \"log-in\",\n \"log_in\",\n \"logon\",\n \"signin\",\n \"sign-in\",\n \"sign_in\",\n \"signup\",\n \"sign-up\",\n \"sign_up\",\n \"register\",\n \"registration\",\n // Commerce\n \"checkout\",\n \"cart\",\n \"basket\",\n // Retrieval, not contact\n \"search\",\n // WordPress, whose comment endpoint is a fixed path on a great many sites\n \"wp-login\",\n \"wp-signup\",\n \"wp-register\",\n \"wp-comments-post\",\n]);\n\n/** Does this `action` post somewhere that is not a way to reach a person?\n * Resolved against a placeholder base so a relative action, a rooted path and\n * a full URL are all read the same way; the base itself is never used. A\n * trailing extension is dropped so `/wp-comments-post.php` reads as its\n * segment. A form with no action posts to its own URL, which says nothing\n * either way — and saying nothing is not evidence against the site. */\nfunction isNonEnquiryAction(action: string | null): boolean {\n if (!action) return false;\n let path: string;\n try {\n path = new URL(action, \"https://form.invalid/\").pathname;\n } catch {\n path = action;\n }\n return path\n .toLowerCase()\n .split(\"/\")\n .some((segment) => NON_ENQUIRY_SEGMENTS.has(segment.replace(/\\.[a-z0-9]+$/, \"\")));\n}\n\n/** One `<form>`'s shape. Exported for its own tests: telling a contact form\n * from a search box is the judgement the conversion check rests on, and it is\n * worth being able to exercise it directly. */\nexport function formShape(form: HTMLElement): FormShape {\n const controls = form.querySelectorAll(\"input, textarea, select\");\n let fieldCount = 0;\n /** Fields that are a way to reply, so the rest can be counted separately —\n * see the enquiry bar below. */\n let contactFieldCount = 0;\n let hasContactField = false;\n let hasPassword = false;\n let hasTextarea = false;\n let hasSubmit = form.querySelectorAll(\"button\").length > 0;\n\n // The three attributes that decide whether a field is easy to fill in on a\n // phone, collected here rather than re-walked later: the mobile keyboard\n // switches on `type`, one-tap fill needs `autocomplete`, and `required` is\n // the only machine-readable way a form says a field is compulsory.\n const fields: FormField[] = [];\n\n for (const control of controls) {\n const type = (control.getAttribute(\"type\") ?? \"\").toLowerCase().trim();\n if (control.tagName === \"INPUT\" && NON_FIELD_INPUTS.has(type)) {\n if (type === \"submit\" || type === \"image\") hasSubmit = true;\n continue;\n }\n if (control.tagName === \"TEXTAREA\") hasTextarea = true;\n if (control.tagName === \"INPUT\" && type === \"password\") hasPassword = true;\n fieldCount += 1;\n fields.push({\n // A bare <input> defaults to text, exactly as a browser treats it, so an\n // author who wrote nothing reads the same as one who wrote type=\"text\".\n type: control.tagName === \"INPUT\" ? type || \"text\" : control.tagName.toLowerCase(),\n name: (control.getAttribute(\"name\") ?? \"\").toLowerCase().trim() || null,\n autocomplete: (control.getAttribute(\"autocomplete\") ?? \"\").toLowerCase().trim() || null,\n required: control.hasAttribute(\"required\"),\n });\n // Any of the attributes an author might carry the meaning in. Checked\n // together rather than in priority order: a field is a contact field if\n // ANY of them says so, and sites disagree about which one to use.\n const signature = toWords(\n [\n type,\n control.getAttribute(\"name\") ?? \"\",\n control.getAttribute(\"id\") ?? \"\",\n control.getAttribute(\"placeholder\") ?? \"\",\n control.getAttribute(\"autocomplete\") ?? \"\",\n control.getAttribute(\"aria-label\") ?? \"\",\n ].join(\" \"),\n );\n if (type === \"email\" || type === \"tel\" || CONTACT_FIELD.test(signature)) {\n hasContactField = true;\n contactFieldCount += 1;\n }\n }\n\n const action = form.getAttribute(\"action\")?.trim() || null;\n // An account door, not a conversation. A password field says so outright; the\n // path says so for the portal logins that ask for a member number and a PIN.\n const isAccountForm = hasPassword || isNonEnquiryAction(action);\n // A message box, or enough questions beyond the contact details to be an\n // intake form. Without this, \"first name + email + Subscribe\" — the second\n // commonest newsletter box on the web — cleared the bar on `fieldCount >= 2`\n // alone.\n //\n // The threshold is TWO non-contact questions, not three. Three was tried and\n // it failed a shape that is everywhere: name / email / phone / subject with\n // no message box, the lean quote form. Reading that as a newsletter signup\n // costs the site a real conversion path and can make a page with a working\n // enquiry form read as having none — which is the same class of error, in\n // the same direction, as the ones this pass exists to remove. A newsletter\n // box asking two questions beyond an email address does not exist in the\n // wild; a quote form that does is ordinary.\n const asksSomethingBack = hasTextarea || fieldCount - contactFieldCount >= 2;\n\n return {\n // A lone contact field is a newsletter box, not an enquiry form. See\n // `FormKind`: on one audited site a footer email box put every page at zero\n // clicks from \"reaching them\", when the only form that reaches a person\n // was the nine-field one on its contact page.\n kind:\n isAccountForm || !hasContactField\n ? \"other\"\n : fieldCount >= 2 && asksSomethingBack\n ? \"enquiry\"\n : \"subscribe\",\n action,\n method: (form.getAttribute(\"method\") ?? \"get\").toLowerCase().trim() || \"get\",\n fieldCount,\n hasContactField,\n hasSubmit,\n fields,\n };\n}\n\n/** Parse one HTML document into the signals every downstream check reads.\n * Pure — the same input always yields the same extract. */\nexport function extractPage(html: string): PageExtract {\n const root = parse(html);\n // node-html-parser surfaces `<!doctype html>` as a TEXT node that is a SIBLING\n // of <html>, not a doctype node, so the walk starts at <html> when there is one.\n const documentEl = root.querySelector(\"html\") ?? root;\n const out: Collected = {\n metas: [],\n links: [],\n jsonLd: [],\n images: [],\n headings: [],\n title: null,\n anchors: [],\n forms: [],\n scriptSrcs: [],\n inlineScriptUrls: [],\n };\n collect(documentEl, out);\n\n const social: Record<string, string> = {};\n const metas: Record<string, string> = {};\n let metaDescription: string | null = null;\n let hasViewportMeta = false;\n for (const m of out.metas) {\n // `charset` is written as a bare attribute (`<meta charset=\"utf-8\">`), not\n // as name/content, so it never had a key here and the charset check could\n // not see it. Normalising it to `charset` gives it the same shape as every\n // other meta; a `<meta http-equiv=\"content-type\">` declaration lands there\n // too, since both are a page declaring its encoding.\n const charset = (m.getAttribute(\"charset\") ?? \"\").trim();\n if (charset) {\n metas[\"charset\"] = charset;\n continue;\n }\n const key = (m.getAttribute(\"property\") ?? m.getAttribute(\"name\") ?? \"\").toLowerCase().trim();\n const content = (m.getAttribute(\"content\") ?? \"\").trim();\n if (!key) {\n const equiv = (m.getAttribute(\"http-equiv\") ?? \"\").toLowerCase().trim();\n if (equiv === \"content-type\" && /charset=/i.test(content)) {\n metas[\"charset\"] = content.replace(/^.*charset=/i, \"\").trim();\n }\n continue;\n }\n if (key === \"description\") metaDescription = content || null;\n else if (key === \"viewport\") hasViewportMeta = content.length > 0;\n if (key.startsWith(\"og:\") || key.startsWith(\"twitter:\")) social[key] = content;\n // Everything else, including description and viewport: `social` owns the\n // two prefixes and `metas` owns the rest, so nothing is stored twice.\n else metas[key] = content;\n }\n\n const canonicalEl = out.links.find(\n (l) => (l.getAttribute(\"rel\") ?? \"\").toLowerCase().trim() === \"canonical\",\n );\n\n return {\n title: out.title,\n metaDescription,\n canonical: canonicalEl?.getAttribute(\"href\")?.trim() || null,\n social,\n headings: out.headings,\n jsonLd: out.jsonLd,\n images: {\n total: out.images.length,\n withAlt: out.images.filter((i) => (i.getAttribute(\"alt\") ?? \"\").trim().length > 0).length,\n },\n hasViewportMeta,\n // Body-scoped: <head> has no visible text, and scoping here rather than\n // filtering keeps the rule obvious.\n text: textOf(root.querySelector(\"body\") ?? documentEl),\n anchors: out.anchors.slice(0, MAX_ANCHORS).map((a): PageAnchor => ({\n href: (a.getAttribute(\"href\") ?? \"\").trim(),\n // The visible label, not the raw text: an anchor wrapping an icon and a\n // span should read as its span. `textOf` already drops the unrendered\n // subtrees an icon sprite lives in.\n text: textOf(a).slice(0, 120),\n rel: (a.getAttribute(\"rel\") ?? \"\").toLowerCase().trim(),\n target: (a.getAttribute(\"target\") ?? \"\").toLowerCase().trim(),\n // NOT lower-cased and NOT trimmed to a keyword: this is prose, and the\n // check that reads it needs to know whether a human wrote a destination\n // into it. apple.com's “Learn more” links all carry one — `aria-label=\"Learn\n // more about accessibility\"` — so judging their link text without it\n // called 60 well-labelled links bare.\n ariaLabel: (a.getAttribute(\"aria-label\") ?? \"\").trim().slice(0, 120),\n })),\n // The TRUE total, so a capped list is never mistaken for a complete one.\n anchorCount: out.anchors.length,\n imageSrcs: out.images\n .map((i) => (i.getAttribute(\"src\") ?? \"\").trim())\n .filter((src) => src.length > 0),\n forms: out.forms.map(formShape),\n metas,\n // A projection of elements `collect` was already gathering — until now only\n // the canonical one was ever read off them.\n links: out.links.map((l) => ({\n rel: (l.getAttribute(\"rel\") ?? \"\").toLowerCase().trim(),\n href: (l.getAttribute(\"href\") ?? \"\").trim(),\n hreflang: (l.getAttribute(\"hreflang\") ?? \"\").trim(),\n type: (l.getAttribute(\"type\") ?? \"\").toLowerCase().trim(),\n })),\n scriptSrcs: out.scriptSrcs.slice(0, MAX_SCRIPTS),\n inlineScriptUrls: [...new Set(out.inlineScriptUrls)].slice(0, MAX_INLINE_URLS),\n // The TRUE total, for the same reason `anchorCount` exists: a capped list\n // must never be mistaken for a complete one.\n scriptCount: out.scriptSrcs.length,\n };\n}\n","import { usablePages } from \"./pages.js\";\nimport type { CrawlResult } from \"./types.js\";\n\n/**\n * What they are running, named back to them.\n *\n * This is not a check and it must never try to be. Nothing here passes or\n * fails, nothing here enters the denominator, and no line of it belongs in the\n * fix list. It exists to answer the reader's first silent question — do these\n * people know what they are talking about — before a single finding is made.\n *\n * \"You're on WordPress running the Astra theme with Elementor, your forms are\n * Gravity Forms, your DNS is at GoDaddy and your mail is Google Workspace\" is a\n * sentence nobody sending a cold audit can write, and it costs us almost\n * nothing because every receipt for it is already in the crawl.\n *\n * The discipline is the same as everywhere else in this codebase, and it is the\n * whole reason `evidence` is non-optional on `StackItem`: every line is an\n * OBSERVATION WITH A RECEIPT — the exact URL or header we read it off — never\n * an inference we cannot show. A reader who disputes a line can go and look at\n * the thing we looked at.\n *\n * The failure this module must not commit is the mirror of the one the checks\n * must not commit. There, our missing measurement must not become their defect.\n * Here, our missing measurement must not become \"they don't have one\": a\n * WordPress site behind a caching plugin that rewrites asset paths is\n * completely invisible to this, and so is any site whose markup we never read.\n * `measured` says whether we were in a position to see anything at all, and the\n * report must be able to say \"we could not tell\" rather than implying absence.\n */\n\nexport type StackLayer =\n | \"cms\"\n | \"theme\"\n | \"plugin\"\n | \"page-builder\"\n | \"framework\"\n | \"hosting\"\n | \"ecommerce\"\n | \"forms\"\n | \"analytics\"\n | \"fonts\";\n\n/** Display order in the readout. Broadest thing first — a reader wants \"you're\n * on WordPress\" before \"you load Hotjar\". */\nexport const LAYER_ORDER: StackLayer[] = [\n \"cms\",\n \"theme\",\n \"page-builder\",\n \"framework\",\n \"ecommerce\",\n \"plugin\",\n \"forms\",\n \"analytics\",\n \"fonts\",\n \"hosting\",\n];\n\nexport const LAYER_LABELS: Record<StackLayer, string> = {\n cms: \"Platform\",\n theme: \"Theme\",\n \"page-builder\": \"Page builder\",\n framework: \"Framework\",\n ecommerce: \"Store\",\n plugin: \"Plugins\",\n forms: \"Forms\",\n analytics: \"Analytics and tracking\",\n fonts: \"Fonts\",\n hosting: \"Hosting and CDN\",\n};\n\nexport type StackItem = {\n layer: StackLayer;\n /** What we are naming, spelled the way its own vendor spells it. */\n name: string;\n /**\n * The receipt — the URL or response header we read it off, truncated only for\n * length. Never optional and never synthesised: a line without a receipt is\n * an assertion, and this module does not make assertions.\n */\n evidence: string;\n};\n\nexport type StackReadout = {\n /**\n * Were we in a position to see anything?\n *\n * False when no page we read carried a `scriptSrcs` array — which is every\n * report stored before that field existed, and any run whose pages all failed\n * to fetch. An empty `items` with `measured: false` means \"we could not\n * tell\"; with `measured: true` it means \"we read the markup and recognised\n * nothing in it\", which is a real and unremarkable answer for a hand-built\n * site.\n */\n measured: boolean;\n items: StackItem[];\n /** How many pages' markup we actually read. The honest denominator for any\n * sentence that starts \"we did not see\". */\n pagesExamined: number;\n /** Did we also read response headers? They carry the hosting and CDN half,\n * and a run without them can say nothing about either. */\n headersExamined: boolean;\n};\n\n/** Matched against every asset URL a page references — script srcs and image\n * srcs today, stylesheet hrefs once the `<link>` set is projected. */\ntype AssetSignature = { layer: StackLayer; name: string; match: RegExp };\n\n/**\n * Deliberately narrow patterns. A signature that fires on a substring of an\n * ordinary URL puts a wrong name in front of the client on page one of the\n * report, which costs more than every correct line on the page earns — the same\n * reasoning that made `BOOKING_HOSTS` match on hostname rather than href after\n * \"cal.com\" credited a booking system to medical.com.\n */\nconst ASSET_SIGNATURES: AssetSignature[] = [\n // Platforms, by the paths only they serve.\n { layer: \"cms\", name: \"WordPress\", match: /\\/wp-(?:content|includes)\\// },\n { layer: \"cms\", name: \"Squarespace\", match: /(?:^|\\/\\/|\\.)squarespace\\.com\\// },\n { layer: \"cms\", name: \"Wix\", match: /(?:^|\\/\\/|\\.)wixstatic\\.com\\// },\n { layer: \"cms\", name: \"Webflow\", match: /(?:^|\\/\\/|\\.)website-files\\.com\\// },\n { layer: \"cms\", name: \"HubSpot CMS\", match: /(?:^|\\/\\/|\\.)(?:hs-scripts|hs-banner)\\.com\\// },\n { layer: \"cms\", name: \"Duda\", match: /(?:^|\\/\\/|\\.)cdn-website\\.com\\// },\n { layer: \"cms\", name: \"GoDaddy Website Builder\", match: /(?:^|\\/\\/|\\.)wsimg\\.com\\// },\n { layer: \"cms\", name: \"Drupal\", match: /\\/(?:core|modules)\\/misc\\/drupal\\.js/ },\n { layer: \"cms\", name: \"Ghost\", match: /\\/assets\\/built\\/[a-z-]+\\.js/ },\n\n // Frameworks, by their build output paths.\n { layer: \"framework\", name: \"Next.js\", match: /\\/_next\\/static\\// },\n { layer: \"framework\", name: \"Nuxt\", match: /\\/_nuxt\\// },\n { layer: \"framework\", name: \"SvelteKit\", match: /\\/_app\\/immutable\\// },\n { layer: \"framework\", name: \"Gatsby\", match: /\\/page-data\\/|webpack-runtime-[a-f0-9]+\\.js/ },\n { layer: \"framework\", name: \"Angular\", match: /\\/(?:polyfills|runtime)[.-][a-z0-9]+\\.js/ },\n\n // Page builders. All WordPress plugins, but a reader thinks of them as the\n // thing they edit in, so they get their own layer rather than being buried\n // among thirty plugin names.\n { layer: \"page-builder\", name: \"Elementor\", match: /\\/wp-content\\/plugins\\/elementor/ },\n { layer: \"page-builder\", name: \"Divi\", match: /\\/wp-content\\/themes\\/[Dd]ivi\\// },\n { layer: \"page-builder\", name: \"WPBakery\", match: /\\/wp-content\\/plugins\\/js_composer\\// },\n { layer: \"page-builder\", name: \"Beaver Builder\", match: /\\/wp-content\\/plugins\\/beaver-builder/ },\n { layer: \"page-builder\", name: \"Oxygen\", match: /\\/wp-content\\/plugins\\/oxygen\\// },\n\n // Stores.\n { layer: \"ecommerce\", name: \"WooCommerce\", match: /\\/wp-content\\/plugins\\/woocommerce\\// },\n { layer: \"ecommerce\", name: \"Shopify\", match: /(?:^|\\/\\/|\\.)shopify\\.com\\// },\n { layer: \"ecommerce\", name: \"BigCommerce\", match: /(?:^|\\/\\/|\\.)bigcommerce\\.com\\// },\n\n // Forms. Which one matters commercially: it is the thing an enquiry actually\n // travels through, and half of these have a free tier that silently drops mail.\n { layer: \"forms\", name: \"Gravity Forms\", match: /\\/plugins\\/gravityforms\\// },\n { layer: \"forms\", name: \"Contact Form 7\", match: /\\/plugins\\/contact-form-7\\// },\n { layer: \"forms\", name: \"WPForms\", match: /\\/plugins\\/wpforms/ },\n { layer: \"forms\", name: \"Ninja Forms\", match: /\\/plugins\\/ninja-forms\\// },\n { layer: \"forms\", name: \"Typeform\", match: /(?:^|\\/\\/|\\.)typeform\\.com\\// },\n { layer: \"forms\", name: \"Jotform\", match: /(?:^|\\/\\/|\\.)jotform\\.com\\// },\n { layer: \"forms\", name: \"HubSpot Forms\", match: /(?:^|\\/\\/|\\.)hsforms\\.net\\// },\n { layer: \"forms\", name: \"Formspree\", match: /(?:^|\\/\\/|\\.)formspree\\.io\\// },\n\n // Analytics and tracking. Absence of ALL of these is its own check (T0-46);\n // here we are only naming what is there.\n { layer: \"analytics\", name: \"Google Tag Manager\", match: /googletagmanager\\.com\\/gtm\\.js/ },\n { layer: \"analytics\", name: \"Google Analytics 4\", match: /googletagmanager\\.com\\/gtag\\/js/ },\n {\n layer: \"analytics\",\n name: \"Google Analytics (legacy)\",\n match: /google-analytics\\.com\\/(?:analytics|ga)\\.js/,\n },\n { layer: \"analytics\", name: \"Meta Pixel\", match: /connect\\.facebook\\.net\\// },\n { layer: \"analytics\", name: \"LinkedIn Insight\", match: /snap\\.licdn\\.com\\// },\n { layer: \"analytics\", name: \"Hotjar\", match: /(?:static|script)\\.hotjar\\.com\\// },\n { layer: \"analytics\", name: \"Microsoft Clarity\", match: /(?:^|\\/\\/|\\.)clarity\\.ms\\// },\n { layer: \"analytics\", name: \"Intercom\", match: /(?:widget|js)\\.intercom(?:cdn)?\\.(?:io|com)\\// },\n { layer: \"analytics\", name: \"Drift\", match: /js\\.driftt\\.com\\// },\n { layer: \"analytics\", name: \"Plausible\", match: /plausible\\.io\\/js\\// },\n { layer: \"analytics\", name: \"Fathom\", match: /cdn\\.usefathom\\.com\\// },\n { layer: \"analytics\", name: \"Segment\", match: /cdn\\.segment\\.(?:com|io)\\// },\n\n // Headless CMSes, by the asset and API hosts they serve from. A modern\n // agency-built site is far likelier to be one of these than to be WordPress,\n // and naming only \"SvelteKit and Netlify\" back to such a client says we did\n // not look very hard.\n { layer: \"cms\", name: \"Prismic\", match: /(?:^|\\/\\/|\\.)prismic\\.io\\// },\n { layer: \"cms\", name: \"Contentful\", match: /(?:^|\\/\\/|\\.)(?:ctfassets\\.net|contentful\\.com)\\// },\n { layer: \"cms\", name: \"Sanity\", match: /(?:^|\\/\\/|\\.)sanity\\.io\\// },\n { layer: \"cms\", name: \"Storyblok\", match: /(?:^|\\/\\/|\\.)storyblok\\.com\\// },\n { layer: \"cms\", name: \"DatoCMS\", match: /(?:^|\\/\\/|\\.)datocms-assets\\.com\\// },\n { layer: \"cms\", name: \"Strapi\", match: /(?:^|\\/\\/|\\.)strapi\\.io\\// },\n { layer: \"cms\", name: \"Craft CMS\", match: /\\/cpresources\\// },\n\n // Fonts. Typekit is a script; Google Fonts arrives as a stylesheet link, so\n // it needs the `<link>` set — which is fed in above.\n { layer: \"fonts\", name: \"Adobe Fonts (Typekit)\", match: /use\\.typekit\\.net\\// },\n { layer: \"fonts\", name: \"Google Fonts\", match: /fonts\\.(?:googleapis|gstatic)\\.com\\// },\n];\n\n/**\n * `<meta name=\"generator\">`, which several platforms fill in honestly and which\n * is the only signal for a few of them.\n *\n * Matched case-insensitively against the whole content string, and the receipt\n * printed is the content itself — so when it says \"WordPress 6.4.2\" the reader\n * sees the version we saw rather than a name we chose.\n */\nconst GENERATOR_SIGNATURES: { layer: StackLayer; name: string; match: RegExp }[] = [\n { layer: \"cms\", name: \"WordPress\", match: /wordpress/i },\n { layer: \"cms\", name: \"Drupal\", match: /drupal/i },\n { layer: \"cms\", name: \"Joomla\", match: /joomla/i },\n { layer: \"cms\", name: \"Webflow\", match: /webflow/i },\n { layer: \"cms\", name: \"Squarespace\", match: /squarespace/i },\n { layer: \"cms\", name: \"Wix\", match: /wix\\.com/i },\n { layer: \"cms\", name: \"Ghost\", match: /ghost/i },\n { layer: \"cms\", name: \"Shopify\", match: /shopify/i },\n { layer: \"page-builder\", name: \"Elementor\", match: /elementor/i },\n { layer: \"framework\", name: \"Hugo\", match: /hugo/i },\n { layer: \"framework\", name: \"Jekyll\", match: /jekyll/i },\n { layer: \"framework\", name: \"Gatsby\", match: /gatsby/i },\n { layer: \"framework\", name: \"Next.js\", match: /next\\.js/i },\n];\n\n/**\n * Response headers that name a host or CDN.\n *\n * Presence of the header is the signal for most of these — `cf-ray` exists only\n * because Cloudflare put it there — so the receipt is the header name and, when\n * it is short enough to be meaningful rather than an opaque id, its value.\n */\nconst HEADER_SIGNATURES: { name: string; header: string; value?: RegExp }[] = [\n { name: \"Cloudflare\", header: \"cf-ray\" },\n { name: \"Netlify\", header: \"x-nf-request-id\" },\n { name: \"Vercel\", header: \"x-vercel-id\" },\n { name: \"AWS CloudFront\", header: \"x-amz-cf-id\" },\n { name: \"Fastly\", header: \"x-served-by\" },\n { name: \"GitHub Pages\", header: \"x-github-request-id\" },\n { name: \"Pantheon\", header: \"x-pantheon-styx-hostname\" },\n { name: \"WP Engine\", header: \"x-wpe-backend\" },\n { name: \"Kinsta\", header: \"x-kinsta-cache\" },\n { name: \"LiteSpeed\", header: \"x-litespeed-cache\" },\n { name: \"Wix\", header: \"x-wix-request-id\" },\n { name: \"Shopify\", header: \"x-shopid\" },\n { name: \"Squarespace\", header: \"x-contextid\" },\n { name: \"Nginx\", header: \"server\", value: /nginx/i },\n { name: \"Apache\", header: \"server\", value: /apache/i },\n { name: \"Microsoft IIS\", header: \"server\", value: /microsoft-iis/i },\n];\n\n/** Long query strings and cache-busting hashes make a receipt unreadable\n * without making it more convincing. */\nconst MAX_EVIDENCE = 120;\n\nfunction receipt(url: string): string {\n return url.length <= MAX_EVIDENCE ? url : `${url.slice(0, MAX_EVIDENCE - 1)}…`;\n}\n\n/**\n * A WordPress theme or plugin slug, from the path that names it.\n *\n * The slug is the directory, so `/wp-content/plugins/gravityforms/js/form.js`\n * gives `gravityforms`. Returned as authored rather than prettified: we are\n * reporting what is on their server, and `wp-rocket` is what they will search\n * for when they go looking.\n */\nfunction slugsFrom(urls: string[], kind: \"themes\" | \"plugins\"): Map<string, string> {\n const re = new RegExp(`/wp-content/${kind}/([^/?#]+)/`);\n const found = new Map<string, string>();\n for (const url of urls) {\n const m = re.exec(url);\n const slug = m?.[1];\n // The first URL that named it is the receipt; later ones say nothing new.\n if (slug && !found.has(slug)) found.set(slug, receipt(url));\n }\n return found;\n}\n\nexport function readStack(crawl: CrawlResult): StackReadout {\n const set = usablePages(crawl.pages);\n const headers = crawl.homeHeaders ?? {};\n const headersExamined = Object.keys(headers).length > 0;\n\n // `scriptSrcs` is optional on PageExtract, and its absence means \"not\n // measured\" — every report stored before the field existed lacks it. Reading\n // it as an empty array would turn those into sites that load no scripts and\n // therefore run nothing we recognise, which is a confident wrong answer.\n const readable = set.pages.filter((p) => p.extract.scriptSrcs !== undefined);\n const measured = readable.length > 0;\n\n const items: StackItem[] = [];\n const seen = new Set<string>();\n const add = (layer: StackLayer, name: string, evidence: string): void => {\n const key = `${layer}::${name}`;\n if (seen.has(key)) return;\n seen.add(key);\n items.push({ layer, name, evidence });\n };\n\n const assetUrls: string[] = [];\n for (const { extract } of readable) {\n // Four sources, and each closes a hole the others leave.\n //\n // URLs named inside inline scripts, because a deferred loader is still a\n // tool this site runs — naming only what was fetched at crawl time leaves\n // the readout silent about every consent-gated tag.\n //\n // And `<link>` hrefs, which is where a stylesheet-delivered font lives.\n // That was noted here as our gap back when the link set was not projected;\n // it is projected now, so the gap closes.\n assetUrls.push(\n ...(extract.scriptSrcs ?? []),\n ...(extract.imageSrcs ?? []),\n ...(extract.inlineScriptUrls ?? []),\n ...(extract.links ?? []).map((l) => l.href),\n );\n }\n\n // Generators BEFORE assets, because the first receipt for a name is the one\n // that gets kept and these are the better receipt twice over: the generator\n // is the site declaring itself rather than us inferring from a file path, and\n // it usually carries the version — \"WordPress 6.4.2\" tells the reader\n // something \"/wp-content/themes/astra/js/frontend.js\" does not. Run the other\n // way round the version was silently discarded on every WordPress site.\n for (const { extract } of readable) {\n const generator = extract.metas?.[\"generator\"];\n if (!generator) continue;\n for (const sig of GENERATOR_SIGNATURES) {\n if (sig.match.test(generator)) {\n add(sig.layer, sig.name, `<meta name=\"generator\"> ${receipt(generator)}`);\n }\n }\n }\n\n for (const sig of ASSET_SIGNATURES) {\n const hit = assetUrls.find((url) => sig.match.test(url));\n if (hit) add(sig.layer, sig.name, receipt(hit));\n }\n\n for (const [slug, evidence] of slugsFrom(assetUrls, \"themes\")) add(\"theme\", slug, evidence);\n for (const [slug, evidence] of slugsFrom(assetUrls, \"plugins\")) add(\"plugin\", slug, evidence);\n\n for (const sig of HEADER_SIGNATURES) {\n const value = headers[sig.header];\n if (value === undefined) continue;\n if (sig.value && !sig.value.test(value)) continue;\n // An opaque request id proves the header was there and says nothing else,\n // so the receipt is the header name; a `server:` value is itself the\n // finding and is worth printing.\n add(\n \"hosting\",\n sig.name,\n sig.value ? `${sig.header}: ${receipt(value)}` : `${sig.header} header`,\n );\n }\n\n return {\n measured,\n items: items.sort(\n (a, b) =>\n LAYER_ORDER.indexOf(a.layer) - LAYER_ORDER.indexOf(b.layer) || a.name.localeCompare(b.name),\n ),\n pagesExamined: readable.length,\n headersExamined,\n };\n}\n","import { usablePages } from \"./pages.js\";\nimport type { PageCapture, PageExtract } from \"./types.js\";\n\n/**\n * Can a visitor actually get from where they landed to a way of contacting you?\n *\n * Everything else in this audit measures whether a site can be found and read.\n * This measures whether it can be ACTED ON, which is the part that decides\n * whether traffic becomes a phone call.\n *\n * The measurement that matters is click distance, and it matters because of\n * where visitors actually land. A search engine — and an answer engine citing a\n * page — sends people to a deep page, not the homepage. A site whose only\n * contact form sits behind the homepage nav is asking a stranger who arrived on\n * a blog post to go looking. Some do. Most leave.\n *\n * So this builds a link graph across the pages the crawl retrieved, finds every\n * way of making contact on each one, and walks outward to find how far the\n * nearest one is. A page with no path at all is a dead end, and a dead end is a\n * defect with a price attached.\n *\n * The honest limit, which every consumer must carry into what it prints: the\n * crawl retrieves a handful of pages, not the whole site. \"Dead end\" here means\n * \"no path among the pages we looked at\" — real, worth reporting, and not the\n * same claim as \"no path exists\". `pagesExamined` is reported so the sentence\n * can say which one it means.\n */\n\nexport type AffordanceKind = \"form\" | \"tel\" | \"mailto\";\n\nexport type ContactAffordance = {\n kind: AffordanceKind;\n /** The crawled page it was found on. */\n page: string;\n /** The number, the address, or the form's action — the receipt. */\n detail: string;\n};\n\nexport type PageJourney = {\n url: string;\n /** Clicks to the nearest page carrying a contact affordance. 0 means it is on\n * this page; null means no path was found among the pages examined. */\n clicksToContact: number | null;\n /** Links from this page to other pages the crawl retrieved. Zero means a\n * visitor who lands here can go nowhere, which is a different and worse\n * problem than merely being far from the contact page. */\n internalLinks: number;\n};\n\nexport type JourneyMap = {\n affordances: ContactAffordance[];\n pages: PageJourney[];\n /** Pages with no path to any contact affordance, among those examined. */\n deadEnds: string[];\n /** The worst distance among pages that DO have a path — the honest headline,\n * because an average hides the one page that strands people. Null when no\n * page has a path. */\n worstClicksToContact: number | null;\n /** How many pages this was computed over. Reported so no consumer can\n * describe a five-page sample as though it were the whole site. */\n pagesExamined: number;\n /**\n * Did every page examined carry a recorded anchor list?\n *\n * False means the crawl did not record links (reports stored before\n * `PageExtract.anchors` existed), and NOTHING here is a finding: `deadEnds`\n * is empty and `worstClicksToContact` is null, because a page whose links we\n * never captured is a page we cannot say anything about. Reading an absent\n * anchors array as \"no links\" would report our own missing field as every\n * page on the site being a dead end.\n */\n anchorsMeasured: boolean;\n};\n\n/** Trailing slash and case folded on the host, so \"/about\" and \"/about/\" are one\n * node in the graph rather than two that never link to each other. The query\n * and hash are dropped: they address a state of a page, not another page. */\nexport function canonicalizeUrl(raw: string): string | null {\n try {\n const u = new URL(raw);\n if (u.protocol !== \"http:\" && u.protocol !== \"https:\") return null;\n const path = u.pathname.replace(/\\/+$/, \"\") || \"/\";\n return `${u.hostname.replace(/^www\\./i, \"\").toLowerCase()}${path}`;\n } catch {\n return null;\n }\n}\n\n/** Absolute URL for an anchor's href, or null when it does not navigate to a\n * page — `tel:`, `mailto:`, `javascript:`, and bare fragments all resolve to\n * \"nowhere else\", and treating them as edges would make every page look\n * better connected than it is. */\nexport function resolveNavigable(href: string, pageUrl: string): string | null {\n const trimmed = href.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) return null;\n if (/^(tel:|mailto:|javascript:|sms:|data:)/i.test(trimmed)) return null;\n try {\n return new URL(trimmed, pageUrl).toString();\n } catch {\n return null;\n }\n}\n\nconst TEL_HREF = /^tel:(.+)$/i;\nconst MAILTO_HREF = /^mailto:([^?]+)/i;\n\n/** Every way of making contact on one page. */\nexport function affordancesOn(page: PageCapture, view?: PageExtract): ContactAffordance[] {\n // The view is passed in by `buildJourney` so every page in one journey is\n // read the same way; on its own the function falls back to what a visitor\n // sees, since a nav injected by JavaScript is a real path for a human.\n const extract = view ?? page.rendered ?? page.raw;\n if (!extract) return [];\n const found: ContactAffordance[] = [];\n\n for (const anchor of extract.anchors ?? []) {\n const tel = TEL_HREF.exec(anchor.href);\n if (tel?.[1]) {\n found.push({ kind: \"tel\", page: page.url, detail: tel[1].trim() });\n continue;\n }\n const mail = MAILTO_HREF.exec(anchor.href);\n if (mail?.[1]) {\n found.push({ kind: \"mailto\", page: page.url, detail: mail[1].trim() });\n }\n }\n\n for (const form of extract.forms ?? []) {\n // Only an enquiry form. A search box, a filter and a lone newsletter email\n // box are all forms too, and counting any of them would report a site\n // nobody can actually reach as having a conversion path — the exact\n // failure this check exists to catch. See `FormKind` for why the\n // one-field case had to be split out: on one audited site a footer email\n // box otherwise put every page at zero clicks from reaching a person.\n if (form.kind !== \"enquiry\") continue;\n found.push({ kind: \"form\", page: page.url, detail: form.action ?? page.url });\n }\n\n return found;\n}\n\nexport function buildJourney(pages: PageCapture[]): JourneyMap {\n // What counts as a page of this site, all read from one view — see pages.ts.\n // A URL the server answered with a 404 is not a dead end on their site; it\n // is a link we followed and should not have.\n const usable = usablePages(pages);\n\n const nodes = new Map<string, { page: PageCapture; extract: PageExtract }>();\n for (const entry of usable.pages) {\n const key = canonicalizeUrl(entry.page.url);\n if (key) nodes.set(key, entry);\n }\n\n const affordances: ContactAffordance[] = [];\n // Keys of pages that carry a way to make contact — the BFS targets.\n const hasContact = new Set<string>();\n for (const [key, entry] of nodes) {\n const found = affordancesOn(entry.page, entry.extract);\n affordances.push(...found);\n if (found.length > 0) hasContact.add(key);\n }\n\n // Forward edges, and the reverse graph the search actually runs on.\n //\n // The search runs BACKWARDS from every contact page at once, rather than\n // forwards from each page separately: one traversal then labels every page\n // with its true distance, instead of one traversal per page. Same answer,\n // and it cannot drift between pages the way N separate searches can.\n const outgoing = new Map<string, Set<string>>();\n const incoming = new Map<string, Set<string>>();\n for (const [key, entry] of nodes) {\n const out = new Set<string>();\n for (const anchor of entry.extract.anchors ?? []) {\n const abs = resolveNavigable(anchor.href, entry.page.url);\n if (!abs) continue;\n const target = canonicalizeUrl(abs);\n if (!target || target === key || !nodes.has(target)) continue;\n out.add(target);\n if (!incoming.has(target)) incoming.set(target, new Set());\n incoming.get(target)?.add(key);\n }\n outgoing.set(key, out);\n }\n\n const distance = new Map<string, number>();\n let frontier = [...hasContact];\n for (const key of frontier) distance.set(key, 0);\n let depth = 0;\n while (frontier.length > 0) {\n depth += 1;\n const next: string[] = [];\n for (const key of frontier) {\n for (const source of incoming.get(key) ?? []) {\n if (distance.has(source)) continue;\n distance.set(source, depth);\n next.push(source);\n }\n }\n frontier = next;\n }\n\n const journeys: PageJourney[] = [...nodes.entries()].map(([key, entry]) => ({\n url: entry.page.url,\n clicksToContact: usable.anchorsMeasured ? (distance.get(key) ?? null) : null,\n internalLinks: outgoing.get(key)?.size ?? 0,\n }));\n\n const reachable = journeys.map((j) => j.clicksToContact).filter((d): d is number => d !== null);\n\n return {\n affordances,\n pages: journeys,\n // Without recorded anchors there is no evidence either way, so there is no\n // finding — not \"every page is a dead end\", which is what reading the\n // absent array as an empty one used to produce.\n deadEnds: usable.anchorsMeasured\n ? journeys.filter((j) => j.clicksToContact === null).map((j) => j.url)\n : [],\n worstClicksToContact: reachable.length > 0 ? Math.max(...reachable) : null,\n pagesExamined: journeys.length,\n anchorsMeasured: usable.anchorsMeasured,\n };\n}\n","import { isPrivateOrLoopbackHost } from \"../util/url.js\";\nimport { pacedEach, sleep as defaultSleep } from \"./crawl.js\";\nimport { canonicalizeUrl, resolveNavigable } from \"./journey.js\";\nimport type { CrawlResult, PageCapture, PageExtract } from \"./types.js\";\n\n/**\n * The questions you can only answer by asking the server.\n *\n * Everything in Tier 0 and Tier 1 reads bytes we already had. This module makes\n * NEW requests to a stranger's website, which changes the ethics and the\n * budget: every probe is capped, paced through `pacedEach`, and counted, and\n * the count is reported. A prospect who never asked for this audit should not\n * be able to notice it in their logs.\n *\n * WHAT MAKES A CHECK BELONG HERE. Only questions where the declared answer and\n * the served answer can differ. `<link rel=\"icon\">` exists in the markup — Tier\n * 1 already knows that — but whether the URL it points at returns an image is a\n * different claim, and it is the one a visitor experiences. The same split\n * governs the og:image, the logo, and the sitemap: a sitemap advertising four\n * hundred URLs is worth nothing if they 404.\n *\n * THE RULE THAT SHAPES EVERY VERDICT HERE. A request that fails is ours, not\n * theirs. `assets.ts` learned this the expensive way and its `classifyProbe`\n * encodes it: only 404 and 410 prove a URL is not there, and 401/403/429/5xx\n * are things WE could not verify. Every field below is three-state for that\n * reason — `undefined` means we did not get an answer, and no check may turn\n * that into a finding about the site.\n *\n * SSRF: every URL probed here is either derived from the origin or came out of\n * the prospect's markup, so `isPrivateOrLoopbackHost` guards each one exactly\n * as it does in `assets.ts` and the crawler.\n */\n\nexport type HttpResponse = {\n /** HTTP status after redirects, or null when the request never answered.\n * Those are different claims and only the first is about their site. */\n status: number | null;\n headers: Record<string, string>;\n /** URL the request settled on. Null when it never got there. */\n finalUrl: string | null;\n /** Redirect hops taken. Null when the client could not report them. */\n hops: number | null;\n /** First bytes, present only when the caller asked for a range. */\n body: Uint8Array | null;\n error: string | null;\n};\n\nexport type HttpProbeDeps = {\n request: (\n url: string,\n opts: { method: \"GET\" | \"HEAD\"; rangeBytes?: number },\n ) => Promise<HttpResponse>;\n delayMs: number;\n sleep?: (ms: number) => Promise<void>;\n};\n\n/** A URL that answered, that provably did not, or that we could not read.\n * The third is not a failure and never enters a denominator. */\nexport type ProbeVerdict = \"ok\" | \"broken\" | \"unverified\";\n\nexport type UrlProbe = { url: string; status: number | null; verdict: ProbeVerdict };\n\nexport type HttpFindings = {\n /** False when we had no origin to probe — nothing below is then about them. */\n measured: boolean;\n /** Requests this stage made, reported so the cost is visible. */\n requests: number;\n\n /** The icon a browser tab actually gets. `url` is what the page declared, or\n * the conventional `/favicon.ico` when it declared nothing. */\n favicon: { url: string; declared: boolean; verdict: ProbeVerdict } | undefined;\n\n /** What `http://` does. `hops` counts redirects; `https` is where it landed. */\n httpUpgrade: { hops: number | null; https: boolean } | undefined;\n\n /** Two homepage samples. Equal statuses mean nothing to report; different\n * ones are the only evidence that would justify the word \"intermittent\". */\n homeSamples: [number | null, number | null] | undefined;\n\n /** A path fetched with and without its trailing slash. `settled` is true when\n * the two agree on one address — either by redirect or by canonical. */\n trailingSlash: { path: string; settled: boolean; detail: string } | undefined;\n\n /** `/index.html`, and whether it duplicates the homepage rather than\n * redirecting or declaring the homepage canonical. Null when it 404s, which\n * is the correct behaviour and not a finding. */\n indexAlias: { duplicate: boolean; detail: string } | null | undefined;\n\n /** The same path in a different case. Null when it 404s — also correct. */\n caseAlias: { path: string; duplicate: boolean; detail: string } | null | undefined;\n\n /** Sampled sitemap URLs. Null when there is no sitemap to sample. */\n sitemapUrls: { checked: number; total: number; broken: UrlProbe[] } | null | undefined;\n\n /** Outbound links to other sites. Null when the site links out nowhere. */\n externalLinks: { checked: number; total: number; broken: UrlProbe[] } | null | undefined;\n\n /** The image that appears when the site is shared. Null when none is\n * declared — Tier 1 owns that, and saying it twice reads as two problems. */\n ogImage:\n | { url: string; verdict: ProbeVerdict; width: number | null; height: number | null }\n | null\n | undefined;\n\n /** The header logo, and the rule we used to find it, so a reader can tell\n * whether we found the right image. */\n logo: { url: string; how: string; verdict: ProbeVerdict } | null | undefined;\n\n /** Internal links reached through more than one redirect. */\n redirectChains: { checked: number; chained: { url: string; hops: number }[] } | undefined;\n};\n\n/** Ceilings. Each one is a promise to a server we were not invited to. */\nexport const MAX_SITEMAP_PROBES = 12;\nexport const MAX_EXTERNAL_PROBES = 15;\nexport const MAX_REDIRECT_PROBES = 10;\n/**\n * Hops before an internal link is worth mentioning.\n *\n * ONE redirect is ordinary and often deliberate — a trailing slash, a locale\n * prefix, a short URL. TWO is where apple.com sits on every `/us/shop/goto/*`\n * link, which is a deliberate redirector doing its job. Three is where a chain\n * stops looking intentional and starts looking like something nobody has\n * revisited, which is the only version of this a reader can act on.\n */\nexport const MAX_ORDINARY_HOPS = 2;\n/** Bytes pulled when a probe needs to read image dimensions. Every format we\n * can measure carries them in the first few hundred bytes; 32KB is slack for\n * a JPEG with a large EXIF block ahead of its first frame header. */\nexport const IMAGE_HEADER_BYTES = 32_768;\n/** An og:image below this in either direction is cropped to mush by every\n * platform that renders it. Facebook's own floor is 200. */\nexport const MIN_OG_IMAGE_EDGE = 200;\n\nexport function classify(status: number | null): ProbeVerdict {\n if (status === null) return \"unverified\";\n if (status >= 200 && status < 300) return \"ok\";\n // The fetch follows redirects, so a 3xx arriving here is one that never\n // resolved — a visitor following it lands nowhere.\n if (status >= 300 && status < 400) return \"broken\";\n if (status === 404 || status === 410) return \"broken\";\n return \"unverified\";\n}\n\nfunction safe(url: string): string | null {\n try {\n const u = new URL(url);\n if (u.protocol !== \"http:\" && u.protocol !== \"https:\") return null;\n if (isPrivateOrLoopbackHost(u.hostname)) return null;\n return u.toString();\n } catch {\n return null;\n }\n}\n\n/** Two URLs that mean the same page. A trailing slash and a `www.` are not\n * differences a reader cares about, and treating them as ones buries the case\n * that matters — every page declaring the homepage canonical. */\nfunction sameAddress(a: string | null, b: string | null): boolean {\n if (!a || !b) return false;\n const key = (u: string) =>\n canonicalizeUrl(u)\n ?.replace(/^www\\./, \"\")\n .replace(/\\/$/, \"\") ?? u;\n return key(a) === key(b);\n}\n\nfunction extractOf(page: PageCapture): PageExtract | null {\n return page.rendered ?? page.raw;\n}\n\n/** The `<link rel=\"canonical\">` of a page we fetched here rather than crawled.\n * A narrow regex rather than a parse: this reads one attribute out of a\n * document we are not otherwise interested in, and a miss costs us an\n * `unmeasured`, never a wrong finding. */\nfunction canonicalOf(body: Uint8Array | null): string | null {\n if (!body) return null;\n const head = new TextDecoder(\"utf-8\", { fatal: false }).decode(body.slice(0, 65_536));\n const tag = head.match(/<link\\b[^>]*\\brel\\s*=\\s*[\"']?canonical[\"']?[^>]*>/i)?.[0];\n const href = tag?.match(/\\bhref\\s*=\\s*[\"']([^\"']+)[\"']/i)?.[1];\n return href?.trim() || null;\n}\n\n/**\n * Pixel dimensions from the first bytes of an image.\n *\n * PNG, GIF and WebP put them at a fixed offset; JPEG requires walking the\n * segment chain to the first frame header. Anything else — SVG above all, which\n * has no intrinsic size worth the name — returns null, and null is reported as\n * \"we did not measure this\", never as a failure. A vector logo is not a defect.\n */\nexport function imageSize(bytes: Uint8Array | null): { width: number; height: number } | null {\n if (!bytes || bytes.length < 24) return null;\n // `>>> 0`, because `0x89 << 24` is NEGATIVE in JavaScript and the PNG\n // signature test silently never matched without it.\n const be32 = (o: number) =>\n ((bytes[o]! << 24) | (bytes[o + 1]! << 16) | (bytes[o + 2]! << 8) | bytes[o + 3]!) >>> 0;\n const be16 = (o: number) => (bytes[o]! << 8) | bytes[o + 1]!;\n const le16 = (o: number) => bytes[o]! | (bytes[o + 1]! << 8);\n\n // PNG: an 8-byte signature, then an IHDR chunk whose first two fields are the\n // dimensions.\n if (be32(0) === 0x89504e47 && be32(4) === 0x0d0a1a0a) {\n return { width: be32(16), height: be32(20) };\n }\n // GIF87a / GIF89a: little-endian, straight after the six-byte magic.\n if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {\n return { width: le16(6), height: le16(8) };\n }\n // RIFF/WEBP. Three sub-formats, each storing the size differently.\n if (be32(0) === 0x52494646 && be32(8) === 0x57454250 && bytes.length >= 30) {\n const fourcc = String.fromCharCode(bytes[12]!, bytes[13]!, bytes[14]!, bytes[15]!);\n if (fourcc === \"VP8 \") return { width: le16(26) & 0x3fff, height: le16(28) & 0x3fff };\n if (fourcc === \"VP8L\") {\n const b = bytes[21]! | (bytes[22]! << 8) | (bytes[23]! << 16) | (bytes[24]! << 24);\n return { width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1 };\n }\n if (fourcc === \"VP8X\") {\n const dim = (o: number) => 1 + (bytes[o]! | (bytes[o + 1]! << 8) | (bytes[o + 2]! << 16));\n return { width: dim(24), height: dim(27) };\n }\n return null;\n }\n // JPEG: walk the markers to the first SOFn, skipping every other segment by\n // its declared length. Bounded by the buffer we were given.\n if (bytes[0] === 0xff && bytes[1] === 0xd8) {\n let i = 2;\n while (i + 9 < bytes.length) {\n if (bytes[i] !== 0xff) {\n i++;\n continue;\n }\n const marker = bytes[i + 1]!;\n // Standalone markers carry no length; SOS means the entropy-coded scan\n // has begun and there is no header left to find.\n if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {\n i += 2;\n continue;\n }\n if (marker === 0xda) return null;\n const len = be16(i + 2);\n // SOFn, excluding the four that are not frame headers.\n if (\n marker >= 0xc0 &&\n marker <= 0xcf &&\n marker !== 0xc4 &&\n marker !== 0xc8 &&\n marker !== 0xcc\n ) {\n return { width: be16(i + 7), height: be16(i + 5) };\n }\n if (len < 2) return null;\n i += 2 + len;\n }\n }\n return null;\n}\n\nfunction empty(measured: boolean): HttpFindings {\n return {\n measured,\n requests: 0,\n favicon: undefined,\n httpUpgrade: undefined,\n homeSamples: undefined,\n trailingSlash: undefined,\n indexAlias: undefined,\n caseAlias: undefined,\n sitemapUrls: undefined,\n externalLinks: undefined,\n ogImage: undefined,\n logo: undefined,\n redirectChains: undefined,\n };\n}\n\nexport async function probeHttp(crawl: CrawlResult, deps: HttpProbeDeps): Promise<HttpFindings> {\n // `.origin`, not `.toString()`: the latter appends a slash to a bare host, so\n // every `${origin}/index.html` in this file became `//index.html` — a URL\n // that 404s everywhere, which read as \"the alias does not exist\" on every\n // site we would ever audit.\n const origin = (() => {\n const ok = safe(crawl.origin);\n return ok ? new URL(ok).origin : null;\n })();\n if (!origin) return empty(false);\n\n const out = empty(true);\n const sleep = deps.sleep ?? defaultSleep;\n let requests = 0;\n\n /** Every request in this module goes through here: it counts, it paces, and\n * it refuses anything the SSRF guard rejects. */\n const ask = async (\n url: string,\n opts: { method: \"GET\" | \"HEAD\"; rangeBytes?: number } = { method: \"HEAD\" },\n ): Promise<HttpResponse | null> => {\n const target = safe(url);\n if (!target) return null;\n if (requests > 0 && deps.delayMs > 0) await sleep(deps.delayMs);\n requests++;\n try {\n return await deps.request(target, opts);\n } catch (err) {\n return {\n status: null,\n headers: {},\n finalUrl: null,\n hops: null,\n body: null,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n };\n\n const home = crawl.pages[0];\n const homeExtract = home ? extractOf(home) : null;\n\n // ---- The icon a tab actually gets -------------------------------------\n {\n const declared = homeExtract?.links?.find((l) =>\n /(^|\\s)(icon|shortcut icon|apple-touch-icon)(\\s|$)/i.test(l.rel ?? \"\"),\n );\n const href = declared?.href ? resolveNavigable(declared.href, home?.url ?? origin) : null;\n const url = href ?? `${origin}/favicon.ico`;\n const res = await ask(url, { method: \"GET\", rangeBytes: 2048 });\n if (res) {\n const type = res.headers[\"content-type\"] ?? \"\";\n const verdict = classify(res.status);\n out.favicon = {\n url,\n declared: Boolean(href),\n // A 200 that hands back the HTML of a soft-404 page is not an icon.\n // The content type is what separates \"served\" from \"answered\".\n verdict:\n verdict === \"ok\" && /^(image|text\\/xml|application\\/xml)/i.test(type)\n ? \"ok\"\n : verdict === \"ok\"\n ? \"broken\"\n : verdict,\n };\n }\n }\n\n // ---- What http:// does -------------------------------------------------\n {\n const insecure = origin.replace(/^https:/, \"http:\");\n if (insecure !== origin) {\n const res = await ask(insecure, { method: \"HEAD\" });\n if (res && res.status !== null) {\n out.httpUpgrade = { hops: res.hops, https: (res.finalUrl ?? \"\").startsWith(\"https:\") };\n }\n }\n }\n\n // ---- Does the homepage answer the same way twice? ----------------------\n {\n const a = await ask(origin, { method: \"HEAD\" });\n const b = await ask(origin, { method: \"HEAD\" });\n if (a && b) out.homeSamples = [a.status, b.status];\n }\n\n // ---- One address per page ----------------------------------------------\n // A path fetched both ways. Picked from the crawl rather than invented, so\n // we are asking about a page that exists.\n {\n const sample = crawl.pages\n .map((p) => p.url)\n .find((u) => {\n try {\n const path = new URL(u).pathname;\n return path !== \"/\" && !/\\.[a-z0-9]+$/i.test(path);\n } catch {\n return false;\n }\n });\n if (sample) {\n const bare = sample.replace(/\\/+$/, \"\");\n const slashed = `${bare}/`;\n const [a, b] = [\n await ask(bare, { method: \"GET\", rangeBytes: 65_536 }),\n await ask(slashed, { method: \"GET\", rangeBytes: 65_536 }),\n ];\n if (a && b && a.status !== null && b.status !== null) {\n const bothServed = classify(a.status) === \"ok\" && classify(b.status) === \"ok\";\n if (!bothServed) {\n // One of them redirects or 404s, which is exactly the desired\n // behaviour — the site has one address for the page.\n out.trailingSlash = {\n path: new URL(bare).pathname,\n settled: true,\n detail: \"one form redirects to the other\",\n };\n } else if (a.finalUrl !== null && a.finalUrl === b.finalUrl) {\n // Compared exactly, NOT through `sameAddress` — that helper ignores a\n // trailing slash, which is the entire subject of this check. Using it\n // here made every site look settled.\n\n out.trailingSlash = {\n path: new URL(bare).pathname,\n settled: true,\n detail: \"both forms resolve to the same address\",\n };\n } else {\n const ca = canonicalOf(a.body);\n const cb = canonicalOf(b.body);\n const agree = sameAddress(ca, cb);\n out.trailingSlash = {\n path: new URL(bare).pathname,\n settled: agree,\n detail: agree\n ? \"both forms answer, and both declare the same canonical\"\n : \"both forms answer with no canonical to settle which is the real one\",\n };\n }\n }\n }\n }\n\n // ---- /index.html ---------------------------------------------------------\n {\n const alias = `${origin}/index.html`;\n const res = await ask(alias, { method: \"GET\", rangeBytes: 65_536 });\n if (res && res.status !== null) {\n if (classify(res.status) !== \"ok\") {\n // A 404 here is correct behaviour, not a defect.\n out.indexAlias = null;\n } else if (sameAddress(res.finalUrl, origin)) {\n out.indexAlias = { duplicate: false, detail: \"it redirects to the homepage\" };\n } else {\n const canonical = canonicalOf(res.body);\n const points = sameAddress(canonical, origin);\n out.indexAlias = {\n duplicate: !points,\n detail: points\n ? \"it answers, and declares the homepage canonical\"\n : \"it answers with the homepage's content and does not point back at it\",\n };\n }\n }\n }\n\n // ---- The same path in a different case ----------------------------------\n {\n const sample = crawl.pages\n .map((p) => p.url)\n .find((u) => {\n try {\n return /\\/[a-z][a-z0-9-]*\\/?$/.test(new URL(u).pathname);\n } catch {\n return false;\n }\n });\n if (sample) {\n const url = new URL(sample);\n const flipped = url.pathname.replace(/\\/([a-z])/g, (_m, c: string) => `/${c.toUpperCase()}`);\n url.pathname = flipped;\n const res = await ask(url.toString(), { method: \"GET\", rangeBytes: 65_536 });\n if (res && res.status !== null) {\n if (classify(res.status) !== \"ok\") {\n out.caseAlias = null;\n } else {\n const canonical = canonicalOf(res.body);\n const points = sameAddress(canonical, sample) || sameAddress(res.finalUrl, sample);\n out.caseAlias = {\n path: flipped,\n duplicate: !points,\n detail: points\n ? \"it resolves back to the lower-case address\"\n : \"it answers as its own page, so the same content has two addresses\",\n };\n }\n }\n }\n }\n\n // ---- Do the sitemap's URLs answer? --------------------------------------\n const sitemapSample = crawl.sitemap.sample ?? [];\n if (!crawl.sitemap.present || sitemapSample.length === 0) {\n out.sitemapUrls = crawl.sitemap.present ? undefined : null;\n } else {\n // Spread across the sitemap rather than its first twelve entries, which on\n // most CMSes are the newest posts and the least likely to be stale.\n const step = Math.max(1, Math.floor(sitemapSample.length / MAX_SITEMAP_PROBES));\n const picked = sitemapSample.filter((_, i) => i % step === 0).slice(0, MAX_SITEMAP_PROBES);\n const broken: UrlProbe[] = [];\n await pacedEach(picked, 0, async (url) => {\n const res = await ask(url, { method: \"HEAD\" });\n if (!res) return;\n const verdict = classify(res.status);\n if (verdict === \"broken\") broken.push({ url, status: res.status, verdict });\n });\n out.sitemapUrls = { checked: picked.length, total: crawl.sitemap.urlCount, broken };\n }\n\n // ---- Links out to other sites -------------------------------------------\n {\n const originKey = canonicalizeUrl(origin)?.split(\"/\")[0] ?? \"\";\n const external = new Set<string>();\n for (const page of crawl.pages) {\n for (const a of extractOf(page)?.anchors ?? []) {\n const abs = resolveNavigable(a.href, page.url);\n if (!abs || !safe(abs)) continue;\n const host = canonicalizeUrl(abs)?.split(\"/\")[0] ?? \"\";\n if (host && host !== originKey) external.add(abs);\n }\n }\n const all = [...external];\n if (all.length === 0) {\n out.externalLinks = null;\n } else {\n const picked = all.slice(0, MAX_EXTERNAL_PROBES);\n const broken: UrlProbe[] = [];\n await pacedEach(picked, 0, async (url) => {\n const res = await ask(url, { method: \"HEAD\" });\n if (!res) return;\n const verdict = classify(res.status);\n if (verdict === \"broken\") broken.push({ url, status: res.status, verdict });\n });\n out.externalLinks = { checked: picked.length, total: all.length, broken };\n }\n }\n\n // ---- The image that appears when the site is shared ----------------------\n {\n const declared = homeExtract?.social?.[\"og:image\"] ?? null;\n const url = declared ? resolveNavigable(declared, home?.url ?? origin) : null;\n if (!url) {\n out.ogImage = null;\n } else {\n const res = await ask(url, { method: \"GET\", rangeBytes: IMAGE_HEADER_BYTES });\n if (res) {\n const size = imageSize(res.body);\n out.ogImage = {\n url,\n verdict: classify(res.status),\n width: size?.width ?? null,\n height: size?.height ?? null,\n };\n }\n }\n }\n\n // ---- The logo ------------------------------------------------------------\n {\n // `imageSrcs` is absent on reports stored before it existed, which reads as\n // \"we did not look\", not \"the page has no images\".\n const images = homeExtract?.imageSrcs;\n const byName = images?.find((src) => /logo/i.test(src));\n const chosen = byName ?? images?.[0] ?? null;\n const url = chosen ? resolveNavigable(chosen, home?.url ?? origin) : null;\n if (!images) {\n out.logo = undefined;\n } else if (!url) {\n out.logo = null;\n } else {\n const res = await ask(url, { method: \"HEAD\" });\n if (res) {\n out.logo = {\n url,\n // Named, because \"the logo\" is a guess and the reader deserves to\n // know which guess we made before they act on the verdict.\n how: byName\n ? \"the first image with 'logo' in its file name\"\n : \"the first image on the homepage\",\n verdict: classify(res.status),\n };\n }\n }\n }\n\n // ---- Internal links that take more than one hop --------------------------\n {\n const originKey = canonicalizeUrl(origin)?.split(\"/\")[0] ?? \"\";\n const internal = new Set<string>();\n for (const page of crawl.pages) {\n for (const a of extractOf(page)?.anchors ?? []) {\n const abs = resolveNavigable(a.href, page.url);\n if (!abs || !safe(abs)) continue;\n const host = canonicalizeUrl(abs)?.split(\"/\")[0] ?? \"\";\n if (host === originKey && !sameAddress(abs, page.url)) internal.add(abs);\n }\n }\n // Spread across the set, not the first ten. On apple.com the first ten\n // internal links are all shop navigation, so the sample described one\n // component rather than the site.\n const all = [...internal];\n const step = Math.max(1, Math.floor(all.length / MAX_REDIRECT_PROBES));\n const picked = all.filter((_, i) => i % step === 0).slice(0, MAX_REDIRECT_PROBES);\n if (picked.length > 0) {\n const chained: { url: string; hops: number }[] = [];\n await pacedEach(picked, 0, async (url) => {\n const res = await ask(url, { method: \"HEAD\" });\n if (res?.hops != null && res.hops > MAX_ORDINARY_HOPS) {\n chained.push({ url, hops: res.hops });\n }\n });\n out.redirectChains = { checked: picked.length, chained };\n }\n }\n\n out.requests = requests;\n return out;\n}\n\n/**\n * The production request.\n *\n * `redirect: \"manual\"` and the chain walked by hand, because `fetch` following\n * redirects for us hides the one thing two of these checks are about: how many\n * hops it took. `res.url` after an automatic follow gives the destination and\n * not the distance.\n *\n * The SSRF guard is applied to every hop, not only the first — a redirect is\n * the classic way to get a fetcher to an address it would have refused.\n */\nexport const MAX_HOPS = 6;\n\nexport function defaultHttpProbeDeps(\n userAgent: string,\n fetchImpl: typeof fetch = fetch,\n): HttpProbeDeps {\n return {\n delayMs: 150,\n async request(url, opts) {\n const headers: Record<string, string> = {\n \"user-agent\": userAgent,\n accept: \"text/html,image/*,*/*\",\n };\n if (opts.rangeBytes) headers.range = `bytes=0-${opts.rangeBytes - 1}`;\n\n let current = url;\n let hops = 0;\n for (;;) {\n if (!safe(current)) {\n return { status: null, headers: {}, finalUrl: null, hops, body: null, error: \"refused\" };\n }\n const res = await fetchImpl(current, {\n method: opts.method,\n headers,\n redirect: \"manual\",\n signal: AbortSignal.timeout(15_000),\n });\n const flat: Record<string, string> = Object.fromEntries(\n [...res.headers].map(([k, v]) => [k.toLowerCase(), v]),\n );\n const location = flat[\"location\"];\n if (res.status >= 300 && res.status < 400 && location && hops < MAX_HOPS) {\n await res.body?.cancel();\n const next = resolveNavigable(location, current);\n if (!next) break;\n current = next;\n hops++;\n continue;\n }\n let body: Uint8Array | null = null;\n if (opts.rangeBytes) {\n // A server that ignores `Range` sends the whole file, so the read is\n // capped on our side too rather than trusting the header we sent.\n const buf = await res.arrayBuffer();\n body = new Uint8Array(buf.slice(0, opts.rangeBytes));\n } else {\n await res.body?.cancel();\n }\n return { status: res.status, headers: flat, finalUrl: current, hops, body, error: null };\n }\n return {\n status: null,\n headers: {},\n finalUrl: current,\n hops,\n body: null,\n error: \"redirect loop\",\n };\n },\n };\n}\n","import { resolveNavigable } from \"./journey.js\";\nimport { usablePages } from \"./pages.js\";\nimport type { Scope } from \"./goals.js\";\nimport type { PageVitals } from \"./accessibility.js\";\nimport type { FormProbe } from \"./interaction.js\";\nimport type { DnsFindings } from \"./dns.js\";\nimport { MIN_OG_IMAGE_EDGE, type HttpFindings, type ProbeVerdict } from \"./http-probes.js\";\nimport type { ChecksResult, CrawlResult, FormShape, PageAnchor, PageExtract } from \"./types.js\";\n\n/**\n * The things a careful person would check with a browser and ten minutes.\n *\n * Every check here is a pure function of what the crawl already stored. Nothing\n * in this file makes a request, which is the point: it can be run over every\n * audit already in the database without touching a prospect's server, and a bug\n * found by that replay costs nothing to fix.\n *\n * FOUR STATES, NOT THREE. The rest of this codebase already knows that our\n * failed measurement must never render as the site's defect, which is why\n * `unmeasured` exists. This file adds the state that review kept catching:\n * `not-applicable`. A site with no `LocalBusiness` schema, no HSTS header and no\n * RTL content must not silently PASS the checks about those — that inflates\n * \"34 of 35 passed\" with checks that never ran, which is the same dishonesty as\n * a false failure wearing nicer clothes. Both non-verdicts leave the\n * denominator; see `tally`.\n *\n * THE FLOOR IS BARE HTML. A hand-written page with a title, a heading and a\n * paragraph should pass almost all of this. A check that a careful, ordinary\n * site fails is not measuring care, it is measuring fashion, and it makes the\n * report an argument rather than a document. Where a bar had to be set, it is\n * set generously and the reasoning is written down next to it.\n *\n * WHAT PASSES IS ONE LINE. Most of these will be green on most sites, and that\n * is by design — they cost one row inside a collapsed section and are invisible\n * until the day one fails, at which point it is the most useful row on the\n * page. That is what makes a trivial check worth having, and it is the only\n * thing that makes it worth having.\n */\n\nexport type CheckStatus = \"pass\" | \"fail\" | \"unmeasured\" | \"not-applicable\";\n\nexport type SiteCheck = {\n key: string;\n /** What was checked, in the reader's terms rather than ours. */\n label: string;\n status: CheckStatus;\n /**\n * What we found — the receipt, on a pass as much as on a fail. A green row\n * saying \"we looked and it is fine\" is worth more when it says what it\n * looked at.\n */\n evidence: string | null;\n /** Why it matters to them. Printed only on a fail; a cleared check needs no\n * argument. */\n why: string;\n scope: Scope;\n};\n\n/**\n * How many checks actually reached a verdict, and how many of those passed.\n *\n * `total` counts ONLY `pass` and `fail`. Printing \"34 of 40\" where six of the\n * forty were never applicable to this site would be a number we inflated on our\n * own behalf, and a reader who worked out what we had done would be right to\n * discard the other thirty-four.\n */\n/** \"all 12 links\", but \"the one link\" — because \"all 1\" reads like a bug in the\n * report, and a reader who spots one wonders what else is generated wrong. */\nfunction countOf(n: number, singular: string, plural: string): string {\n return n === 1 ? `the one ${singular}` : `all ${n} ${plural}`;\n}\n\nexport function tally(checks: SiteCheck[]): { passed: number; failed: number; total: number } {\n const passed = checks.filter((c) => c.status === \"pass\").length;\n const failed = checks.filter((c) => c.status === \"fail\").length;\n return { passed, failed, total: passed + failed };\n}\n\n/** One check, phrased once. `evidence` is the receipt either way; `ok` is the\n * verdict. Use `skip`/`unknown` rather than passing a check that did not run. */\nfunction check(\n key: string,\n label: string,\n why: string,\n scope: Scope,\n ok: boolean,\n evidence: string | null,\n): SiteCheck {\n return { key, label, status: ok ? \"pass\" : \"fail\", evidence, why, scope };\n}\n\n/** This site has nothing for the check to be about — no schema block, no HSTS\n * header, no second page. Not a pass, and it leaves the denominator. */\nfunction skip(key: string, label: string, why: string, scope: Scope, note: string): SiteCheck {\n return { key, label, status: \"not-applicable\", evidence: note, why, scope };\n}\n\n/** WE could not look. Also leaves the denominator, and for the more important\n * reason: it is our gap, and it must never be printed as their defect. */\nfunction unknown(\n key: string,\n label: string,\n why: string,\n scope: Scope,\n note: string | null = null,\n): SiteCheck {\n return { key, label, status: \"unmeasured\", evidence: note, why, scope };\n}\n\n// ─── Anchors ─────────────────────────────────────────────────────────────────\n\n/**\n * Link text that names no destination.\n *\n * Deliberately short and deliberately whole-phrase. \"More\" inside \"More about\n * our process\" is a fine link; a link whose ENTIRE text is \"more\" is not, and\n * matching a substring would have flagged the first.\n */\nconst VAGUE_LINK_TEXT = new Set([\n \"click here\",\n \"here\",\n \"read more\",\n \"learn more\",\n \"more\",\n \"more info\",\n \"more information\",\n \"find out more\",\n \"this\",\n \"link\",\n \"this link\",\n \"continue\",\n \"go\",\n]);\n\n/** Hosts that only ever appear in a link by mistake. `*.local` and the RFC\n * reserved names are development leftovers; a visitor following one gets a\n * browser error page with the site's name at the top of it. */\nconst LOCAL_HOST =\n /^(localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1\\]|.*\\.local|.*\\.test|.*\\.localhost)$/i;\n\n/** Placeholder social URLs, as shipped by the theme. Matched on the whole path\n * so a real profile at /yourbusiness-la is untouched. */\nconst PLACEHOLDER_SOCIAL =\n /^(?:https?:\\/\\/)?(?:www\\.)?(?:facebook|twitter|x|instagram|linkedin|youtube|tiktok|pinterest)\\.com\\/?(?:(?:in|company|user|channel|@)\\/)?(?:yourhandle|username|yourusername|yourpage|yourcompany|your-business|profile\\.php|home)?\\/?$/i;\n\n/** A `tel:` a phone can actually dial: digits, with the punctuation a dialler\n * tolerates. `tel:call-us-today` is a link that does nothing. */\nconst DIALABLE_TEL = /^tel:\\+?[0-9().\\-\\s]{6,}$/i;\n\nfunction anchorChecks(pages: { url: string; extract: PageExtract }[], origin: string): SiteCheck[] {\n const out: SiteCheck[] = [];\n const measured = pages.length > 0 && pages.every((p) => p.extract.anchors !== undefined);\n\n const WHY_DEAD = \"A link that goes nowhere reads as a broken site to the person who clicks it.\";\n if (!measured) {\n return [\n unknown(\"dead-links\", \"Links that go nowhere\", WHY_DEAD, \"quick\"),\n unknown(\n \"staging-links\",\n \"Links pointing at a development address\",\n \"A link to localhost or a staging host takes the visitor to a browser error page with your name on it.\",\n \"quick\",\n ),\n unknown(\n \"insecure-links\",\n \"Links back to your own http address\",\n \"A link to the http:// spelling of your own address makes the visitor ask for the page in the clear before your redirect sends them back, and costs them a round trip to do it.\",\n \"quick\",\n ),\n unknown(\n \"link-text\",\n \"Links that say where they go\",\n \"“Read more” tells a visitor nothing, and it is the whole label a screen reader announces.\",\n \"content\",\n ),\n unknown(\n \"tel-dialable\",\n \"Phone links a phone can dial\",\n \"A tel: link that is not a number does nothing at all when tapped.\",\n \"quick\",\n ),\n unknown(\n \"social-placeholders\",\n \"Social links that go to your accounts\",\n \"An unedited theme placeholder sends visitors to a login page or a stranger.\",\n \"quick\",\n ),\n unknown(\n \"nav-consistency\",\n \"Navigation that agrees with itself\",\n \"The same menu item leading to two different pages is confusing in a way visitors blame on you.\",\n \"quick\",\n ),\n ];\n }\n\n const all: { a: PageAnchor; page: string }[] = [];\n for (const p of pages) for (const a of p.extract.anchors ?? []) all.push({ a, page: p.url });\n // `anchors` is capped per page; `anchorCount` is not. Every denominator below\n // says \"the links we read\" and uses `all.length` on purpose — but where a\n // count is quoted as a proportion, the true total has to appear beside it or\n // we are printing our own ceiling as the site's link count. apple.com: 6000\n // read, 14,153 there.\n const trueTotal = pages.reduce((n, p) => n + (p.extract.anchorCount ?? 0), 0);\n const ofTrue = trueTotal > all.length ? ` — of ${trueTotal} on these pages` : \"\";\n\n // `href=\"\"` never reaches us — extract.ts only collects anchors with a\n // non-empty href — so the whole finding here is the javascript: no-op. A bare\n // `href=\"#\"` is deliberately NOT counted: it is how a great many perfectly\n // good disclosure buttons are written, and we cannot see the click handler\n // that makes it work.\n const dead = all.filter(({ a }) => /^javascript:\\s*(void\\s*\\(\\s*0\\s*\\)|;)?\\s*$/i.test(a.href));\n out.push(\n check(\n \"dead-links\",\n \"Links that go nowhere\",\n WHY_DEAD,\n \"quick\",\n dead.length === 0,\n dead.length === 0\n ? `none among the ${all.length} links we read`\n : `${dead.length} of ${all.length}: ${dead\n .slice(0, 3)\n .map((d) => `“${d.a.text || d.a.href}” on ${d.page}`)\n .join(\", \")}`,\n ),\n );\n\n // A link to the site's OWN host is never a stray development link, whatever\n // that host looks like. Without this the check fired on every internal link\n // of any site served from a `.test`, `.local` or `localhost` name — which\n // includes our own fixture, and would have included any client running an\n // internal tool. The finding is a link that leaves for a development\n // address, not a site that lives at one.\n let ownHost: string | null = null;\n try {\n ownHost = new URL(origin).hostname.toLowerCase();\n } catch {\n ownHost = null;\n }\n const staging = all.filter(({ a, page }) => {\n const abs = resolveNavigable(a.href, page);\n if (!abs) return false;\n try {\n const host = new URL(abs).hostname.toLowerCase();\n return host !== ownHost && LOCAL_HOST.test(host);\n } catch {\n return false;\n }\n });\n out.push(\n check(\n \"staging-links\",\n \"Links pointing at a development address\",\n \"A link to localhost or a staging host takes the visitor to a browser error page with your name on it.\",\n \"quick\",\n staging.length === 0,\n staging.length === 0\n ? \"none\"\n : `${staging.length} ${staging.length === 1 ? \"link\" : \"links\"}, including ${staging\n .slice(0, 3)\n .map((s) => s.a.href)\n .join(\", \")}`,\n ),\n );\n\n // Only meaningful on a site that is itself on https — on an http site the\n // finding is the site, and `insecureEntry` already says so.\n if (origin.startsWith(\"https://\")) {\n // Only links back to the site's OWN host.\n //\n // The rule was every http:// link, and it was wrong about what happens.\n // Following an outbound one navigates AWAY: the padlock on the page they\n // left is untouched, browsers now upgrade top-level http navigations on\n // their own, and the destination almost always redirects to https anyway.\n // Four of seven measurable sites failed this, and three of the four failed\n // on `http://www.facebook.com/…` and the like — old social links that cost\n // their visitors nothing. That is a finding invented out of a claim that\n // does not follow.\n //\n // A link to your own http address is a different thing and still true: the\n // first request really does leave in the clear, and HSTS cannot help on a\n // browser's first contact with the site.\n const insecure = all.filter(({ a }) => {\n const href = a.href.trim();\n if (!/^http:\\/\\//i.test(href)) return false;\n try {\n return new URL(href).hostname.replace(/^www\\./i, \"\") === ownHost?.replace(/^www\\./i, \"\");\n } catch {\n return false;\n }\n });\n out.push(\n check(\n \"insecure-links\",\n \"Links back to your own http address\",\n \"A link to the http:// spelling of your own address makes the visitor ask for the page in the clear before your redirect sends them back, and costs them a round trip to do it.\",\n \"quick\",\n insecure.length === 0,\n insecure.length === 0\n ? \"every link to your own pages stays on https\"\n : `${insecure.length} ${insecure.length === 1 ? \"link\" : \"links\"} to your own http:// address, including ${insecure\n .slice(0, 3)\n .map((s) => s.a.href)\n .join(\", \")}`,\n ),\n );\n } else {\n out.push(\n skip(\n \"insecure-links\",\n \"Links back to your own http address\",\n \"A link to the http:// spelling of your own address makes the visitor ask for the page in the clear before your redirect sends them back, and costs them a round trip to do it.\",\n \"quick\",\n \"the site is not served over https, which is the finding above instead\",\n ),\n );\n }\n\n // The label the link ACTUALLY announces, which is the aria-label when there is\n // one and the visible text otherwise. Judging the text alone failed 60 of\n // apple.com's links, every one of them written\n // `aria-label=\"Learn more about accessibility\"` around a span reading \"Learn\n // more\" — a labelled link marked bare, which is the same blind spot that got\n // T0-05 cut. A vague aria-label still counts: the point is the destination,\n // not the attribute.\n //\n // `ariaLabel` is optional, and absent means we never captured it. On a report\n // stored before it existed that is every anchor, so the check goes unmeasured\n // rather than judging text we know is only half the label.\n const labelled = all.every(({ a }) => a.ariaLabel !== undefined);\n const WHY_TEXT =\n \"“Read more” tells a visitor nothing, and with no aria-label behind it, it is the whole label a screen reader announces when it lists the links on your page.\";\n if (!labelled) {\n out.push(unknown(\"link-text\", \"Links that say where they go\", WHY_TEXT, \"content\"));\n } else {\n const vague = all.filter(({ a }) =>\n VAGUE_LINK_TEXT.has((a.ariaLabel || a.text).trim().toLowerCase()),\n );\n // A generous bar on purpose. One \"read more\" on a blog index is ordinary\n // writing; a page built entirely out of them is the finding, and a threshold\n // of one would fail most of the good web.\n const VAGUE_LIMIT = 5;\n out.push(\n check(\n \"link-text\",\n \"Links that say where they go\",\n WHY_TEXT,\n \"content\",\n vague.length <= VAGUE_LIMIT,\n vague.length === 0\n ? `all ${all.length} links we read${ofTrue} name their destination`\n : `${vague.length} of the ${all.length} we read${ofTrue} say only “${[...new Set(vague.map((v) => (v.a.ariaLabel || v.a.text).trim()))].slice(0, 3).join(\"”, “\")}”`,\n ),\n );\n }\n\n const tels = all.filter(({ a }) => /^tel:/i.test(a.href.trim()));\n const badTels = tels.filter(({ a }) => !DIALABLE_TEL.test(a.href.trim()));\n const WHY_TEL = \"A tel: link that is not a number does nothing at all when it is tapped.\";\n out.push(\n tels.length === 0\n ? skip(\n \"tel-dialable\",\n \"Phone links a phone can dial\",\n WHY_TEL,\n \"quick\",\n \"no tel: links on the pages we read\",\n )\n : check(\n \"tel-dialable\",\n \"Phone links a phone can dial\",\n WHY_TEL,\n \"quick\",\n badTels.length === 0,\n badTels.length === 0\n ? `all ${tels.length} dial a number`\n : badTels\n .map((b) => b.a.href)\n .slice(0, 3)\n .join(\", \"),\n ),\n );\n\n const placeholders = all.filter(({ a }) => PLACEHOLDER_SOCIAL.test(a.href.trim()));\n out.push(\n check(\n \"social-placeholders\",\n \"Social links that go to your accounts\",\n \"An unedited theme placeholder sends a visitor to a login page or to somebody else entirely.\",\n \"quick\",\n placeholders.length === 0,\n placeholders.length === 0\n ? \"none left unedited\"\n : placeholders\n .map((p) => p.a.href)\n .slice(0, 3)\n .join(\", \"),\n ),\n );\n\n // Nav drift, defined off the shared template rather than guessed at: a link\n // whose TEXT appears on every page we read is part of the navigation, and it\n // should reach the same place from every page. `consistency.ts` derives the\n // same shared set for its off-template check; this asks whether that set\n // agrees with itself.\n out.push(navConsistency(pages));\n\n return out;\n}\n\nfunction navConsistency(pages: { url: string; extract: PageExtract }[]): SiteCheck {\n const WHY =\n \"The same menu item leading to two different pages is the kind of confusion visitors blame on you rather than on the menu.\";\n // Needs at least two pages to compare, and a nav is only a nav if it repeats.\n if (pages.length < 2) {\n return skip(\n \"nav-consistency\",\n \"Navigation that agrees with itself\",\n WHY,\n \"quick\",\n \"only one page was read, so there is no shared navigation to compare\",\n );\n }\n\n const perPage = pages.map(({ url, extract }) => {\n const m = new Map<string, Set<string>>();\n for (const a of extract.anchors ?? []) {\n const text = a.text.trim().toLowerCase();\n const abs = resolveNavigable(a.href, url);\n if (!text || !abs) continue;\n if (!m.has(text)) m.set(text, new Set());\n m.get(text)!.add(abs);\n }\n return m;\n });\n\n const shared = [...(perPage[0]?.keys() ?? [])].filter((text) =>\n perPage.every((m) => m.has(text)),\n );\n if (shared.length === 0) {\n return skip(\n \"nav-consistency\",\n \"Navigation that agrees with itself\",\n WHY,\n \"quick\",\n \"no link text appears on every page, so we could not identify a shared navigation\",\n );\n }\n\n // Drift is \"there is NO destination this text reaches from every page\", not\n // \"this text reaches more than one destination\". The union rule read a\n // mega-menu as a broken menu: on apple.com the word \"Mac\" links to /mac/, to\n // two shop pages and to a support page — the same four destinations on all\n // twenty pages, byte for byte — and the union counted four, so 18 menu items\n // were reported as leading somewhere different depending on the page. The\n // intersection is /mac/ on every page, which is the truth.\n //\n // Strictly weaker than the union rule: an empty intersection over sets that\n // are each non-empty implies more than one distinct target, so nothing that\n // passes today can begin to fail. It can only stop lying.\n const drifting = shared.filter((text) => {\n let common: Set<string> | null = null;\n for (const m of perPage) {\n const here = m.get(text) ?? new Set<string>();\n const kept: string[] = common === null ? [...here] : [...common].filter((t) => here.has(t));\n common = new Set(kept);\n if (common.size === 0) return true;\n }\n return common === null || common.size === 0;\n });\n\n return check(\n \"nav-consistency\",\n \"Navigation that agrees with itself\",\n WHY,\n \"quick\",\n drifting.length === 0,\n drifting.length === 0\n ? `${shared.length} shared menu ${shared.length === 1 ? \"item goes\" : \"items go\"} to the same place on every page`\n : `${drifting\n .map((d) => `“${d}”`)\n .slice(0, 3)\n .join(\n \", \",\n )} ${drifting.length === 1 ? \"leads\" : \"lead\"} somewhere different depending on the page`,\n );\n}\n\n// ─── Page text ───────────────────────────────────────────────────────────────\n\nconst LOREM = /\\b(lorem ipsum|dolor sit amet|consectetur adipiscing)\\b/i;\n\n/** Placeholder copy a theme ships and an owner forgets. Whole phrases, because\n * \"your text here\" as a fragment of a real sentence is not a finding. */\nconst PLACEHOLDER_COPY =\n /\\b(your (?:text|content|title|headline|company name|business name) here|insert (?:text|content|your) |add your (?:text|content) here|edit this text|this is a sample|sample text|placeholder text|coming soon\\.{3})/i;\n\n/**\n * Template syntax that reached the page.\n *\n * Bare `null` and `undefined` are deliberately absent: they are English words\n * that appear in honest prose (\"the null hypothesis\"), and flagging them would\n * fail a page for writing well. What is matched is syntax nobody types on\n * purpose.\n */\nconst TEMPLATE_LEAKAGE = /(\\{\\{[^}]{0,80}\\}\\}|\\[object Object\\]|<%=?[^%]{0,80}%>|\\$\\{[^}]{0,60}\\})/;\n\n/**\n * Mojibake — UTF-8 read as Latin-1, then re-encoded.\n *\n * `’` for an apostrophe, `Â` before a space, `` for a stray byte-order\n * mark. Extremely common on sites that have been migrated between platforms,\n * extremely visible to a reader, and a one-line fix. The sequences below only\n * occur as an encoding accident.\n */\nconst MOJIBAKE = /(â€[™œ“”˜]|Ã[©¨¤¡«»]||Â[\\s£©®])/;\n\n/**\n * A page that ANNOUNCES itself unfinished — matched against its title and its\n * headings, never its prose.\n *\n * Running text was the first rule and the corpus falsified it twice out of\n * twice: \"the list of open positions is always changing, so check back soon\"\n * on a working careers page, and \"Santa Ana, CA (Coming Soon)\" beside a new\n * store on a locations list. Both are the business working, and we reported\n * both as the business broken. A heading is what separates a page that IS a\n * placeholder from a page that mentions the future.\n *\n * \"page not found\" left with the prose rule. Whether a missing page behaves is\n * asked and answered against a real request (see basics.ts); matching the\n * phrase in body text could only ever add a second, worse-informed voice.\n */\nconst UNDER_CONSTRUCTION =\n /\\b(under construction|coming soon|site is being (?:built|updated)|check back soon)\\b/i;\n\nfunction textChecks(\n pages: { url: string; extract: PageExtract }[],\n businessName: string | null,\n): SiteCheck[] {\n const out: SiteCheck[] = [];\n\n const firstMatch = (re: RegExp): { page: string; snippet: string } | null => {\n for (const p of pages) {\n const m = re.exec(p.extract.text);\n if (m) {\n const at = Math.max(0, m.index - 40);\n return { page: p.url, snippet: `…${p.extract.text.slice(at, m.index + 60).trim()}…` };\n }\n }\n return null;\n };\n\n const cases: [string, string, string, Scope, RegExp][] = [\n [\n \"lorem\",\n \"Placeholder Latin left in the copy\",\n \"Lorem ipsum on a live page tells a visitor the site was never finished.\",\n \"quick\",\n LOREM,\n ],\n [\n \"placeholder-copy\",\n \"Theme placeholder text left in the copy\",\n \"“Your text here” on a live page reads as a site nobody has looked at since it was built.\",\n \"quick\",\n PLACEHOLDER_COPY,\n ],\n [\n \"template-leakage\",\n \"Template code showing as text\",\n \"A visitor sees the machinery instead of the sentence, which reads as broken rather than unfinished.\",\n \"quick\",\n TEMPLATE_LEAKAGE,\n ],\n [\n \"mojibake\",\n \"Text with broken characters\",\n \"Apostrophes and quotes rendering as ’ is the most visible sign of a migration nobody checked, and it is a one-line fix at the source.\",\n \"quick\",\n MOJIBAKE,\n ],\n ];\n\n for (const [key, label, why, scope, re] of cases) {\n if (pages.length === 0) {\n out.push(unknown(key, label, why, scope));\n continue;\n }\n const hit = firstMatch(re);\n out.push(\n check(\n key,\n label,\n why,\n scope,\n hit === null,\n hit === null\n ? `none across the ${pages.length} pages we read`\n : `${hit.page}: ${hit.snippet}`,\n ),\n );\n }\n\n out.push(underConstruction(pages));\n out.push(nameInTitle(pages, businessName));\n return out;\n}\n\n/** See `UNDER_CONSTRUCTION`: the title and the headings, which is where a page\n * that is a placeholder says so, and not the prose, where saying so is\n * ordinary copy. */\nfunction underConstruction(pages: { url: string; extract: PageExtract }[]): SiteCheck {\n const KEY = \"under-construction\";\n const LABEL = \"Pages that say they are unfinished\";\n const WHY = \"A live page announcing it is coming soon is a page better unlinked until it is not.\";\n if (pages.length === 0) return unknown(KEY, LABEL, WHY, \"content\");\n\n for (const p of pages) {\n const headlines = [p.extract.title ?? \"\", ...p.extract.headings.map((h) => h.text)];\n for (const line of headlines) {\n const m = UNDER_CONSTRUCTION.exec(line);\n if (m) {\n return check(KEY, LABEL, WHY, \"content\", false, `${p.url}: “${line.trim()}”`);\n }\n }\n }\n return check(\n KEY,\n LABEL,\n WHY,\n \"content\",\n true,\n `no page announces itself unfinished, across the ${pages.length} we read`,\n );\n}\n\n/** Dropped before taking the head of a business name: \"The Pointe Burbank\" is\n * distinguished by \"pointe\", and every name beginning \"The\" would otherwise be\n * tested against a word that appears in half the titles on the web. */\nconst LEADING_ARTICLES = new Set([\"the\"]);\n\nfunction nameInTitle(\n pages: { url: string; extract: PageExtract }[],\n businessName: string | null,\n): SiteCheck {\n const WHY =\n \"The title is the line in a search result, a browser tab and a shared link. Without your name in it, all three are anonymous.\";\n const LABEL = \"Your name in the page title\";\n // Depends on a name the analyze stage inferred. No name is OUR gap, and a\n // check that grades a site against a name we guessed at would be worse than\n // no check.\n if (!businessName) return unknown(\"name-in-title\", LABEL, WHY, \"quick\");\n const home = pages[0];\n if (!home || !home.extract.title) {\n return unknown(\"name-in-title\", LABEL, WHY, \"quick\");\n }\n // The DISTINCTIVE word, not every word of the name.\n //\n // Requiring all of them was the first rule and the corpus caught it out:\n // \"Worthe Real Estate Group\" against the title \"Worthe — Over 7,000,000 SF of\n // properties in the Los Angeles area\" was scored a fail. That is a good\n // title. It names the company and says what they do, and we failed it for\n // dropping a category word the model had appended to the name. Three of the\n // four fails were that same shape.\n //\n // The question this check asks is whether a stranger seeing the tab, the\n // search result or the shared link learns whose page it is — so the test is\n // the head of the name, the part a person types first. \"QTI Sensing\n // Solutions\" against \"Your Thermistor Experts\" still fails, which is the one\n // fail of the four that was real.\n const words = businessName\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter((w) => w.length > 2 && !LEADING_ARTICLES.has(w));\n const head = words[0];\n const title = home.extract.title.toLowerCase();\n const hit = head !== undefined && title.includes(head);\n return check(\"name-in-title\", LABEL, WHY, \"quick\", hit, `“${home.extract.title}”`);\n}\n\n// ─── Headings ────────────────────────────────────────────────────────────────\n\nfunction headingChecks(\n pages: { url: string; extract: PageExtract }[],\n businessName: string | null,\n): SiteCheck[] {\n const WHY_PRESENT =\n \"The h1 is the page's headline. A page without one gives a reader — and an assistant looking for something to quote — nothing that says what the page is about.\";\n const WHY_ONE =\n \"When everything on the page is marked as the page's headline, none of it is, and whatever gets quoted back is whichever one came first in the markup.\";\n if (pages.length === 0) {\n return [\n unknown(\"h1-present\", \"A headline on every page\", WHY_PRESENT, \"content\"),\n unknown(\"h1-one-headline\", \"One headline, not a list of them\", WHY_ONE, \"content\"),\n unknown(\n \"h1-not-name\",\n \"Headlines that say what the page is about\",\n \"A headline that is just your company name on every page describes none of them.\",\n \"content\",\n ),\n unknown(\n \"h1-distinct\",\n \"A different headline on each page\",\n \"Identical headlines make every page look like the same page, to a reader and to a search engine.\",\n \"content\",\n ),\n ];\n }\n\n // One entry per declared address, minus the pages hidden from search — see\n // `contentPages`. Pages we read that are ALL hidden are neither our gap nor\n // their defect: the noindex is reported where it belongs, and a site that\n // withdraws itself from search owes it no headlines.\n const counted = contentPages(pages);\n if (counted.length === 0) {\n const HIDDEN =\n \"every page we read asks not to be indexed, which is reported under meta-noindex\";\n return [\n skip(\"h1-present\", \"A headline on every page\", WHY_PRESENT, \"content\", HIDDEN),\n skip(\"h1-one-headline\", \"One headline, not a list of them\", WHY_ONE, \"content\", HIDDEN),\n skip(\n \"h1-not-name\",\n \"Headlines that say what the page is about\",\n \"A headline that is just your company name on every page describes none of them.\",\n \"content\",\n HIDDEN,\n ),\n skip(\n \"h1-distinct\",\n \"A different headline on each page\",\n \"Identical headlines make every page look like the same page, to a reader and to a search engine.\",\n \"content\",\n HIDDEN,\n ),\n ];\n }\n\n const h1sPerPage = counted.map((p) => ({\n url: p.url,\n h1s: p.extract.headings.filter((h) => h.level === 1).map((h) => h.text.trim()),\n }));\n\n // Two findings, two rows.\n //\n // These were one check, \"exactly one h1 per page\", and it was the loudest row\n // in the battery — 24 of the 29 sites in the stored corpus failed it, on one\n // line of evidence reading \"has none, has 2, has 33\". Two different faults of\n // two different sizes, welded together and reported as one.\n //\n // NO h1 is a real defect, and 115 of 489 pages had none. SEVERAL is a\n // different question with a different answer: the HTML spec permits an h1 per\n // sectioning element and Google has said on the record that more than one is\n // fine, so \"you have two\" is not a finding we can defend. Twenty-eight of the\n // corpus's 44 multi-h1 pages sit at exactly two.\n //\n // Note: extract.ts records a heading by its ACCESSIBLE name, so `<h1><img\n // alt=\"Acme\"></h1>` counts. It did not until this split, and every page using\n // that very ordinary pattern was arriving here as a page with no headline.\n const missing = h1sPerPage.filter((p) => p.h1s.length === 0);\n const out: SiteCheck[] = [\n check(\n \"h1-present\",\n \"A headline on every page\",\n WHY_PRESENT,\n \"content\",\n missing.length === 0,\n missing.length === 0\n ? `all ${counted.length} pages carry one`\n : `${missing.length} of ${counted.length}: ${missing\n .slice(0, 3)\n .map((p) => p.url)\n .join(\", \")}`,\n ),\n ];\n\n // Three, not two. Two has a standard and correct cause — a visually-hidden\n // headline for screen readers alongside the visible one — and the corpus is\n // full of it. Three has no such pattern behind it; past that the number\n // climbs fast (5, 6, 8, 27, 33), which is a template marking every card on\n // the page as the headline.\n const CROWDED_AT = 3;\n const crowded = h1sPerPage.filter((p) => p.h1s.length >= CROWDED_AT);\n out.push(\n check(\n \"h1-one-headline\",\n \"One headline, not a list of them\",\n WHY_ONE,\n \"content\",\n crowded.length === 0,\n crowded.length === 0\n ? `no page marks more than two of its lines as the headline, across the ${counted.length} we read`\n : crowded\n .slice(0, 3)\n .map((p) => `${p.url} has ${p.h1s.length}`)\n .join(\", \"),\n ),\n );\n\n const WHY_NAME = \"A headline that is only your company name describes none of the page below it.\";\n const LABEL_NAME = \"Headlines that say what the page is about\";\n const named = h1sPerPage.filter((p) => p.h1s.length === 1);\n if (!businessName || named.length === 0) {\n out.push(unknown(\"h1-not-name\", LABEL_NAME, WHY_NAME, \"content\"));\n } else {\n const bare = named.filter(\n (p) =>\n p.h1s[0]!.toLowerCase().replace(/[^\\p{L}\\p{N}]+/gu, \"\") ===\n businessName.toLowerCase().replace(/[^\\p{L}\\p{N}]+/gu, \"\"),\n );\n out.push(\n check(\n \"h1-not-name\",\n LABEL_NAME,\n WHY_NAME,\n \"content\",\n bare.length === 0,\n bare.length === 0\n ? \"every headline describes its page\"\n : `${bare.length} ${bare.length === 1 ? \"page's headline is\" : \"pages' headlines are\"} just “${businessName}”`,\n ),\n );\n }\n\n const WHY_DISTINCT =\n \"Identical headlines make every page look like the same page, to a reader scanning tabs and to a search engine choosing which one to show.\";\n const LABEL_DISTINCT = \"A different headline on each page\";\n if (counted.length < 2) {\n out.push(\n skip(\n \"h1-distinct\",\n LABEL_DISTINCT,\n WHY_DISTINCT,\n \"content\",\n \"only one page was read, so there is nothing to compare it with\",\n ),\n );\n } else {\n // `counted` is already one entry per address the site says it has. Five\n // `?tab=` deep-links into one page are one headline, not five identical\n // ones — see `contentPages`.\n const distinct = counted.filter(\n (p) => p.extract.headings.filter((h) => h.level === 1).length === 1,\n );\n if (distinct.length < 2) {\n out.push(\n skip(\n \"h1-distinct\",\n LABEL_DISTINCT,\n WHY_DISTINCT,\n \"content\",\n distinct.length === 0\n ? \"no page we read carries exactly one headline, which is reported above\"\n : \"the pages we read all declare the same address, so there is nothing to compare\",\n ),\n );\n } else {\n const texts = distinct.map((p) =>\n p.extract.headings\n .find((h) => h.level === 1)!\n .text.trim()\n .toLowerCase(),\n );\n const dupes = texts.filter((t, i) => texts.indexOf(t) !== i);\n out.push(\n check(\n \"h1-distinct\",\n LABEL_DISTINCT,\n WHY_DISTINCT,\n \"content\",\n dupes.length === 0,\n dupes.length === 0\n ? `${texts.length} pages, ${new Set(texts).size} different headlines`\n : `“${[...new Set(dupes)].slice(0, 2).join(\"”, “\")}” appears on more than one page`,\n ),\n );\n }\n }\n\n return out;\n}\n\n// ─── Structured data ─────────────────────────────────────────────────────────\n\n/**\n * Every check here is CONDITIONAL, and that is the whole design.\n *\n * A site with no `LocalBusiness` block has not failed a LocalBusiness check —\n * it has no LocalBusiness to check, which is a different sentence and belongs\n * outside the denominator. The absence of schema ALTOGETHER is already reported\n * by `checks.schema.missingExpected`; asking the same question twice under two\n * headings reads as two problems.\n */\ntype SchemaNode = Record<string, unknown>;\n\nfunction collectNodes(node: unknown, into: SchemaNode[], depth = 0): void {\n if (depth > 8) return;\n if (Array.isArray(node)) {\n for (const n of node) collectNodes(n, into, depth + 1);\n return;\n }\n if (!node || typeof node !== \"object\") return;\n const obj = node as SchemaNode;\n if (obj[\"@type\"]) into.push(obj);\n for (const [k, v] of Object.entries(obj)) {\n if (k === \"@type\") continue;\n if (v && typeof v === \"object\") collectNodes(v, into, depth + 1);\n }\n}\n\nconst typeOf = (n: SchemaNode): string[] => {\n const t = n[\"@type\"];\n const raw =\n typeof t === \"string\" ? [t] : Array.isArray(t) ? t.filter((x) => typeof x === \"string\") : [];\n return raw.map((s) => String(s).replace(/^https?:\\/\\/schema\\.org\\//i, \"\"));\n};\n\nconst isType = (n: SchemaNode, names: string[]): boolean =>\n typeOf(n).some((t) => names.some((name) => t.toLowerCase() === name.toLowerCase()));\n\nconst str = (v: unknown): string | null => (typeof v === \"string\" && v.trim() ? v.trim() : null);\n\n/**\n * The address inside a schema.org value, whatever shape the emitter chose.\n *\n * `logo` was read as a string only, and the corpus said that was wrong: of the\n * fifteen sites we reported as \"missing logo\", NINE publish a perfectly\n * well-formed `{\"@type\":\"ImageObject\",\"url\":…}` — the shape Yoast, Rank Math\n * and Google's own documentation all use. We were reading the rarer spelling\n * and printing our own narrowness as their omission, which is the one thing\n * this report may never do.\n *\n * An object carrying only `@id` is a REFERENCE to a node declared elsewhere in\n * the same @graph, so it is resolved against the nodes we collected rather than\n * counted on its own: an `@id` pointing at nothing really is a dangling\n * declaration, and should still fail.\n */\nfunction schemaUrl(value: unknown, nodes: SchemaNode[], depth = 0): string | null {\n if (depth > 3) return null;\n const direct = str(value);\n if (direct) return direct;\n if (Array.isArray(value)) {\n for (const v of value) {\n const hit = schemaUrl(v, nodes, depth + 1);\n if (hit) return hit;\n }\n return null;\n }\n if (!value || typeof value !== \"object\") return null;\n const obj = value as SchemaNode;\n const inline = str(obj[\"url\"]) ?? str(obj[\"contentUrl\"]);\n if (inline) return inline;\n const id = str(obj[\"@id\"]);\n if (!id) return null;\n const target = nodes.find((n) => n !== obj && str(n[\"@id\"]) === id);\n return target ? schemaUrl(target, nodes, depth + 1) : null;\n}\n\nfunction schemaChecks(\n pages: { url: string; extract: PageExtract }[],\n origin: string,\n phones: string[],\n): SiteCheck[] {\n const nodes: SchemaNode[] = [];\n // Which page each node was embedded in. JSON-LD resolves a relative IRI\n // against the document it sits in, so a block reading `\"url\": \"/\"` is a\n // correct self-reference and only looks like a dangling one if you compare\n // the raw string — see `schema-url-matches`.\n const declaredOn = new Map<SchemaNode, string>();\n let parsedAny = false;\n for (const p of pages) {\n for (const block of p.extract.jsonLd) {\n const before = nodes.length;\n try {\n collectNodes(JSON.parse(block), nodes);\n parsedAny = true;\n } catch {\n // Invalid blocks are counted by checks.schema.invalidBlocks; here they\n // simply contribute nothing.\n }\n for (let i = before; i < nodes.length; i++) declaredOn.set(nodes[i]!, p.url);\n }\n }\n\n const ORG = [\"Organization\", \"LocalBusiness\", \"ProfessionalService\", \"Corporation\"];\n const org = nodes.find((n) => isType(n, ORG));\n const local = nodes.find((n) => isType(n, [\"LocalBusiness\", \"ProfessionalService\"]));\n\n const noSchema = (key: string, label: string, why: string): SiteCheck =>\n skip(\n key,\n label,\n why,\n \"content\",\n parsedAny\n ? \"no such block in your structured data\"\n : \"this site publishes no structured data\",\n );\n\n const out: SiteCheck[] = [];\n\n const WHY_ORG =\n \"This is the block an assistant reads to learn who you are. Half-filled, it answers half the question and the rest gets taken from a directory you do not control.\";\n if (!org) {\n out.push(noSchema(\"schema-org-complete\", \"Your business details in structured data\", WHY_ORG));\n } else {\n // `name` stays string-only — its schema.org range is Text. `url` and `logo`\n // do not: both accept an object, and reading only the string spelling is\n // what made this check the loudest false accusation in the battery.\n const missing = ([\"name\", \"url\", \"logo\"] as const).filter((f) =>\n f === \"name\" ? !str(org[f]) : !schemaUrl(org[f], nodes),\n );\n out.push(\n check(\n \"schema-org-complete\",\n \"Your business details in structured data\",\n WHY_ORG,\n \"content\",\n missing.length === 0,\n missing.length === 0\n ? `name, url and logo all present on your ${typeOf(org)[0]} block`\n : `your ${typeOf(org)[0]} block is missing ${missing.join(\", \")}`,\n ),\n );\n }\n\n const WHY_LOCAL =\n \"A local business block without an address, a phone or opening hours is the one an assistant will fill in from elsewhere.\";\n if (!local) {\n out.push(\n noSchema(\"schema-local-complete\", \"Your address and hours in structured data\", WHY_LOCAL),\n );\n } else {\n const missing = ([\"address\", \"telephone\", \"openingHours\"] as const).filter(\n (f) => local[f] === undefined && local[`${f}Specification`] === undefined,\n );\n out.push(\n check(\n \"schema-local-complete\",\n \"Your address and hours in structured data\",\n WHY_LOCAL,\n \"content\",\n missing.length === 0,\n missing.length === 0\n ? \"address, telephone and opening hours all present\"\n : `missing ${missing.join(\", \")}`,\n ),\n );\n }\n\n const WHY_URL =\n \"A structured-data url pointing at a domain you no longer use tells every assistant that reads it to go and look somewhere else.\";\n const declaredUrl = org ? schemaUrl(org[\"url\"], nodes) : null;\n if (!declaredUrl) {\n out.push(noSchema(\"schema-url-matches\", \"Structured data pointing at this site\", WHY_URL));\n } else {\n // Resolved against the page the block was declared on, because that is what\n // a JSON-LD parser does. airstrip.com publishes `\"url\": \"/\"` on its\n // homepage — correct, and unambiguous to every consumer — and a raw string\n // comparison read it as a url pointing at nothing and failed them for it.\n //\n // An unparseable url is still a fail, not an exception: it is a declaration\n // pointing at nothing this site serves.\n const base = (org ? declaredOn.get(org) : null) ?? origin;\n const absolute = ((): string | null => {\n try {\n return new URL(declaredUrl, base).toString();\n } catch {\n return null;\n }\n })();\n const host = (u: string): string | null => {\n try {\n return new URL(u).hostname.replace(/^www\\./i, \"\").toLowerCase();\n } catch {\n return null;\n }\n };\n const declaredHost = absolute === null ? null : host(absolute);\n const sameHost = declaredHost !== null && declaredHost === host(origin);\n out.push(\n check(\n \"schema-url-matches\",\n \"Structured data pointing at this site\",\n WHY_URL,\n \"content\",\n sameHost,\n // The resolved address, since that is the one we compared. Printing the\n // raw \"/\" back at a reader who passed would read as a bug in us.\n absolute ?? declaredUrl,\n ),\n );\n }\n\n const WHY_PHONE =\n \"When the number in your structured data is not the number on your page, an assistant may quote either one.\";\n const schemaPhone = org ? str(org[\"telephone\"]) : null;\n const digits = (s: string): string => s.replace(/\\D/g, \"\").replace(/^1(?=\\d{10}$)/, \"\");\n if (!schemaPhone || phones.length === 0) {\n out.push(\n skip(\n \"schema-phone-matches\",\n \"One phone number, everywhere\",\n WHY_PHONE,\n \"quick\",\n !schemaPhone\n ? \"no telephone in your structured data\"\n : \"we found no phone number in the page text to compare it with\",\n ),\n );\n } else {\n const match = phones.some((p) => digits(p) === digits(schemaPhone));\n out.push(\n check(\n \"schema-phone-matches\",\n \"One phone number, everywhere\",\n WHY_PHONE,\n \"quick\",\n match,\n match\n ? `${schemaPhone}, matching the page`\n : `${schemaPhone} in schema, not among the numbers on your pages`,\n ),\n );\n }\n\n // Self-serving reviews are INELIGIBLE FOR RICH RESULTS — they are not a\n // penalty, and calling them one would be exactly the overstatement this\n // report cannot afford. A Review nested under a Product or a named third\n // party is fine; one hanging off the organisation's own block is not.\n const WHY_REVIEW =\n \"Google does not show star ratings a business publishes about itself, so the markup does nothing but sit there.\";\n const selfReview = nodes.filter(\n (n) => isType(n, [\"AggregateRating\", \"Review\"]) && org !== undefined,\n );\n const selfServing = org\n ? selfReview.filter(() => org[\"aggregateRating\"] !== undefined || org[\"review\"] !== undefined)\n : [];\n if (!org || selfReview.length === 0) {\n out.push(\n skip(\n \"schema-self-review\",\n \"Ratings you publish about yourself\",\n WHY_REVIEW,\n \"content\",\n \"no review or rating markup on this site\",\n ),\n );\n } else {\n out.push(\n check(\n \"schema-self-review\",\n \"Ratings you publish about yourself\",\n WHY_REVIEW,\n \"content\",\n selfServing.length === 0,\n selfServing.length === 0\n ? \"your rating markup is attached to something other than your own organisation\"\n : `an aggregateRating sits on your own ${typeOf(org)[0]} block`,\n ),\n );\n }\n\n return out;\n}\n\n// ─── Response headers ────────────────────────────────────────────────────────\n\n/**\n * The six headers, one check each.\n *\n * Split from a single aggregate deliberately. As one row, \"4 of 6 security\n * headers\" is a grade nobody can act on; as six rows it is four green lines and\n * two named one-line fixes, and the reader can see which. Every one of them is\n * a line in a config file, which is why they are all `quick`.\n */\nconst SECURITY_HEADER_CHECKS: {\n header: string;\n label: string;\n why: string;\n /**\n * Optional: whether the VALUE actually restricts anything.\n *\n * Presence alone is a weak test, and on one header it was no test at all —\n * `Permissions-Policy: geolocation=*` behaves identically to sending no\n * header, and passed. A check a site can green by shipping a no-op is not a\n * check.\n *\n * `referrer-policy` had the same hole and has been pulled out of this list\n * entirely — see `referrerPolicyCheck`, where absence is not a finding at all.\n * The remaining four are presence-only: `content-security-policy: default-src\n * *` and `x-frame-options: ALLOWALL` are no-ops that would pass, and both\n * need their own browser check before they get a rule here. Writing one from\n * the specification is how the `permissions-policy` wording went wrong in the\n * first place.\n */\n restricts?: (value: string) => boolean;\n}[] = [\n {\n header: \"strict-transport-security\",\n label: \"Telling browsers to always use https\",\n why: \"Without it, the first visit of the day can still be made over plain http before the redirect happens.\",\n },\n {\n header: \"content-security-policy\",\n label: \"A policy for what may run on your pages\",\n why: \"It is the difference between a compromised script running on your site and being blocked by the browser.\",\n },\n {\n header: \"x-content-type-options\",\n label: \"Stopping browsers guessing file types\",\n why: \"One line that prevents a browser treating an uploaded file as something executable.\",\n },\n {\n header: \"x-frame-options\",\n label: \"Stopping other sites framing yours\",\n why: \"Without it, anyone can put your site inside theirs and collect what visitors type into it.\",\n },\n {\n header: \"permissions-policy\",\n label: \"Declaring what your pages may ask for\",\n // NOT \"it stops an embedded third party quietly asking for their camera\",\n // which is what this said and is not true. Tested in a browser: a\n // cross-origin iframe is refused camera and geolocation with no header set\n // at all, and is GRANTED them with no header set the moment the site writes\n // `allow=\"geolocation\"` on the iframe itself. The gate is the attribute the\n // site author types, so nothing here happens quietly. What the header adds\n // is the ability to say no to code already running on the page — including\n // code the owner did not write, if a dependency is ever compromised.\n why: \"It limits what code running on your own pages may ask visitors for — including code you did not write, if a dependency is ever compromised.\",\n // At least one directive whose allowlist is not `*`. `camera=()` denies it\n // outright, `camera=(self)` keeps it to your own code, `camera=*` grants it\n // to anything the page delegates to — which is the browser default, so a\n // header made only of those changes nothing.\n restricts: (value) =>\n value\n .split(\",\")\n .map((d) => d.trim())\n .filter(Boolean)\n .some((d) => {\n const eq = d.indexOf(\"=\");\n if (eq < 0) return false;\n const allow = d.slice(eq + 1).trim();\n return allow !== \"*\";\n }),\n },\n];\n\n/** A `server` or `x-powered-by` that names a version. The header on its own is\n * ordinary; the version number is the part that tells an attacker which list\n * of known holes to work through. */\nconst VERSIONED = /\\d+\\.\\d+/;\n\n/**\n * Two values leak MORE than sending nothing. Everything else, absence included,\n * is fine.\n *\n * This used to be a presence check whose `why` read \"by default the full URL a\n * visitor came from is handed to every site they click through to\". Measured in\n * a browser, from a page at `/some/deep/path?secret=token123`, a cross-origin\n * request carries:\n *\n * no header Referer: http://host/ ← origin only\n * strict-origin-when-cross-origin Referer: http://host/\n * origin-when-cross-origin Referer: http://host/\n * same-origin / no-referrer Referer: (none)\n * no-referrer-when-downgrade Referer: http://host/some/deep/path?secret=token123\n * unsafe-url Referer: http://host/some/deep/path?secret=token123\n *\n * So the old `why` described the pre-2020 default, and the old check failed\n * every site that simply left the header alone while passing the two that\n * actually spill the query string. Both halves were backwards.\n *\n * Absence is `not-applicable`, not a pass: the site did nothing, and crediting\n * it for the browser's behaviour would be as dishonest as blaming it.\n */\nconst LEAKS_MORE_THAN_DEFAULT = new Set([\"unsafe-url\", \"no-referrer-when-downgrade\"]);\n\nfunction referrerPolicyCheck(headers: Record<string, string>, measured: boolean): SiteCheck {\n const LABEL = \"Not leaking more of your addresses than the browser already does\";\n const WHY =\n \"Browsers already trim the page address to your bare domain before handing it to a site a visitor clicks through to. Two values of this header undo that and send the whole address, query string included.\";\n if (!measured) return unknown(\"header-referrer-policy\", LABEL, WHY, \"quick\");\n const raw = headers[\"referrer-policy\"];\n if (raw === undefined) {\n return skip(\n \"header-referrer-policy\",\n LABEL,\n WHY,\n \"quick\",\n \"no referrer-policy header, so the browser's own default applies — which does not leak the address\",\n );\n }\n // A list is legal; the browser takes the last value it understands.\n const values = raw\n .split(\",\")\n .map((v) => v.trim().toLowerCase())\n .filter(Boolean);\n const effective = [...values].reverse().find((v) => KNOWN_REFERRER_POLICIES.has(v)) ?? null;\n if (effective === null) {\n return skip(\n \"header-referrer-policy\",\n LABEL,\n WHY,\n \"quick\",\n `referrer-policy names no value a browser recognises (“${raw.slice(0, 60)}”), so the default applies`,\n );\n }\n const leaks = LEAKS_MORE_THAN_DEFAULT.has(effective);\n return check(\n \"header-referrer-policy\",\n LABEL,\n WHY,\n \"quick\",\n !leaks,\n leaks\n ? `referrer-policy: ${effective} — that sends the full address, query string and all, to every site your visitors click through to`\n : `referrer-policy: ${effective}`,\n );\n}\n\nconst KNOWN_REFERRER_POLICIES = new Set([\n \"no-referrer\",\n \"no-referrer-when-downgrade\",\n \"origin\",\n \"origin-when-cross-origin\",\n \"same-origin\",\n \"strict-origin\",\n \"strict-origin-when-cross-origin\",\n \"unsafe-url\",\n]);\n\nfunction headerChecks(headers: Record<string, string>, measured: boolean): SiteCheck[] {\n const out: SiteCheck[] = [];\n\n for (const { header, label, why, restricts } of SECURITY_HEADER_CHECKS) {\n if (!measured) {\n out.push(unknown(`header-${header}`, label, why, \"quick\"));\n continue;\n }\n const value = headers[header];\n const present = value !== undefined;\n const ok = present && (restricts === undefined || restricts(value));\n out.push(\n check(\n `header-${header}`,\n label,\n why,\n \"quick\",\n ok,\n !present\n ? `no ${header} header`\n : ok\n ? `${header} is set`\n : `${header} is set, but every directive in it is “*” — which is what the browser does without the header`,\n ),\n );\n }\n\n out.push(referrerPolicyCheck(headers, measured));\n\n const WHY_LEAK =\n \"Naming the exact version you run tells anyone looking which published vulnerabilities to try first.\";\n if (!measured) {\n out.push(\n unknown(\"header-version-leak\", \"Not advertising your software versions\", WHY_LEAK, \"quick\"),\n );\n } else {\n const leaks = [\"server\", \"x-powered-by\"]\n .filter((h) => headers[h] && VERSIONED.test(headers[h]!))\n .map((h) => `${h}: ${headers[h]}`);\n out.push(\n check(\n \"header-version-leak\",\n \"Not advertising your software versions\",\n WHY_LEAK,\n \"quick\",\n leaks.length === 0,\n leaks.length === 0 ? \"no version numbers in your response headers\" : leaks.join(\", \"),\n ),\n );\n }\n\n const WHY_GZIP =\n \"Uncompressed HTML is roughly four times the bytes for the same page, paid for by every visitor on a phone.\";\n if (!measured) {\n out.push(unknown(\"header-compression\", \"Pages sent compressed\", WHY_GZIP, \"quick\"));\n } else {\n const enc = headers[\"content-encoding\"] ?? \"\";\n const ok = /gzip|br|zstd|deflate/i.test(enc);\n out.push(\n check(\n \"header-compression\",\n \"Pages sent compressed\",\n WHY_GZIP,\n \"quick\",\n ok,\n ok ? `content-encoding: ${enc}` : \"your HTML is served uncompressed\",\n ),\n );\n }\n\n // Conditional on HSTS existing at all: a site with no HSTS has already been\n // reported by the check above, and failing it twice reads as two problems.\n const WHY_MAXAGE =\n \"A max-age of a few minutes gives almost none of the protection the header exists for.\";\n const hsts = headers[\"strict-transport-security\"];\n const SIX_MONTHS = 15_552_000;\n if (!measured || !hsts) {\n out.push(\n skip(\n \"header-hsts-age\",\n \"An https policy that lasts\",\n WHY_MAXAGE,\n \"quick\",\n \"no strict-transport-security header to read a max-age from\",\n ),\n );\n } else {\n const m = /max-age\\s*=\\s*(\\d+)/i.exec(hsts);\n const age = m ? Number(m[1]) : 0;\n out.push(\n check(\n \"header-hsts-age\",\n \"An https policy that lasts\",\n WHY_MAXAGE,\n \"quick\",\n age >= SIX_MONTHS,\n `max-age=${age}${age >= SIX_MONTHS ? \"\" : \" — six months is the usual minimum\"}`,\n ),\n );\n }\n\n return out;\n}\n\n// ─── robots.txt and the sitemap ──────────────────────────────────────────────\n\nfunction sidecarChecks(crawl: CrawlResult, linkedPages: Set<string>): SiteCheck[] {\n const discoveredLinks = linkedPages.size;\n const out: SiteCheck[] = [];\n const robotsMeasured = crawl.sidecarErrors.robots === null;\n\n const WHY_DISALLOW =\n \"A blanket Disallow tells every search engine and every assistant to stay off the whole site. It is usually left over from a staging server.\";\n if (!robotsMeasured) {\n out.push(\n unknown(\n \"robots-not-blocking\",\n \"robots.txt not blocking your whole site\",\n WHY_DISALLOW,\n \"quick\",\n ),\n );\n } else if (crawl.robotsTxt === null) {\n // No robots.txt is not a defect — it means everything is allowed, which is\n // exactly what this check wants to be true.\n out.push(\n check(\n \"robots-not-blocking\",\n \"robots.txt not blocking your whole site\",\n WHY_DISALLOW,\n \"quick\",\n true,\n \"no robots.txt, so nothing is disallowed\",\n ),\n );\n } else {\n // Only a `Disallow: /` under a group that applies to everyone. A rule\n // aimed at one crawler is a policy, not an accident.\n const blocked = /user-agent\\s*:\\s*\\*[^]*?^\\s*disallow\\s*:\\s*\\/\\s*$/im.test(crawl.robotsTxt);\n out.push(\n check(\n \"robots-not-blocking\",\n \"robots.txt not blocking your whole site\",\n WHY_DISALLOW,\n \"quick\",\n !blocked,\n blocked ? \"Disallow: / applies to every crawler\" : \"nothing site-wide is disallowed\",\n ),\n );\n }\n\n const WHY_SITEMAP_LINE =\n \"Naming the sitemap in robots.txt is how a crawler that has never seen your site finds the list of your pages.\";\n if (!robotsMeasured || crawl.robotsTxt === null) {\n out.push(\n skip(\n \"robots-names-sitemap\",\n \"robots.txt naming your sitemap\",\n WHY_SITEMAP_LINE,\n \"quick\",\n robotsMeasured ? \"this site has no robots.txt\" : \"we could not read robots.txt on this run\",\n ),\n );\n } else {\n const named = /^\\s*sitemap\\s*:/im.test(crawl.robotsTxt);\n out.push(\n check(\n \"robots-names-sitemap\",\n \"robots.txt naming your sitemap\",\n WHY_SITEMAP_LINE,\n \"quick\",\n named,\n named ? \"a Sitemap: line is present\" : \"no Sitemap: line in robots.txt\",\n ),\n );\n }\n\n const sitemapMeasured = crawl.sidecarErrors.sitemap === null;\n const WHY_COVERAGE =\n \"A sitemap listing fewer pages than your own navigation links to leaves the rest to be found by luck.\";\n if (!sitemapMeasured || !crawl.sitemap.present) {\n out.push(\n skip(\n \"sitemap-coverage\",\n \"A sitemap that lists your pages\",\n WHY_COVERAGE,\n \"content\",\n sitemapMeasured\n ? \"this site has no sitemap.xml\"\n : \"we could not read the sitemap on this run\",\n ),\n );\n } else if (discoveredLinks === 0) {\n out.push(\n unknown(\"sitemap-coverage\", \"A sitemap that lists your pages\", WHY_COVERAGE, \"content\"),\n );\n } else if (crawl.sitemap.truncated !== false) {\n // A COUNT WE DID NOT FINISH READING CANNOT BECOME A FINDING.\n //\n // The guard below already refuses to name pages against a truncated\n // SAMPLE. This is the truncation one level up: a sitemap index whose\n // children we stopped following. viget.com publishes 35 of them holding\n // 1,636 URLs; we followed the first few, counted 66, and reported that 179\n // of their own pages were missing from their sitemap. Every number in that\n // sentence was ours.\n //\n // `undefined` lands here too, and deliberately. It means the run predates\n // this field, so we do not know whether the read was complete — and \"we do\n // not know\" is not \"we read the whole thing\". A flat sitemap, which is what\n // almost every small business publishes, records `false` and is unaffected.\n out.push(\n unknown(\"sitemap-coverage\", \"A sitemap that lists your pages\", WHY_COVERAGE, \"content\"),\n );\n } else {\n // Compared against pages WE found links to, never against our own crawl\n // cap. \"We read 14 of your pages\" is a fact about our limit, and using it\n // as the denominator would report our ceiling as their gap.\n const ok = crawl.sitemap.urlCount >= discoveredLinks;\n // When the crawl carried the WHOLE sitemap, a count is not the best we can\n // do — we can name a page it leaves out, and a named page is a job where a\n // count is only a worry. Only when it is whole: against a truncated sample\n // an absent URL may be one we did not carry, and printing that as their\n // omission is our own ceiling reported as their gap for the second time in\n // the same check.\n const sample = crawl.sitemap.sample;\n const whole = sample !== undefined && crawl.sitemap.urlCount <= sample.length;\n // Both sides keyed by `norm2`: `linkedPages` already is, and a sitemap that\n // lists `http://`, `www.` or `%26` where the pages link `https://`, bare or\n // `&` is the same list. Keying only one side named whole sites as missing.\n const listed = new Set(sample?.map((u) => norm2(u) ?? u));\n const unlisted = whole ? [...linkedPages].filter((a) => !listed.has(a)) : [];\n out.push(\n check(\n \"sitemap-coverage\",\n \"A sitemap that lists your pages\",\n WHY_COVERAGE,\n \"content\",\n ok && unlisted.length === 0,\n unlisted.length > 0\n ? `${unlisted.length} ${unlisted.length === 1 ? \"page your own links point to is\" : \"pages your own links point to are\"} missing from it — ${unlisted[0]}`\n : `${crawl.sitemap.urlCount} URLs listed, against ${discoveredLinks} distinct pages your own links point to`,\n ),\n );\n }\n\n // REPORTED, NEVER FAILED — and this is a reconciliation, not a softening.\n //\n // `checks.ts` already took llms.txt out of the Findability score, on the\n // grounds that \"no answer engine has documented consuming llms.txt to build\n // an answer, and Google has publicly dismissed it. Scoring a proposal nobody\n // has committed to reading, and then grading a prospect down for not having\n // one, is the one place this audit asserted more than it knew.\"\n //\n // A red row in the battery IS a grade-down. We removed it from the number and\n // left it in the list, under a `why` calling llms.txt \"the one file that tells\n // an assistant what you do\" — the exact claim the score comment says we cannot\n // make. Having one is worth noting. Not having one is not a finding, and until\n // an engine documents reading it, saying otherwise sells a fix that does\n // nothing.\n const WHY_LLMS =\n \"A short llms.txt states, in your own words, what you do and which pages matter. No answer engine has published that it reads one, so this is recorded rather than counted against you.\";\n const llmsMeasured = crawl.sidecarErrors.llms === null;\n if (!llmsMeasured) {\n out.push(unknown(\"llms-txt\", \"A file written for AI assistants\", WHY_LLMS, \"content\"));\n } else if (crawl.llmsTxt.present) {\n out.push(\n check(\n \"llms-txt\",\n \"A file written for AI assistants\",\n WHY_LLMS,\n \"content\",\n true,\n crawl.llmsTxt.firstLine ?? \"present\",\n ),\n );\n } else {\n out.push(\n skip(\n \"llms-txt\",\n \"A file written for AI assistants\",\n WHY_LLMS,\n \"content\",\n \"no llms.txt — recorded, not counted against you, because no engine has published that it reads one\",\n ),\n );\n }\n\n return out;\n}\n\n// ─── Analytics ───────────────────────────────────────────────────────────────\n\n/** Scripts a site must be serving before \"all of them are ours\" reads as an\n * engineering operation with its own telemetry rather than a simple site. See\n * `analyticsCheck` — a judgement call, stated as one. */\nconst FIRST_PARTY_BUNDLE = 10;\n\nconst ANALYTICS_MARKERS =\n /(googletagmanager\\.com|google-analytics\\.com|plausible\\.io|usefathom\\.com|clarity\\.ms|segment\\.(?:com|io)|matomo|piwik|statcounter|hotjar\\.com|posthog\\.com|umami|simpleanalytics|cloudflareinsights\\.com)/i;\n\nfunction analyticsCheck(pages: { url: string; extract: PageExtract }[]): SiteCheck {\n const WHY =\n \"Without any analytics you cannot tell whether a change helped, which visit turned into an enquiry, or whether the work was worth doing.\";\n const LABEL = \"Something measuring whether any of this works\";\n const readable = pages.filter((p) => p.extract.scriptSrcs !== undefined);\n if (readable.length === 0) return unknown(\"analytics\", LABEL, WHY, \"quick\");\n\n // Loaded scripts AND hosts named inside inline ones, because a growing share\n // of careful sites defer their tags. reddoorla.com injects gtag.js only after\n // the first pointer or scroll, for privacy — so at crawl time there is no\n // analytics `src` in the DOM at all, and reading that as \"this site measures\n // nothing\" was our blindness printed as their defect. It is exactly the\n // client who did the considerate thing who would have received that line.\n const loaded = readable.flatMap((p) => p.extract.scriptSrcs ?? []);\n const named = readable.flatMap((p) => p.extract.inlineScriptUrls ?? []);\n const hit = loaded.find((s) => ANALYTICS_MARKERS.test(s));\n const deferred = hit ? undefined : named.find((h) => ANALYTICS_MARKERS.test(h));\n\n // WHAT THIS CHECK CANNOT SEE, AND WHERE THAT MATTERS.\n //\n // Analytics is only visible here when it is somebody else's script. A site\n // that ships its own bundle and reports to its own endpoint from inside it is\n // invisible to any amount of markup reading. apple.com is exactly that: 47\n // scripts, every one first-party, no third-party host anywhere — and we told\n // it that it measures nothing.\n //\n // The discriminator below is a judgement call and worth naming as one. A\n // large all-first-party bundle is an engineering operation that plausibly has\n // its own telemetry; one hand-written `main.js` on a plumber's website is\n // not, and \"no analytics\" is a sound and useful reading there. Ten is the\n // line, chosen because it separates those two cases and nothing more\n // principled was available.\n //\n // The direction of the error is deliberate: we lose the finding on a handful\n // of very large sites, rather than accuse a capable client of flying blind\n // when they are not. They would check, and they would be right.\n //\n // Judged on LOADED scripts only. `inlineScriptUrls` catches deferred loaders,\n // which is why the markers are tested against it above — but it also picks up\n // any URL that happens to sit in a template string, so it is far too noisy to\n // answer \"does this site run anybody else's code\". apple.com had exactly two\n // such URLs, a support link and an SVG namespace, and requiring none of them\n // kept this guard switched off.\n const thirdParty = loaded.filter((src) => /^https?:\\/\\//i.test(src));\n if (!hit && !deferred && thirdParty.length === 0 && loaded.length >= FIRST_PARTY_BUNDLE) {\n return unknown(\"analytics\", LABEL, WHY, \"quick\");\n }\n\n return check(\n \"analytics\",\n LABEL,\n WHY,\n \"quick\",\n hit !== undefined || deferred !== undefined,\n hit ??\n (deferred\n ? `${deferred}, loaded on first interaction`\n : \"no third-party analytics on the pages we read\"),\n );\n}\n\n// ─── Metas ───────────────────────────────────────────────────────────────────\n\n/**\n * What the page tells search engines and assistants about itself.\n *\n * `<meta name=\"robots\">` is the highest-value check in the whole battery. A\n * site ships `noindex` from staging, loses everything, and nothing on the page\n * looks wrong — it is invisible to the owner and instantly visible to us.\n *\n * Deliberately NOT here: the viewport-zoom check. axe's `meta-viewport` rule\n * tests exactly that (`user-scalable=no`, `maximum-scale` under 2) against the\n * rendered DOM, and asking the same question twice under two headings reads as\n * two problems.\n */\nfunction metaChecks(pages: { url: string; extract: PageExtract }[]): SiteCheck[] {\n const out: SiteCheck[] = [];\n const readable = pages.filter((p) => p.extract.metas !== undefined);\n\n const WHY_NOINDEX =\n \"This one line tells every search engine and assistant to leave the page out of their results entirely. It is almost always left over from a staging site, and nothing on the page looks wrong.\";\n const WHY_NOFOLLOW =\n \"It tells search engines not to follow any link on the page, so nothing you link to gets credit from it.\";\n const WHY_CHARSET =\n \"Without a declared character set the browser guesses, and a wrong guess is what turns apostrophes into ’.\";\n\n if (readable.length === 0) {\n return [\n unknown(\"meta-noindex\", \"Pages search engines are allowed to list\", WHY_NOINDEX, \"quick\"),\n unknown(\"meta-nofollow\", \"Links search engines are allowed to follow\", WHY_NOFOLLOW, \"quick\"),\n unknown(\"meta-charset\", \"A declared character set\", WHY_CHARSET, \"quick\"),\n unknown(\n \"title-length\",\n \"Titles that survive a search result\",\n \"A title that is too long is cut off mid-word in the result, and one that is too short wastes the most valuable line you get.\",\n \"content\",\n ),\n unknown(\n \"description-length\",\n \"Descriptions that survive a search result\",\n \"The description is the sentence under your link. Too long and it is truncated; too short and the engine writes its own.\",\n \"content\",\n ),\n unknown(\n \"og-image-absolute\",\n \"A share image with a full web address\",\n \"A relative og:image renders no card at all on Slack, LinkedIn or iMessage — the link arrives as bare text.\",\n \"quick\",\n ),\n unknown(\n \"duplicate-descriptions\",\n \"A different description on each page\",\n \"Identical descriptions make every page look like the same page in a search result.\",\n \"content\",\n ),\n ];\n }\n\n const flagged = (token: RegExp): { url: string; value: string }[] =>\n readable\n .filter((p) => token.test(p.extract.metas?.[\"robots\"] ?? \"\"))\n .map((p) => ({ url: p.url, value: p.extract.metas![\"robots\"]! }));\n\n const noindex = flagged(/\\bnoindex\\b/i);\n out.push(\n check(\n \"meta-noindex\",\n \"Pages search engines are allowed to list\",\n WHY_NOINDEX,\n \"quick\",\n noindex.length === 0,\n noindex.length === 0\n ? `none of the ${readable.length} pages we read asks to be hidden`\n : `${noindex.length} ${noindex.length === 1 ? \"page carries\" : \"pages carry\"} noindex: ${noindex\n .slice(0, 3)\n .map((n) => n.url)\n .join(\", \")}`,\n ),\n );\n\n const nofollow = flagged(/\\bnofollow\\b/i);\n out.push(\n check(\n \"meta-nofollow\",\n \"Links search engines are allowed to follow\",\n WHY_NOFOLLOW,\n \"quick\",\n nofollow.length === 0,\n nofollow.length === 0\n ? \"no page-wide nofollow\"\n : `${nofollow.length} ${nofollow.length === 1 ? \"page\" : \"pages\"}`,\n ),\n );\n\n const noCharset = readable.filter((p) => !p.extract.metas?.[\"charset\"]);\n out.push(\n check(\n \"meta-charset\",\n \"A declared character set\",\n WHY_CHARSET,\n \"quick\",\n noCharset.length === 0,\n noCharset.length === 0\n ? `declared on all ${readable.length} pages`\n : `missing on ${noCharset.length} ${noCharset.length === 1 ? \"page\" : \"pages\"}`,\n ),\n );\n\n // Generous bands, on purpose. Sixty characters is where Google truncates by\n // PIXEL width, not by character count, so a hard 60 fails a great many\n // perfectly good titles. Only the genuinely broken ends are flagged.\n const TITLE_MIN = 10;\n const TITLE_MAX = 70;\n const titled = pages.filter((p) => p.extract.title);\n if (titled.length === 0) {\n out.push(\n skip(\n \"title-length\",\n \"Titles that survive a search result\",\n \"A title that is too long is cut off mid-word in the result, and one that is too short wastes the most valuable line you get.\",\n \"content\",\n \"no page we read carries a title, which is reported separately\",\n ),\n );\n } else {\n const bad = titled.filter(\n (p) => p.extract.title!.length < TITLE_MIN || p.extract.title!.length > TITLE_MAX,\n );\n out.push(\n check(\n \"title-length\",\n \"Titles that survive a search result\",\n \"A title that is too long is cut off mid-word in the result, and one that is too short wastes the most valuable line you get.\",\n \"content\",\n bad.length === 0,\n bad.length === 0\n ? `all ${titled.length} are between ${TITLE_MIN} and ${TITLE_MAX} characters`\n : bad\n .slice(0, 2)\n .map((p) => `${p.url}: ${p.extract.title!.length} characters`)\n .join(\", \"),\n ),\n );\n }\n\n // Wide on purpose, and wider than it was. The band exists to catch a stub\n // (\"Home\") and a wall of text that is cut off mid-sentence — not to enforce a\n // character budget, because truncation is by PIXEL width and no character\n // count predicts it. At 50–170 apple.com failed twice: a 172-character home\n // page and a 48-character specs page, both by two characters, and both\n // perfectly good descriptions. That is the same mistake the title band\n // already avoids by running 10–70 rather than clamping at 60.\n const DESC_MIN = 40;\n const DESC_MAX = 200;\n // Over the pages the site publishes — one per declared address, none it hides\n // from search. See `contentPages`.\n const described = contentPages(pages).filter((p) => p.extract.metaDescription);\n if (described.length === 0) {\n out.push(\n skip(\n \"description-length\",\n \"Descriptions that survive a search result\",\n \"The description is the sentence under your link. Too long and it is truncated; too short and the engine writes its own.\",\n \"content\",\n \"no page we read carries a description, which is reported separately\",\n ),\n );\n } else {\n const bad = described.filter(\n (p) =>\n p.extract.metaDescription!.length < DESC_MIN ||\n p.extract.metaDescription!.length > DESC_MAX,\n );\n out.push(\n check(\n \"description-length\",\n \"Descriptions that survive a search result\",\n \"The description is the sentence under your link. Too long and it is truncated; too short and the engine writes its own.\",\n \"content\",\n bad.length === 0,\n bad.length === 0\n ? `all ${described.length} are between ${DESC_MIN} and ${DESC_MAX} characters`\n : bad\n .slice(0, 2)\n .map((p) => `${p.url}: ${p.extract.metaDescription!.length} characters`)\n .join(\", \"),\n ),\n );\n }\n\n const WHY_OG =\n \"A relative og:image renders no card at all on Slack, LinkedIn or iMessage — the link arrives as bare text.\";\n const withOg = pages.filter((p) => p.extract.social[\"og:image\"]);\n if (withOg.length === 0) {\n out.push(\n skip(\n \"og-image-absolute\",\n \"A share image with a full web address\",\n WHY_OG,\n \"quick\",\n \"no page we read declares an og:image\",\n ),\n );\n } else {\n const relative = withOg.filter((p) => !/^https?:\\/\\//i.test(p.extract.social[\"og:image\"]!));\n out.push(\n check(\n \"og-image-absolute\",\n \"A share image with a full web address\",\n WHY_OG,\n \"quick\",\n relative.length === 0,\n relative.length === 0\n ? withOg[0]!.extract.social[\"og:image\"]!\n : `${relative[0]!.url} declares “${relative[0]!.extract.social[\"og:image\"]}”, which has no host`,\n ),\n );\n }\n\n const WHY_DUP_DESC =\n \"Identical descriptions make every page look like the same page in a search result, and the engine usually replaces them with a sentence of its own choosing.\";\n // One entry per declared address, for the same reason `h1-distinct` folds:\n // the same page fetched under five query strings is not five pages sharing a\n // description. See `byDeclaredAddress`.\n const distinctDescribed = byDeclaredAddress(described);\n if (distinctDescribed.length < 2) {\n out.push(\n skip(\n \"duplicate-descriptions\",\n \"A different description on each page\",\n WHY_DUP_DESC,\n \"content\",\n described.length < 2\n ? \"fewer than two pages carry a description, so there is nothing to compare\"\n : \"the pages we read that carry a description all declare the same address, so there is nothing to compare\",\n ),\n );\n } else {\n const seen = new Map<string, string[]>();\n for (const p of distinctDescribed) {\n const key = p.extract.metaDescription!.trim().toLowerCase();\n seen.set(key, [...(seen.get(key) ?? []), p.url]);\n }\n const dupes = [...seen.entries()].filter(([, urls]) => urls.length > 1);\n out.push(\n check(\n \"duplicate-descriptions\",\n \"A different description on each page\",\n WHY_DUP_DESC,\n \"content\",\n dupes.length === 0,\n dupes.length === 0\n ? `${distinctDescribed.length} pages, ${seen.size} different descriptions`\n : `${dupes[0]![1].length} pages share one description`,\n ),\n );\n }\n\n return out;\n}\n\n// ─── The <link> set ──────────────────────────────────────────────────────────\n\nfunction linkChecks(\n pages: { url: string; extract: PageExtract }[],\n origin: string,\n /** Whether the Tier 2 probe reached a verdict on the icon itself. */\n faviconProbed: boolean,\n): SiteCheck[] {\n const readable = pages.filter((p) => p.extract.links !== undefined);\n const WHY_FAVICON =\n \"Without one the browser shows a blank page icon in the tab and the bookmark, which is the smallest possible thing to fix and the most often seen.\";\n const WHY_CANON_SELF =\n \"A canonical naming another page tells search engines to index that page instead of this one. That is right for a genuine duplicate, and the fastest way to remove real pages from search results when it is not.\";\n const WHY_CANON_ORIGIN =\n \"A canonical on another domain hands every page's search ranking to that domain. It is usually a staging host or a site they used to own.\";\n const WHY_HREFLANG =\n \"An hreflang set that does not name itself is ignored, so the translated pages compete with each other instead of being offered to the right reader.\";\n\n if (readable.length === 0) {\n return [\n unknown(\"favicon-declared\", \"An icon for the browser tab\", WHY_FAVICON, \"quick\"),\n unknown(\"canonical-self\", \"Pages that point at themselves\", WHY_CANON_SELF, \"structural\"),\n unknown(\n \"canonical-origin\",\n \"Canonicals that stay on your domain\",\n WHY_CANON_ORIGIN,\n \"structural\",\n ),\n unknown(\"hreflang-self\", \"Language alternates that name themselves\", WHY_HREFLANG, \"content\"),\n ];\n }\n\n const out: SiteCheck[] = [];\n const icon = readable\n .flatMap((p) => p.extract.links ?? [])\n .find((l) => /(^|\\s)(icon|shortcut icon|apple-touch-icon)(\\s|$)/.test(l.rel));\n // SUPERSEDED once we have actually fetched the icon.\n //\n // Declaring `<link rel=\"icon\">` is optional: `/favicon.ico` at the root is\n // the older convention and every browser asks for it unprompted. apple.com\n // declares no icon link at all and its tab icon is perfect — and our own\n // report said so on the very next line, failing \"an icon for the browser\n // tab\" directly above passing \"a favicon that actually loads\". Two answers to\n // one question, and the wrong one first.\n //\n // So when the probe reached a verdict, `favicon-served` owns this and the\n // declaration is a detail, not a finding. Without a probe — an old stored\n // report — the declaration is the best evidence there is and still stands.\n out.push(\n faviconProbed\n ? skip(\n \"favicon-declared\",\n \"An icon for the browser tab\",\n WHY_FAVICON,\n \"quick\",\n icon\n ? \"the icon your pages declare is the one we fetched, reported below\"\n : \"no icon is declared, so we fetched /favicon.ico instead — reported below\",\n )\n : check(\n \"favicon-declared\",\n \"An icon for the browser tab\",\n WHY_FAVICON,\n \"quick\",\n icon !== undefined,\n icon ? `rel=\"${icon.rel}\" → ${icon.href}` : \"no icon link on the pages we read\",\n ),\n );\n\n const host = (u: string, base?: string): string | null => {\n try {\n return new URL(u, base).hostname.replace(/^www\\./i, \"\").toLowerCase();\n } catch {\n return null;\n }\n };\n const ownHost = host(origin);\n\n const canonical = pages.filter((p) => p.extract.canonical);\n if (canonical.length === 0) {\n out.push(\n skip(\n \"canonical-self\",\n \"Pages that point at themselves\",\n WHY_CANON_SELF,\n \"structural\",\n \"no page we read declares a canonical, which is reported separately\",\n ),\n skip(\n \"canonical-origin\",\n \"Canonicals that stay on your domain\",\n WHY_CANON_ORIGIN,\n \"structural\",\n \"no page we read declares a canonical\",\n ),\n );\n } else {\n // Compared path-wise and host-wise, ignoring a trailing slash and a `www.`,\n // because those differences are not what this check is about and flagging\n // them would bury the one case that matters.\n const norm = (u: string, base: string): string | null => {\n try {\n const parsed = new URL(u, base);\n return `${parsed.hostname.replace(/^www\\./i, \"\").toLowerCase()}${\n parsed.pathname.replace(/\\/+$/, \"\") || \"/\"\n }`;\n } catch {\n return null;\n }\n };\n const notSelf = canonical.filter((p) => {\n const a = norm(p.extract.canonical!, p.url);\n const b = norm(p.url, p.url);\n return a === null || b === null || a !== b;\n });\n\n // A page naming ANOTHER page canonical is not a defect — it is what a\n // canonical is FOR, and it is how a competent site handles a duplicate.\n // apple.com serves one comparison page at `/airpods-4/compare/`,\n // `/airpods-max/compare/` and `/airpods-pro/compare/`, all three correctly\n // naming `/airpods/compare/` and all three headlined \"Compare AirPods\n // models\". We failed it twice for doing the right thing: first for pointing\n // away at all, then, after a first attempt at a threshold, for doing it\n // three times.\n //\n // COUNTING CANNOT SEPARATE THESE. \"Many pages, one target\" is the shape of\n // a proper consolidation AND of the classic wipeout; the difference is\n // whether the pages really are the same page. So the headline is the\n // instrument, and there are three ways for it to say no.\n //\n // 1. The named page is one we also read, and carries a different\n // headline. The page has told search engines to index something it is\n // not. This is what catches every-page-names-the-home-page.\n //\n // 2. Several pages name the same target and do not agree on a headline\n // among themselves. They cannot all be duplicates of one page, so the\n // canonical is a constant where an address belongs — and this needs no\n // access to the target at all.\n //\n // 3. Three or more pages, and every single one names one same address.\n // A site cannot be one page served at every address, whatever its\n // headlines say. The backstop for a template that also writes one\n // headline everywhere.\n //\n // A lone page pointing at a target we never read is none of the three, and\n // stays a pass: our gap is not their defect.\n const soleH1 = (p: { extract: PageExtract }): string | null => {\n const h1s = p.extract.headings.filter((h) => h.level === 1);\n return h1s.length === 1 ? h1s[0]!.text.trim().toLowerCase() : null;\n };\n const byAddress = new Map<string, { url: string; extract: PageExtract }>();\n for (const p of pages) {\n const key = norm(p.url, p.url);\n if (key !== null && !byAddress.has(key)) byAddress.set(key, p);\n }\n\n // (1) contradicted by the page it names\n const contradicted = notSelf.filter((p) => {\n const target = norm(p.extract.canonical!, p.url);\n const other = target === null ? undefined : byAddress.get(target);\n if (other === undefined) return false;\n const mine = soleH1(p);\n const theirs = soleH1(other);\n return mine !== null && theirs !== null && mine !== theirs;\n });\n\n // (2) claimants of one target that are not the same page as each other\n const byTarget = new Map<string, { url: string; extract: PageExtract }[]>();\n for (const p of notSelf) {\n const t = norm(p.extract.canonical!, p.url);\n if (t !== null) byTarget.set(t, [...(byTarget.get(t) ?? []), p]);\n }\n const disagreeing = [...byTarget.entries()].filter(([, claimants]) => {\n const heads = claimants.map(soleH1).filter((h): h is string => h !== null);\n return heads.length > 1 && new Set(heads).size > 1;\n });\n\n // (3) the whole crawl pointing at one address\n const WIPEOUT_AT = 3;\n const wipeout =\n canonical.length >= WIPEOUT_AT && notSelf.length === canonical.length && byTarget.size === 1;\n\n const collapsing = contradicted.length > 0 || disagreeing.length > 0 || wipeout;\n out.push(\n check(\n \"canonical-self\",\n \"Pages that point at themselves\",\n WHY_CANON_SELF,\n \"structural\",\n !collapsing,\n collapsing\n ? contradicted.length > 0\n ? `${contradicted[0]!.url} names ${contradicted[0]!.extract.canonical} canonical, and the two carry different headlines`\n : wipeout\n ? `all ${canonical.length} pages we read name ${[...byTarget.keys()][0]} canonical`\n : `${disagreeing[0]![1].length} pages with different headlines all name ${disagreeing[0]![0]} canonical`\n : notSelf.length === 0\n ? `all ${canonical.length} canonicals point at their own page`\n : `${canonical.length - notSelf.length} of ${canonical.length} point at their own page; the ${notSelf.length === 1 ? \"other names a page it duplicates\" : `other ${notSelf.length} name a page they duplicate`}`,\n ),\n );\n\n const offOrigin = canonical.filter((p) => {\n const h = host(p.extract.canonical!, p.url);\n return h !== null && h !== ownHost;\n });\n out.push(\n check(\n \"canonical-origin\",\n \"Canonicals that stay on your domain\",\n WHY_CANON_ORIGIN,\n \"structural\",\n offOrigin.length === 0,\n offOrigin.length === 0\n ? \"every canonical stays on this domain\"\n : `${offOrigin[0]!.url} points at ${offOrigin[0]!.extract.canonical}`,\n ),\n );\n }\n\n // Conditional by nature: a monolingual site has no alternates, and passing it\n // would be five free points for something it does not do.\n const alternates = readable.filter((p) =>\n (p.extract.links ?? []).some((l) => l.rel === \"alternate\" && l.hreflang),\n );\n if (alternates.length === 0) {\n out.push(\n skip(\n \"hreflang-self\",\n \"Language alternates that name themselves\",\n WHY_HREFLANG,\n \"content\",\n \"this site publishes no language alternates\",\n ),\n );\n } else {\n // A page that names another page canonical carries THAT page's alternate\n // set, and naming itself in it would be the mistake. apple.com's\n // `/airpods-4/compare/` lists one alternate — `/airpods/compare/`, the page\n // it declares itself a duplicate of — and we read the missing\n // self-reference as a broken hreflang set.\n const own = alternates.filter((p) => {\n const c = p.extract.canonical ? norm2(p.extract.canonical, p.url) : null;\n return c === null || c === norm2(p.url);\n });\n if (own.length === 0) {\n out.push(\n skip(\n \"hreflang-self\",\n \"Language alternates that name themselves\",\n WHY_HREFLANG,\n \"content\",\n \"every page we read that publishes alternates declares a different page canonical, so the set is not its own to name\",\n ),\n );\n return out;\n }\n const missingSelf = own.filter((p) => {\n const mine = norm2(p.url);\n return !(p.extract.links ?? []).some(\n (l) => l.rel === \"alternate\" && l.hreflang && norm2(l.href, p.url) === mine,\n );\n });\n out.push(\n check(\n \"hreflang-self\",\n \"Language alternates that name themselves\",\n WHY_HREFLANG,\n \"content\",\n missingSelf.length === 0,\n missingSelf.length === 0\n ? `${own.length} ${own.length === 1 ? \"page lists\" : \"pages list\"} themselves among their alternates`\n : `${missingSelf[0]!.url} is not among its own alternates`,\n ),\n );\n }\n\n return out;\n}\n\n/**\n * The address a page says it IS: its canonical when it declares one, its own\n * URL otherwise.\n *\n * Two pages sharing a declared address are ONE page, and comparing them to each\n * other invents findings. apple.com produced both shapes in a single crawl:\n * `/accessibility/features/` was fetched five times under `?vision`, `?hearing`\n * and three more tab deep-links, and `/airpods-4/compare/` and\n * `/airpods-max/compare/` both serve — and both correctly declare — the page at\n * `/airpods/compare/`. Compared as six pages they read as five sharing one\n * description and two sharing a headline. Folded, apple.com has neither\n * problem, and never did.\n *\n * `norm2` already drops the query string, which is what folds the deep links.\n * The canonical is what folds a duplicate served at a different path.\n */\nfunction declaredAddress(p: { url: string; extract: PageExtract }): string | null {\n return p.extract.canonical ? norm2(p.extract.canonical, p.url) : norm2(p.url);\n}\n\n/**\n * One entry per distinct declared address, keeping the first page to claim it.\n *\n * ONLY for the checks that compare pages to each other. It must never become\n * the crawl's page list: on a site where every page names the home page\n * canonical — the exact failure `canonical-self` exists to catch — this folds\n * the whole site to one entry. That is why every caller re-checks that it still\n * has two pages to compare and skips rather than passing when it does not.\n */\nfunction byDeclaredAddress<T extends { url: string; extract: PageExtract }>(pages: T[]): T[] {\n const seen = new Map<string, T>();\n for (const p of pages) {\n const key = declaredAddress(p) ?? p.url;\n if (!seen.has(key)) seen.set(key, p);\n }\n return [...seen.values()];\n}\n\n/** A page the site itself withdraws from search, by `<meta name=\"robots\">`.\n * Unknown — `metas` never captured, a report from before the field existed —\n * is NOT hidden: dropping a page on a guess would silence a real finding to\n * hide our own gap. */\nfunction hiddenFromSearch(extract: PageExtract): boolean {\n return /\\bnoindex\\b/i.test(extract.metas?.[\"robots\"] ?? \"\");\n}\n\n/** The same document under two addresses: title, description and headline all\n * agree. Declaring another page's canonical is not enough on its own — see\n * `contentPages`. */\nfunction sameDocument(a: { extract: PageExtract }, b: { extract: PageExtract }): boolean {\n const h1s = (e: PageExtract) =>\n e.headings\n .filter((h) => h.level === 1)\n .map((h) => h.text.trim())\n .join(\"\\n\");\n return (\n a.extract.title === b.extract.title &&\n a.extract.metaDescription === b.extract.metaDescription &&\n h1s(a.extract) === h1s(b.extract)\n );\n}\n\n/**\n * The address a page WE READ folds onto: the address it declares, when the\n * page at that address was also read and is the same document. Null when the\n * page stands on its own — it declares itself, or it declares a page we never\n * read, or it declares a page it does not resemble.\n */\nfunction aliasAddress<T extends { url: string; extract: PageExtract }>(\n p: T,\n read: Map<string, T>,\n): string | null {\n const declared = declaredAddress(p);\n if (!declared || declared === norm2(p.url)) return null;\n const target = read.get(declared);\n return target !== undefined && sameDocument(target, p) ? declared : null;\n}\n\n/**\n * The pages a content check may count — and only those: minus the pages the\n * site hides from search, and minus an alias of a page already counted.\n *\n * ALIASES. preveta.com and richardmacdonald.com serve `/home/` beside `/`, and\n * icovy.com an `/old-home-2`, each declaring `/` and reading exactly like it.\n * Counted apart they doubled every homepage finding — \"2 of 20 have no\n * headline\", both of them the same page.\n *\n * BUT ONLY A CONFIRMED ALIAS. sapidyne.com declares `/` as the canonical on 19\n * of its 20 pages, each with its own title and its own headline. That is the\n * defect `canonical-self` reports, not aliasing, and folding on the declaration\n * alone measured the whole site as one page — \"all 1 are between 40 and 200\n * characters\". A page folds only onto a page we read that reads the same\n * (`sameDocument`); otherwise it stands, and owes its own headline. This is\n * also why `byDeclaredAddress` — which folds on the declaration, for the checks\n * that compare pages to each other — must never become the crawl's page list.\n *\n * NOINDEX. A page the site withdraws from search owes it no headline, no\n * description and no sitemap entry. vascularperfusion.solutions was told its\n * `/cart` — which its own robots meta hides, and which the same report flagged\n * under meta-noindex — was missing from the sitemap. Two rows, opposite\n * complaints, one page. `meta-noindex` itself still reads every page.\n */\nfunction contentPages<T extends { url: string; extract: PageExtract }>(pages: T[]): T[] {\n const read = new Map<string, T>();\n for (const p of pages) {\n const at = norm2(p.url);\n if (at && !read.has(at)) read.set(at, p);\n }\n const counted = new Set<string>();\n const out: T[] = [];\n for (const p of pages) {\n if (hiddenFromSearch(p.extract)) continue;\n const at = aliasAddress(p, read) ?? norm2(p.url) ?? p.url;\n if (counted.has(at)) continue;\n counted.add(at);\n out.push(p);\n }\n return out;\n}\n\n/**\n * Host + path, trailing slash and `www.` ignored — the comparison every URL\n * equality check in this file wants. Scheme and query are dropped by\n * construction: richardmacdonald.com's sitemap lists every URL as `http://`\n * against an `https://` crawl, and a key that kept the scheme named all 52 of\n * their pages — the homepage first — as missing from it.\n *\n * Percent-escapes are decoded. coyote.us links to `/courses/arts-&-crafts-` and\n * its sitemap lists `/courses/arts-%26-crafts-`; those are one address to every\n * browser and every crawler, and comparing the raw strings reported that page\n * as missing from a sitemap it is in. A path that does not decode (a lone `%`)\n * is compared as written rather than thrown away.\n */\nfunction norm2(u: string, base?: string): string | null {\n try {\n const parsed = new URL(u, base);\n const path = parsed.pathname.replace(/\\/+$/, \"\") || \"/\";\n let decoded: string;\n try {\n decoded = decodeURIComponent(path);\n } catch {\n decoded = path;\n }\n return `${parsed.hostname.replace(/^www\\./i, \"\").toLowerCase()}${decoded}`;\n } catch {\n return null;\n }\n}\n\n// ─── Forms, in the detail that decides whether they get filled in ────────────\n\n/** Providers whose endpoint we recognise, so an action pointing at one is not\n * reported as a mystery. Deliberately incomplete — see the check. */\nconst FORM_PROVIDERS =\n /(formspree\\.io|hsforms\\.(?:net|com)|typeform\\.com|jotform\\.com|wufoo\\.com|formstack\\.com|getform\\.io|basin\\.com|netlify|web3forms\\.com|formsubmit\\.co|google\\.com\\/forms)/i;\n\nfunction formChecks(\n pages: { url: string; extract: PageExtract }[],\n /** What the Tier 4 probe watched a form on this site actually do, when it\n * reached a verdict. Markup is a prediction; this is an observation. */\n probes: (FormProbe | null | undefined)[] = [],\n): SiteCheck[] {\n const WHY_TYPES =\n \"An email field typed as plain text gives a phone the ordinary keyboard, so the visitor hunts for the @ at exactly the moment they had decided to write to you.\";\n const WHY_AUTOCOMPLETE =\n \"With these, a phone offers to fill the whole form in one tap. Without them it does not offer at all.\";\n const WHY_METHOD =\n \"A form that submits with GET puts everything the visitor typed into the address bar, and from there into your server logs and their browser history.\";\n const WHY_ACTION =\n \"A form posting to an endpoint nobody owns any more looks like it worked and silently goes nowhere.\";\n const WHY_REQUIRED =\n \"Without it the browser cannot stop a half-filled form being sent, and the visitor finds out something was missing only if somebody tells them.\";\n\n // A FORM WE WATCHED REFUSE AN EMPTY SUBMISSION is a form whose markup did not\n // govern the outcome — its JavaScript did.\n //\n // viget.com's contact form is marked `method=\"get\"` with nothing `required`,\n // and we pressed its button and watched it paint an error instead of\n // submitting. We then published, in the same report, that the browser cannot\n // stop a half-filled form being sent. Two answers to one question, and the\n // wrong one was the finding: the probe had already settled it.\n //\n // So the two markup lints that ONLY matter when the browser is doing the\n // submitting step aside when the probe says it is not. `form-field-types` and\n // `form-autocomplete` are untouched — a text input where an email belongs\n // still gives a phone the wrong keyboard however the form is sent.\n const watched = probes.filter((p): p is FormProbe => !!p);\n const jsControlled = new Set(\n watched.filter((p) => p.emptyRefused === true).map((p) => norm2(p.url) ?? p.url),\n );\n const scriptRuns = (url: string) => jsControlled.has(norm2(url) ?? url);\n\n const enquiries = pages.flatMap((p) =>\n (p.extract.forms ?? [])\n .filter((f) => f.kind === \"enquiry\")\n .map((f) => ({ form: f, url: p.url })),\n );\n const measured = enquiries.filter((e) => e.form.fields !== undefined);\n\n if (enquiries.length === 0) {\n const note = \"no enquiry form on the pages we read, which is reported separately\";\n return [\n skip(\"form-field-types\", \"Fields typed for a phone keyboard\", WHY_TYPES, \"content\", note),\n skip(\n \"form-autocomplete\",\n \"Fields a phone can fill in one tap\",\n WHY_AUTOCOMPLETE,\n \"content\",\n note,\n ),\n skip(\"form-method\", \"A form that does not put answers in the URL\", WHY_METHOD, \"quick\", note),\n skip(\n \"form-action\",\n \"A form that posts somewhere we recognise\",\n WHY_ACTION,\n \"structural\",\n note,\n ),\n skip(\"form-required\", \"Fields marked as required\", WHY_REQUIRED, \"quick\", note),\n ];\n }\n if (measured.length === 0) {\n return [\n unknown(\"form-field-types\", \"Fields typed for a phone keyboard\", WHY_TYPES, \"content\"),\n unknown(\n \"form-autocomplete\",\n \"Fields a phone can fill in one tap\",\n WHY_AUTOCOMPLETE,\n \"content\",\n ),\n unknown(\"form-method\", \"A form that does not put answers in the URL\", WHY_METHOD, \"quick\"),\n unknown(\"form-action\", \"A form that posts somewhere we recognise\", WHY_ACTION, \"structural\"),\n unknown(\"form-required\", \"Fields marked as required\", WHY_REQUIRED, \"quick\"),\n ];\n }\n\n const out: SiteCheck[] = [];\n const looksLikeEmail = (f: NonNullable<FormShape[\"fields\"]>[number]): boolean =>\n /e-?mail/i.test(`${f.name ?? \"\"} ${f.autocomplete ?? \"\"}`);\n const looksLikePhone = (f: NonNullable<FormShape[\"fields\"]>[number]): boolean =>\n /\\b(phone|tel|mobile)\\b/i.test(`${f.name ?? \"\"} ${f.autocomplete ?? \"\"}`);\n\n const mistyped = measured.flatMap(({ form, url }) =>\n (form.fields ?? [])\n .filter(\n (f) => (looksLikeEmail(f) && f.type !== \"email\") || (looksLikePhone(f) && f.type !== \"tel\"),\n )\n .map((f) => `${url}: ${f.name ?? \"a field\"} is type=\"${f.type}\"`),\n );\n out.push(\n check(\n \"form-field-types\",\n \"Fields typed for a phone keyboard\",\n WHY_TYPES,\n \"content\",\n mistyped.length === 0,\n mistyped.length === 0\n ? \"your email and phone fields use the right input types\"\n : mistyped.slice(0, 2).join(\", \"),\n ),\n );\n\n const withAuto = measured.filter(({ form }) => (form.fields ?? []).some((f) => f.autocomplete));\n out.push(\n check(\n \"form-autocomplete\",\n \"Fields a phone can fill in one tap\",\n WHY_AUTOCOMPLETE,\n \"content\",\n withAuto.length === measured.length,\n withAuto.length === measured.length\n ? `${countOf(measured.length, \"enquiry form\", \"enquiry forms\")} ${\n measured.length === 1 ? \"carries\" : \"carry\"\n } autocomplete`\n : `${measured.length - withAuto.length} of ${measured.length} carry none`,\n ),\n );\n\n const getForms = measured.filter(({ form, url }) => form.method !== \"post\" && !scriptRuns(url));\n const getButScripted = measured.filter(\n ({ form, url }) => form.method !== \"post\" && scriptRuns(url),\n );\n out.push(\n getForms.length === 0 && getButScripted.length > 0\n ? skip(\n \"form-method\",\n \"A form that does not put answers in the URL\",\n WHY_METHOD,\n \"quick\",\n `${getButScripted[0]!.url} is marked GET, but we watched its script handle the submit instead, so the attribute never decides`,\n )\n : check(\n \"form-method\",\n \"A form that does not put answers in the URL\",\n WHY_METHOD,\n \"quick\",\n getForms.length === 0,\n getForms.length === 0\n ? \"every enquiry form posts\"\n : `${getForms[0]!.url} submits with ${getForms[0]!.form.method.toUpperCase()}`,\n ),\n );\n\n // A provider we do not recognise is UNMEASURED, never a failure: our list will\n // always be incomplete, and calling a working in-house endpoint broken is\n // exactly the false alarm that costs a prospect's trust in everything else.\n const offsite = measured.filter(({ form, url }) => {\n const action = form.action;\n if (!action) return false;\n const h = norm2(action, url);\n const own = norm2(url);\n return h !== null && own !== null && h.split(\"/\")[0] !== own.split(\"/\")[0];\n });\n const unrecognised = offsite.filter(({ form }) => !FORM_PROVIDERS.test(form.action ?? \"\"));\n out.push(\n unrecognised.length > 0\n ? unknown(\"form-action\", \"A form that posts somewhere we recognise\", WHY_ACTION, \"structural\")\n : check(\n \"form-action\",\n \"A form that posts somewhere we recognise\",\n WHY_ACTION,\n \"structural\",\n true,\n offsite.length === 0\n ? \"your forms post to your own site\"\n : `posts to ${offsite[0]!.form.action}`,\n ),\n );\n\n const bare = measured.filter(({ form }) => !(form.fields ?? []).some((f) => f.required));\n const noRequired = bare.filter(({ url }) => !scriptRuns(url));\n const bareButChecked = bare.filter(({ url }) => scriptRuns(url));\n out.push(\n noRequired.length === 0 && bareButChecked.length > 0\n ? skip(\n \"form-required\",\n \"Fields marked as required\",\n WHY_REQUIRED,\n \"quick\",\n `${bareButChecked[0]!.url} marks none, and we watched it refuse an empty submission anyway — its script does the checking`,\n )\n : check(\n \"form-required\",\n \"Fields marked as required\",\n WHY_REQUIRED,\n \"quick\",\n noRequired.length === 0,\n noRequired.length === 0\n ? `every enquiry form marks at least one field required`\n : `${noRequired[0]!.url} marks none`,\n ),\n );\n\n return out;\n}\n\n// ─── What the browser itself reported ────────────────────────────────────────\n\n/**\n * The five findings a browser hands you for free once you have already opened\n * the page, and which no amount of markup reading can reach.\n *\n * A JavaScript error on the home page is the single most demonstrable \"your\n * site is broken\" finding there is: it is invisible in the HTML, invisible to\n * the owner, and a reader can confirm it in fifteen seconds with their own\n * developer tools. Horizontal overflow at 375px is the same shape — the most\n * common mobile bug on the web, and one nobody sees until somebody looks.\n */\nfunction browserChecks(\n pages: { url: string; extract: PageExtract; vitals?: unknown }[],\n): SiteCheck[] {\n const WHY_CONSOLE =\n \"A script that throws stops running, so whatever it was doing — a menu, a form, a gallery — silently stops working for every visitor from that point on.\";\n const WHY_REQUESTS =\n \"A file the page asks for and does not get is a missing image, an unstyled section or a feature that does nothing.\";\n const WHY_OVERFLOW =\n \"Something on the page is wider than a phone screen, so the whole page scrolls sideways. Most of your visitors are on a phone, and this is the first thing they will notice.\";\n const WHY_TINY =\n \"Text under twelve pixels is uncomfortable on a desktop and unreadable on a phone without pinching.\";\n const WHY_HEAVY =\n \"These images are downloaded at several times the size they are shown at, so every visitor pays for pixels they never see.\";\n\n const withVitals = pages.filter((p): p is typeof p & { vitals: PageVitals } => p.vitals != null);\n if (withVitals.length === 0) {\n return [\n unknown(\"console-errors\", \"Pages without JavaScript errors\", WHY_CONSOLE, \"structural\"),\n unknown(\"failed-requests\", \"Files the page asks for and gets\", WHY_REQUESTS, \"quick\"),\n unknown(\"mobile-overflow\", \"Pages that fit a phone screen\", WHY_OVERFLOW, \"structural\"),\n unknown(\"tiny-text\", \"Text big enough to read\", WHY_TINY, \"content\"),\n unknown(\"oversized-images\", \"Images sized for where they are shown\", WHY_HEAVY, \"quick\"),\n ];\n }\n\n const out: SiteCheck[] = [];\n\n const erroring = withVitals.filter((p) => p.vitals.consoleErrors.length > 0);\n out.push(\n check(\n \"console-errors\",\n \"Pages without JavaScript errors\",\n WHY_CONSOLE,\n \"structural\",\n erroring.length === 0,\n erroring.length === 0\n ? `none on the ${withVitals.length} pages we opened`\n : `${erroring[0]!.url}: ${erroring[0]!.vitals.consoleErrors[0]}`,\n ),\n );\n\n // FIRST-PARTY ONLY. A blocked analytics beacon or a third-party widget that\n // 404s is our network, an ad blocker, or somebody else's outage — reporting\n // it as their broken site is the exact error this codebase exists to avoid.\n const broken = withVitals.flatMap((p) =>\n p.vitals.failedRequests.filter((r) => r.firstParty).map((r) => ({ ...r, page: p.url })),\n );\n out.push(\n check(\n \"failed-requests\",\n \"Files the page asks for and gets\",\n WHY_REQUESTS,\n \"quick\",\n broken.length === 0,\n broken.length === 0\n ? \"every file your pages asked for came back\"\n : `${broken.length}: ${broken\n .slice(0, 2)\n .map((b) => `${b.url}${b.status ? ` (${b.status})` : \"\"}`)\n .join(\", \")}`,\n ),\n );\n\n const measuredOverflow = withVitals.filter((p) => p.vitals.overflowAt375 !== null);\n if (measuredOverflow.length === 0) {\n out.push(\n unknown(\"mobile-overflow\", \"Pages that fit a phone screen\", WHY_OVERFLOW, \"structural\"),\n );\n } else {\n // A couple of pixels is a rounding artefact of a scrollbar or a border, not\n // a layout bug. The bar is where a human would actually see the page move.\n const OVERFLOW_TOLERANCE = 4;\n const wide = measuredOverflow.filter((p) => p.vitals.overflowAt375! > OVERFLOW_TOLERANCE);\n out.push(\n check(\n \"mobile-overflow\",\n \"Pages that fit a phone screen\",\n WHY_OVERFLOW,\n \"structural\",\n wide.length === 0,\n wide.length === 0\n ? `all ${measuredOverflow.length} fit a 375px screen`\n : `${wide[0]!.url} is ${wide[0]!.vitals.overflowAt375}px wider than the screen`,\n ),\n );\n }\n\n const measuredText = withVitals.filter((p) => p.vitals.tinyText !== null);\n if (measuredText.length === 0) {\n out.push(unknown(\"tiny-text\", \"Text big enough to read\", WHY_TINY, \"content\"));\n } else {\n const tiny = measuredText.filter((p) => p.vitals.tinyText!.count > 0);\n out.push(\n check(\n \"tiny-text\",\n \"Text big enough to read\",\n WHY_TINY,\n \"content\",\n tiny.length === 0,\n tiny.length === 0\n ? \"no text under 12px on the pages we opened\"\n : `${tiny[0]!.url}: ${tiny[0]!.vitals.tinyText!.count} ${\n tiny[0]!.vitals.tinyText!.count === 1 ? \"passage\" : \"passages\"\n }, e.g. “${tiny[0]!.vitals.tinyText!.sample}”`,\n ),\n );\n }\n\n const heavy = withVitals.flatMap((p) => p.vitals.oversizedImages);\n out.push(\n check(\n \"oversized-images\",\n \"Images sized for where they are shown\",\n WHY_HEAVY,\n \"quick\",\n heavy.length === 0,\n heavy.length === 0\n ? \"every image is close to the size it is drawn at\"\n : `${heavy[0]!.src} is ${heavy[0]!.naturalWidth}px wide and drawn at ${heavy[0]!.renderedWidth}px`,\n ),\n );\n\n return out;\n}\n\n// ─── The domain, as opposed to the website ───────────────────────────────────\n\n/**\n * Five findings that never touch their web server.\n *\n * Every one of these is invisible from any amount of reading their HTML, and\n * two of them — \"anyone can send email as you\" and \"your domain renews in\n * forty-one days\" — are acted on the same afternoon they are read.\n */\n/** A published DNS record can run to several hundred characters. The reader\n * needs to see that one exists and how it ends — `~all`, `p=reject` — not to\n * read every include. The full record is one `dig` away and we are not it. */\nfunction clipRecord(record: string, max = 90): string {\n const one = record.replace(/\\s+/g, \" \").trim();\n return one.length <= max ? one : `${one.slice(0, max - 1).trimEnd()}…`;\n}\n\nexport function dnsChecks(dns: DnsFindings | null): SiteCheck[] {\n const WHY_SPF =\n \"Without it, anyone can send email that appears to come from your address, and your own mail is more likely to be filed as spam.\";\n const WHY_DMARC =\n \"SPF says who may send as you. DMARC is what tells the receiving mail server to act on that, and to report attempts to you. Without it the first one is advisory.\";\n const WHY_MX =\n \"There is no mail server listed for your domain, so email sent to any address at it is rejected.\";\n const WHY_CONTACT_MX =\n \"The address you publish is on a domain with no mail server, so anything sent to it bounces.\";\n // WHAT THIS CHECK CANNOT SEE. The registry publishes the date a registration\n // lapses. It does not publish whether the registrar will renew it\n // automatically, and most well-run domains renew inside the last thirty days\n // — so a date close at hand is a thing to confirm, not a fault we have found.\n // Saying otherwise would be our missing measurement dressed up as their\n // defect, and the client who answers \"auto-renew is on\" is then right and we\n // are wrong about the one line they checked.\n const WHY_EXPIRY =\n \"Registration dates are public; whether it renews automatically is not, so this is one to confirm at your registrar rather than a fault we can see. When a registration does lapse the site and the email stop together, usually on a weekend, and getting them back is neither quick nor certain.\";\n\n if (!dns || !dns.measured) {\n return [\n unknown(\"dns-spf\", \"An SPF record, so your email is trusted\", WHY_SPF, \"quick\"),\n unknown(\"dns-dmarc\", \"A DMARC record, so SPF is enforced\", WHY_DMARC, \"quick\"),\n unknown(\"dns-mx\", \"A mail server for your domain\", WHY_MX, \"structural\"),\n unknown(\"dns-contact-mx\", \"An email address that can receive mail\", WHY_CONTACT_MX, \"quick\"),\n unknown(\"domain-expiry\", \"A domain registration with room to spare\", WHY_EXPIRY, \"quick\"),\n ];\n }\n\n const out: SiteCheck[] = [];\n\n out.push(\n dns.spf === undefined\n ? unknown(\"dns-spf\", \"An SPF record, so your email is trusted\", WHY_SPF, \"quick\")\n : check(\n \"dns-spf\",\n \"An SPF record, so your email is trusted\",\n WHY_SPF,\n \"quick\",\n dns.spf !== null,\n (dns.spf ? clipRecord(dns.spf) : null) ?? `no v=spf1 record on ${dns.domain}`,\n ),\n );\n\n out.push(\n dns.dmarc === undefined\n ? unknown(\"dns-dmarc\", \"A DMARC record, so SPF is enforced\", WHY_DMARC, \"quick\")\n : check(\n \"dns-dmarc\",\n \"A DMARC record, so SPF is enforced\",\n WHY_DMARC,\n \"quick\",\n dns.dmarc !== null,\n (dns.dmarc ? clipRecord(dns.dmarc) : null) ?? `no record at _dmarc.${dns.domain}`,\n ),\n );\n\n out.push(\n dns.mx === undefined\n ? unknown(\"dns-mx\", \"A mail server for your domain\", WHY_MX, \"structural\")\n : check(\n \"dns-mx\",\n \"A mail server for your domain\",\n WHY_MX,\n \"structural\",\n dns.mx.length > 0,\n dns.mx.length > 0 ? dns.mx.slice(0, 2).join(\", \") : `no MX record on ${dns.domain}`,\n ),\n );\n\n out.push(\n dns.contactMx === undefined\n ? unknown(\"dns-contact-mx\", \"An email address that can receive mail\", WHY_CONTACT_MX, \"quick\")\n : dns.contactMx === null\n ? skip(\n \"dns-contact-mx\",\n \"An email address that can receive mail\",\n WHY_CONTACT_MX,\n \"quick\",\n \"the addresses you publish are on your own domain, which is checked above\",\n )\n : check(\n \"dns-contact-mx\",\n \"An email address that can receive mail\",\n WHY_CONTACT_MX,\n \"quick\",\n dns.contactMx.hasMx,\n `${dns.contactMx.domain}${dns.contactMx.hasMx ? \" accepts mail\" : \" has no mail server\"}`,\n ),\n );\n\n // Thirty days is the point at which this stops being a diary note and starts\n // being a thing to do today. Many ccTLDs publish no expiry at all, which is\n // the registry's choice and reads as unmeasured.\n const DAYS = 30;\n if (dns.expiresAt === undefined) {\n out.push(\n unknown(\"domain-expiry\", \"A domain registration with room to spare\", WHY_EXPIRY, \"quick\"),\n );\n } else {\n const on = dns.expiresAt.slice(0, 10);\n const days = Math.round((Date.parse(dns.expiresAt) - Date.now()) / 86_400_000);\n // Never \"renews on\" — that word asserts an auto-renewal the registry does\n // not publish. The date is the whole of what we know.\n const evidence =\n days < 0\n ? `the registration lapsed on ${on}`\n : days > DAYS\n ? `registered through ${on}`\n : `registered through ${on} — ${days === 1 ? \"1 day\" : `${days} days`} away`;\n out.push(\n check(\n \"domain-expiry\",\n \"A domain registration with room to spare\",\n WHY_EXPIRY,\n \"quick\",\n days > DAYS,\n evidence,\n ),\n );\n }\n\n return out;\n}\n\n/**\n * The Tier 2 verdicts — what the server actually served.\n *\n * Every field of `HttpFindings` is three-state, and the shape below is the same\n * three times over: `undefined` becomes `unmeasured`, `null` becomes\n * `not-applicable`, and only a real answer reaches a pass or a fail. A probe we\n * could not read (403 from bot management, 429 we caused ourselves) arrives as\n * the verdict `unverified` and stops there — it never becomes a finding.\n */\nexport function httpChecks(http: HttpFindings | null): SiteCheck[] {\n const WHY_FAVICON =\n \"The icon in the browser tab and the bookmark bar. When the file is missing the tab shows a blank sheet, which is the difference between a site that looks maintained and one that does not.\";\n const WHY_UPGRADE =\n \"Somebody typing your address without https, or following an old link, should land on the secure version in one step. Every extra redirect is delay on the first impression, and a chain that ends on http means the page is served in the clear.\";\n const WHY_STABLE =\n \"Two requests seconds apart got different answers, which is what an overloaded or half-deployed server looks like from outside. Some visitors are seeing the bad one.\";\n const WHY_SLASH =\n \"When /about and /about/ both answer with the same page and neither says which is real, search engines index both and split the credit between them.\";\n const WHY_INDEX =\n \"Your homepage answers at two addresses. Links, shares and search results scatter across both instead of accumulating on one.\";\n const WHY_CASE =\n \"The same page answers at two spellings, so the same content competes with itself in search results.\";\n const WHY_SITEMAP_LIVE =\n \"A sitemap is a list you hand to search engines saying 'these pages exist'. Entries that no longer answer waste the crawl budget you were trying to direct.\";\n const WHY_EXTERNAL =\n \"A link on your site that leads nowhere is a dead end for the visitor and a small mark against the page for a search engine.\";\n const WHY_OG_SERVED =\n \"This is the picture that appears when your page is shared in a message, on LinkedIn, or in a Slack channel. When the file does not load, the share renders as a bare grey box.\";\n const WHY_OG_SIZE = `A share image under ${MIN_OG_IMAGE_EDGE} pixels on either edge is either cropped to nothing or dropped entirely by the platform showing it.`;\n const WHY_LOGO =\n \"A logo that does not load leaves a broken-image icon at the top of every page, and it is the first thing on the page.\";\n const WHY_CHAINS =\n \"A link that redirects more than once makes every visitor wait through each hop, and search engines pass less credit along a chain than a direct link.\";\n\n const ALL: [string, string, string, Scope][] = [\n [\"favicon-served\", \"A favicon that actually loads\", WHY_FAVICON, \"quick\"],\n [\"https-upgrade\", \"http:// reaching https:// without a detour\", WHY_UPGRADE, \"structural\"],\n [\"home-stable\", \"A homepage that answers the same way twice\", WHY_STABLE, \"structural\"],\n [\"trailing-slash\", \"One address per page, slash or no slash\", WHY_SLASH, \"structural\"],\n [\"index-alias\", \"A homepage that lives at one address\", WHY_INDEX, \"structural\"],\n [\"case-alias\", \"Paths that do not answer to two spellings\", WHY_CASE, \"structural\"],\n [\"sitemap-urls-live\", \"A sitemap whose pages still exist\", WHY_SITEMAP_LIVE, \"quick\"],\n [\"external-links-live\", \"Outbound links that still work\", WHY_EXTERNAL, \"quick\"],\n [\"og-image-served\", \"A share image that loads\", WHY_OG_SERVED, \"quick\"],\n [\"og-image-size\", \"A share image big enough to render\", WHY_OG_SIZE, \"quick\"],\n [\"logo-served\", \"A logo that loads\", WHY_LOGO, \"quick\"],\n [\"redirect-chains\", \"Internal links that go straight there\", WHY_CHAINS, \"quick\"],\n ];\n\n const meta = new Map(ALL.map(([key, label, why, scope]) => [key, { label, why, scope }]));\n const unmeasuredAll = (): SiteCheck[] =>\n ALL.map(([key, label, why, scope]) => unknown(key, label, why, scope));\n\n if (!http || !http.measured) return unmeasuredAll();\n\n const out: SiteCheck[] = [];\n /**\n * One place where the three states are read, so no check below can invent a\n * fourth reading — and so `not-applicable` and `unmeasured` cannot both be\n * pushed for the same key, which is what happened when each check decided\n * for itself.\n *\n * undefined from `value` we never got an answer → unmeasured\n * undefined from `verdict` we got one and could not read it → unmeasured\n * null from `verdict` there was nothing of this kind → not-applicable\n */\n const add = (\n key: string,\n value: unknown,\n verdict: () => { ok: boolean; evidence: string } | null | undefined,\n ): void => {\n const m = meta.get(key)!;\n if (value === undefined) return void out.push(unknown(key, m.label, m.why, m.scope));\n const v = verdict();\n if (v === undefined) return void out.push(unknown(key, m.label, m.why, m.scope));\n if (v === null) return void out.push(skip(key, m.label, m.why, m.scope, notApplicable(key)));\n out.push(check(key, m.label, m.why, m.scope, v.ok, v.evidence));\n };\n\n const notApplicable = (key: string): string =>\n key === \"index-alias\"\n ? \"/index.html does not answer, which is the behaviour we were hoping for\"\n : key === \"case-alias\"\n ? \"a differently-cased path 404s, which is the behaviour we were hoping for\"\n : key === \"og-image-served\" || key === \"og-image-size\"\n ? \"the homepage declares no share image, which is covered above\"\n : key === \"sitemap-urls-live\"\n ? \"there is no sitemap to check, which is covered above\"\n : key === \"external-links-live\"\n ? \"this site does not link out to anywhere else\"\n : \"there was nothing of this kind to check\";\n\n /** A probe that answered 403 or 429 told us about a CDN, not about the site. */\n const fromVerdict = (v: ProbeVerdict, ok: string, bad: string) =>\n v === \"unverified\" ? undefined : { ok: v === \"ok\", evidence: v === \"ok\" ? ok : bad };\n\n add(\"favicon-served\", http.favicon, () => {\n const f = http.favicon!;\n return fromVerdict(\n f.verdict,\n f.declared ? \"the declared icon is served\" : \"/favicon.ico is served\",\n f.declared ? `the icon at ${f.url} does not load` : \"no icon at /favicon.ico\",\n );\n });\n\n add(\"https-upgrade\", http.httpUpgrade, () => {\n const u = http.httpUpgrade!;\n if (!u.https) return { ok: false, evidence: \"http:// does not end up on https://\" };\n const hops = u.hops;\n if (hops === null) return { ok: true, evidence: \"http:// arrives on https://\" };\n return {\n ok: hops <= 2,\n evidence:\n hops <= 2\n ? `http:// arrives on https:// in ${hops === 1 ? \"one hop\" : `${hops} hops`}`\n : `http:// takes ${hops} redirects to reach https://`,\n };\n });\n\n add(\"home-stable\", http.homeSamples, () => {\n const [a, b] = http.homeSamples!;\n // Both halves must have answered, and a pair of matching non-2xx statuses\n // is a different check's business — reporting it here would say\n // \"intermittent\" about a server that is consistently down.\n if (a === null || b === null) return null;\n if (a === b)\n return a >= 200 && a < 400 ? { ok: true, evidence: `two requests, both ${a}` } : null;\n return { ok: false, evidence: `two requests seconds apart answered ${a} and ${b}` };\n });\n\n add(\"trailing-slash\", http.trailingSlash, () => {\n const t = http.trailingSlash!;\n return { ok: t.settled, evidence: `${t.path} — ${t.detail}` };\n });\n\n add(\"index-alias\", http.indexAlias, () => {\n const i = http.indexAlias;\n return i === null ? null : { ok: !i!.duplicate, evidence: `/index.html — ${i!.detail}` };\n });\n\n add(\"case-alias\", http.caseAlias, () => {\n const c = http.caseAlias;\n return c === null ? null : { ok: !c!.duplicate, evidence: `${c!.path} — ${c!.detail}` };\n });\n\n add(\"sitemap-urls-live\", http.sitemapUrls, () => {\n const s = http.sitemapUrls;\n if (s === null) return null;\n const { checked, total, broken } = s!;\n // The denominator travels with the verdict: \"every URL works\" would be a\n // lie about a sitemap of four hundred when we asked about twelve. Phrased\n // so the two numbers cannot be read as a ratio of working to broken —\n // \"12 of 49 answer\" says the other 37 did not, and means the opposite.\n return broken.length === 0\n ? {\n ok: true,\n evidence:\n checked >= total\n ? `all ${total} answer`\n : `we sampled ${checked} of ${total}; all of them answer`,\n }\n : {\n ok: false,\n evidence: `${broken.length} of the ${checked} we sampled are gone — ${broken[0]!.url}`,\n };\n });\n\n add(\"external-links-live\", http.externalLinks, () => {\n const e = http.externalLinks;\n if (e === null) return null;\n const { checked, total, broken } = e!;\n return broken.length === 0\n ? {\n ok: true,\n evidence:\n checked >= total\n ? `${countOf(total, \"outbound link\", \"outbound links\")} ${\n total === 1 ? \"answers\" : \"answer\"\n }`\n : `we sampled ${checked} of ${total} outbound links; all of them answer`,\n }\n : {\n ok: false,\n evidence: `${broken.length} of the ${checked} we sampled are dead — ${broken[0]!.url}`,\n };\n });\n\n add(\"og-image-served\", http.ogImage, () => {\n const o = http.ogImage;\n if (o === null) return null;\n return fromVerdict(\n o!.verdict,\n \"the share image loads\",\n `the share image does not load — ${o!.url}`,\n );\n });\n\n add(\"og-image-size\", http.ogImage, () => {\n const o = http.ogImage;\n if (o === null) return null;\n // A format we cannot measure — SVG above all — is not a small image.\n if (o!.width === null || o!.height === null) return null;\n const small = o!.width < MIN_OG_IMAGE_EDGE || o!.height < MIN_OG_IMAGE_EDGE;\n return { ok: !small, evidence: `${o!.width}×${o!.height}` };\n });\n\n add(\"logo-served\", http.logo, () => {\n const l = http.logo;\n if (l === null) return null;\n return fromVerdict(l!.verdict, `${l!.how} loads`, `${l!.how} does not load — ${l!.url}`);\n });\n\n add(\"redirect-chains\", http.redirectChains, () => {\n const r = http.redirectChains!;\n return r.chained.length === 0\n ? { ok: true, evidence: `${r.checked} internal links, none redirecting twice` }\n : {\n ok: false,\n evidence: `${r.chained.length} of ${r.checked} take ${r.chained[0]!.hops} hops — ${r.chained[0]!.url}`,\n };\n });\n\n return out;\n}\n\n/**\n * The two verdicts that came from pressing the button.\n *\n * Both are about the same failure a business actually suffers: an enquiry that\n * never arrives. A form that submits empty puts a blank message in the inbox\n * and tells the visitor it worked; a form that accepts \"not-an-email\" takes a\n * real enquiry and leaves no way to answer it. Neither is visible in the\n * markup — the form looks perfect either way.\n *\n * `undefined` on either field means the click told us nothing (a cross-origin\n * form, a control we could not reach, a browser pass that never ran) and reads\n * as unmeasured. A form we could not press is our gap, never their defect.\n */\nfunction formInteractionChecks(probes: (FormProbe | null | undefined)[]): SiteCheck[] {\n const WHY_EMPTY =\n \"Somebody who taps send too early should be told what is missing. A form that submits anyway puts a blank enquiry in your inbox and tells them it worked, so they never follow up and you have nothing to follow up on.\";\n const WHY_EMAIL =\n \"A mistyped address is the one mistake that costs you the whole enquiry: the message arrives, and there is no way to answer it. Catching it while the visitor is still on the page is the only chance to fix it.\";\n\n const probe = probes.find((p) => p) ?? null;\n\n // Absent on every page means the browser pass never tried a form — probing\n // was turned off for this run, or the crawl predates the probe entirely.\n // OURS, so it reads as unmeasured. Saying \"we found no enquiry form\" instead\n // would be a false statement about their site covering a choice of ours, and\n // it was one: all 29 sites in the stored corpus were told exactly that, five\n // of them while publishing a form we had simply never pressed.\n const neverTried = probes.every((p) => p === undefined);\n if (neverTried) {\n const NOTE = \"we did not try a form on this run\";\n return [\n unknown(\n \"form-rejects-empty\",\n \"A form that catches an empty submission\",\n WHY_EMPTY,\n \"quick\",\n NOTE,\n ),\n unknown(\n \"form-rejects-bad-email\",\n \"A form that catches a mistyped email\",\n WHY_EMAIL,\n \"quick\",\n NOTE,\n ),\n ];\n }\n\n if (!probe) {\n // We looked and no page we read has a form we could identify as an enquiry.\n // That IS a fact about the site — but the goal battery owns \"is there a way\n // to get in touch\", so this says nothing about it.\n return [\n skip(\n \"form-rejects-empty\",\n \"A form that catches an empty submission\",\n WHY_EMPTY,\n \"quick\",\n \"we found no enquiry form to try\",\n ),\n skip(\n \"form-rejects-bad-email\",\n \"A form that catches a mistyped email\",\n WHY_EMAIL,\n \"quick\",\n \"we found no enquiry form to try\",\n ),\n ];\n }\n\n const out: SiteCheck[] = [\n probe.emptyRefused === undefined\n ? unknown(\"form-rejects-empty\", \"A form that catches an empty submission\", WHY_EMPTY, \"quick\")\n : check(\n \"form-rejects-empty\",\n \"A form that catches an empty submission\",\n WHY_EMPTY,\n \"quick\",\n probe.emptyRefused,\n probe.emptyHow ?? \"\",\n ),\n ];\n\n out.push(\n probe.invalidEmailRefused === null\n ? skip(\n \"form-rejects-bad-email\",\n \"A form that catches a mistyped email\",\n WHY_EMAIL,\n \"quick\",\n \"this form does not ask for an email address\",\n )\n : probe.invalidEmailRefused === undefined\n ? unknown(\n \"form-rejects-bad-email\",\n \"A form that catches a mistyped email\",\n WHY_EMAIL,\n \"quick\",\n )\n : check(\n \"form-rejects-bad-email\",\n \"A form that catches a mistyped email\",\n WHY_EMAIL,\n \"quick\",\n probe.invalidEmailRefused,\n probe.invalidEmailHow ?? \"\",\n ),\n );\n\n return out;\n}\n\nexport const TIER4_CHECK_KEYS = [\"form-rejects-empty\", \"form-rejects-bad-email\"] as const;\n\nexport const TIER2_HTTP_CHECK_KEYS = [\n \"favicon-served\",\n \"https-upgrade\",\n \"home-stable\",\n \"trailing-slash\",\n \"index-alias\",\n \"case-alias\",\n \"sitemap-urls-live\",\n \"external-links-live\",\n \"og-image-served\",\n \"og-image-size\",\n \"logo-served\",\n \"redirect-chains\",\n] as const;\n\nexport const TIER2_DNS_CHECK_KEYS = [\n \"dns-spf\",\n \"dns-dmarc\",\n \"dns-mx\",\n \"dns-contact-mx\",\n \"domain-expiry\",\n] as const;\n\nexport const TIER3_CHECK_KEYS = [\n \"console-errors\",\n \"failed-requests\",\n \"mobile-overflow\",\n \"tiny-text\",\n \"oversized-images\",\n] as const;\n\nexport const TIER1_CHECK_KEYS = [\n \"meta-noindex\",\n \"meta-nofollow\",\n \"meta-charset\",\n \"title-length\",\n \"description-length\",\n \"og-image-absolute\",\n \"duplicate-descriptions\",\n \"favicon-declared\",\n \"canonical-self\",\n \"canonical-origin\",\n \"hreflang-self\",\n \"form-field-types\",\n \"form-autocomplete\",\n \"form-method\",\n \"form-action\",\n \"form-required\",\n] as const;\n\nexport const TIER0_CHECK_KEYS = [\n \"dead-links\",\n \"staging-links\",\n \"insecure-links\",\n \"link-text\",\n \"tel-dialable\",\n \"social-placeholders\",\n \"nav-consistency\",\n \"lorem\",\n \"placeholder-copy\",\n \"template-leakage\",\n \"mojibake\",\n \"under-construction\",\n \"name-in-title\",\n \"h1-present\",\n \"h1-one-headline\",\n \"h1-not-name\",\n \"h1-distinct\",\n \"schema-org-complete\",\n \"schema-local-complete\",\n \"schema-url-matches\",\n \"schema-phone-matches\",\n \"schema-self-review\",\n ...SECURITY_HEADER_CHECKS.map((h) => `header-${h.header}`),\n \"header-version-leak\",\n \"header-compression\",\n \"header-hsts-age\",\n \"robots-not-blocking\",\n \"robots-names-sitemap\",\n \"sitemap-coverage\",\n \"llms-txt\",\n \"analytics\",\n] as const;\n\n/**\n * Every check in this file, for one site.\n *\n * `checks` is accepted but optional: a run whose checks stage failed can still\n * answer most of this from the crawl alone, exactly as `basics` does, and the\n * few checks that need it degrade to `unmeasured` rather than taking the rest\n * down with them.\n */\nexport function runSiteChecks(\n crawl: CrawlResult,\n checks: ChecksResult | null,\n businessName: string | null = null,\n dns: DnsFindings | null = null,\n http: HttpFindings | null = null,\n): SiteCheck[] {\n const set = usablePages(crawl.pages);\n const pages = set.pages.map((p) => ({\n url: p.page.url,\n extract: p.extract,\n // Carried through so the browser checks can read what the render pass\n // observed without a second traversal of the capture list.\n vitals: p.page.vitals,\n }));\n\n // Distinct same-origin ADDRESSES the site's own links point at, keyed the way\n // `norm2` keys everything else here. This is the honest denominator for\n // sitemap coverage: our crawl is capped, so using the number of pages we READ\n // would report our own ceiling as their gap.\n //\n // A link whose target we read is folded onto the address of the page it is a\n // confirmed alias of, and dropped when it hides itself from search — the same\n // two exclusions `contentPages` makes, for the same reasons. A link we never\n // followed keeps its own address: with no canonical and no robots meta behind\n // it, nothing excuses it, and a page we could not read that the sitemap\n // leaves out is a real gap.\n const read = new Map<string, (typeof pages)[number]>();\n for (const p of pages) {\n const at = norm2(p.url);\n if (at && !read.has(at)) read.set(at, p);\n }\n const linked = new Set<string>();\n for (const { url, extract } of pages) {\n for (const a of extract.anchors ?? []) {\n const abs = resolveNavigable(a.href, url);\n if (!abs) continue;\n try {\n if (new URL(abs).origin !== crawl.origin) continue;\n } catch {\n // Not a URL we can reason about; it is not evidence either way.\n continue;\n }\n const at = norm2(abs);\n if (!at) continue;\n const target = read.get(at);\n if (target && hiddenFromSearch(target.extract)) continue;\n linked.add(target ? (aliasAddress(target, read) ?? at) : at);\n }\n }\n\n // Phone numbers as the page writes them, for the schema comparison. Read off\n // the consistency inventory rather than re-scraped, so there is one\n // implementation of \"what numbers does this site publish\" and it is the one\n // with tests.\n const phones = (checks?.consistency?.phones ?? []).map((p) => p.normalized);\n\n const headersMeasured = Object.keys(crawl.homeHeaders ?? {}).length > 0;\n\n return [\n ...anchorChecks(pages, crawl.origin),\n ...textChecks(pages, businessName),\n ...headingChecks(pages, businessName),\n ...schemaChecks(pages, crawl.origin, phones),\n ...metaChecks(pages),\n ...linkChecks(\n pages,\n crawl.origin,\n http?.favicon !== undefined && http.favicon.verdict !== \"unverified\",\n ),\n ...formChecks(\n pages,\n crawl.pages.map((p) => p.formProbe),\n ),\n ...browserChecks(pages),\n ...dnsChecks(dns),\n ...httpChecks(http),\n ...formInteractionChecks(crawl.pages.map((p) => p.formProbe)),\n ...headerChecks(crawl.homeHeaders ?? {}, headersMeasured),\n ...sidecarChecks(crawl, linked),\n analyticsCheck(pages),\n ];\n}\n"],"mappings":"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","names":["parse","NodeType","MAX_WALK_DEPTH","parse","NodeType","safe","sleep"]}