@goflag/cli 0.2.3 → 0.2.5
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.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/report/fingerprint.ts","../src/lib/core/extract/static.ts","../src/lib/core/types.ts","../src/lib/core/extract/json-ld.ts","../src/lib/core/fetch/static.ts","../src/lib/core/extract/headless.ts","../src/lib/core/extract/heuristics.ts","../src/lib/core/extract/hydration.ts","../src/lib/core/probes/abort.ts","../src/lib/core/probes/manifest.ts","../src/lib/core/probes/robots.ts","../src/lib/core/probes/sitemap.ts","../src/lib/core/inspect.ts","../src/lib/core/glob.ts","../src/lib/core/discover.ts","../src/lib/core/crawl.ts","../src/lib/rules/evaluate.ts","../src/lib/rules/index.ts","../src/lib/rules/extraction/types.ts","../src/lib/rules/extraction/from-page.ts","../src/lib/core/lint.ts","../src/lib/core/i18n.ts","../src/lib/rules/site-rules.ts","../src/lib/core/lint-site.ts","../src/lib/core/locales.ts","../src/lib/core/sitemap/discover.ts","../src/lib/core/sitemap/parse.ts","../src/lib/core/coverage.ts","../src/lib/rules/advisory.ts","../src/lib/rules/profiles/index.ts","../src/lib/rules/prose.ts","../src/lib/core/net/fetch-url.ts","../src/lib/core/links/extract.ts","../src/lib/core/links/classify.ts","../src/lib/core/links/check.ts","../src/lib/core/links/types.ts","../src/lib/core/links/audit.ts","../src/lib/core/net/normalize-url.ts","../src/report/conformance.ts","../src/report/build.ts","../src/report/render-terminal.ts","../src/report/render-summary.ts","../src/report/summarize.ts","../src/report/logger.ts"],"sourcesContent":["/**\n * Stable finding fingerprints.\n *\n * Every finding in the report carries an `id` — a short, deterministic key\n * derived from the *identity* of the problem, not its transient details\n * (status codes, character counts, wording). This is what lets an agent (or\n * a CI baseline) say \"finding `seo-1a2b3c4d5e` is still open\" across runs,\n * and — because page URLs are normalized to origin-independent routes — even\n * across environments (localhost vs. staging vs. prod).\n *\n * Pure, no I/O. `createHash` is the only dependency.\n */\n\nimport { createHash } from \"node:crypto\";\n\n/**\n * Normalize a same-origin URL to a stable, origin-independent route key so a\n * finding on `http://localhost:3000/en/about` and `https://site.com/en/about`\n * fingerprints the same. Drops the origin, collapses a trailing slash, keeps\n * the query string (it can change what page you get).\n */\nexport function routeKey(rawUrl: string): string {\n try {\n const u = new URL(rawUrl);\n const path = u.pathname.replace(/\\/+$/, \"\") || \"/\";\n return `${path}${u.search}`;\n } catch {\n return rawUrl;\n }\n}\n\n/**\n * Normalize a link target for fingerprinting. Unlike {@link routeKey} this\n * keeps the origin, because a broken link's identity includes *where* it\n * points (two different external hosts are two different findings). Only the\n * trailing slash is normalized away.\n */\nexport function targetKey(rawUrl: string): string {\n try {\n const u = new URL(rawUrl);\n const path = u.pathname.replace(/\\/+$/, \"\") || \"/\";\n return `${u.origin}${path}${u.search}`;\n } catch {\n return rawUrl;\n }\n}\n\n/**\n * Build a finding id: `<category>-<10 hex chars>`. The parts are joined with\n * a NUL separator (which can't appear in a URL or rule id) before hashing, so\n * distinct tuples never collide by concatenation.\n */\nexport function fingerprint(category: string, ...parts: string[]): string {\n const digest = createHash(\"sha256\").update(parts.join(\"\\u0000\")).digest(\"hex\").slice(0, 10);\n return `${category}-${digest}`;\n}\n","import * as cheerio from \"cheerio\";\nimport type {\n GenericMeta,\n HreflangAlternate,\n IconLink,\n OpenGraph,\n OpenGraphImage,\n Page,\n ParsedLinks,\n RawHead,\n RawLinkTag,\n RawMetaTag,\n RawScriptTag,\n Sourced,\n TagOrigin,\n TwitterCard,\n} from \"../types\";\nimport { PAGE_SCHEMA_VERSION } from \"../types\";\nimport { parseJsonLdScripts } from \"./json-ld\";\n\nexport interface ExtractStaticOptions {\n /** Base URL used to resolve relative `href`/`src` values. Defaults to the\n * fetch's final URL (which is what `fetchStatic` provides). */\n baseUrl: string;\n}\n\n/**\n * Parse a fetched HTML document into a partial `Page` (everything except the\n * `fetch` metadata + side-channel probes, which the orchestrator stitches in).\n *\n * This function is fully deterministic and side-effect-free. It accepts a\n * string of HTML and returns a JSON-serializable object.\n */\nexport function extractStatic(\n html: string,\n options: ExtractStaticOptions,\n): Omit<Page, \"fetch\" | \"fetchedAt\" | \"probes\" | \"extractor\" | \"html\" | \"hydration\"> {\n const $ = cheerio.load(html);\n const base = resolveBase($, options.baseUrl);\n\n const raw = extractRaw($);\n const meta = parseGenericMeta(raw, base);\n const openGraph = parseOpenGraph(raw);\n const twitter = parseTwitter(raw);\n const links = parseLinks(raw, base);\n const jsonLd = parseJsonLdScripts(raw.scripts);\n\n return {\n schemaVersion: PAGE_SCHEMA_VERSION,\n raw,\n meta,\n openGraph,\n twitter,\n links,\n jsonLd,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Raw inventory\n// ---------------------------------------------------------------------------\n\nfunction extractRaw($: cheerio.CheerioAPI): RawHead {\n const html = $(\"html\").first();\n const baseEl = $(\"head base\").first();\n\n // React 19 / Next 15 streaming SSR emits hoistable metadata (`<title>`,\n // `<meta>`, `<link>`, JSON-LD) inside `<body>`; the browser hoists them\n // into `<head>` at parse time, and social scrapers read them wherever\n // they appear. Scoping to `<head>` alone would miss every tag on these\n // increasingly common pages, so we collect document-wide (the legacy\n // head-only case is a strict subset).\n const head: RawHead = {\n title: pickTitle($),\n htmlLang: attr(html, \"lang\"),\n htmlDir: attr(html, \"dir\"),\n baseHref: attr(baseEl, \"href\"),\n metas: [],\n links: [],\n scripts: [],\n };\n\n $(\"meta\").each((_, el) => {\n head.metas.push(toRawMeta($, el));\n });\n $(\"link\").each((_, el) => {\n head.links.push(toRawLink($, el));\n });\n $('script[type=\"application/ld+json\"]').each((_, el) => {\n head.scripts.push(toRawScript($, el));\n });\n\n return head;\n}\n\n/**\n * Document title, preferring a real `<head><title>` but falling back to\n * the first `<title>` rendered in the body (React 19 hoistables). SVG\n * `<title>` elements are skipped — they're accessibility labels for\n * inline icons, not the document title.\n */\nfunction pickTitle($: cheerio.CheerioAPI): string | undefined {\n const headTitle = trim($(\"head title\").first().text());\n if (headTitle) return headTitle;\n let found: string | undefined;\n $(\"title\").each((_, el) => {\n if (found) return;\n if ($(el).parents(\"svg\").length > 0) return;\n const text = trim($(el).text());\n if (text) found = text;\n });\n return found || undefined;\n}\n\nfunction toRawMeta($: cheerio.CheerioAPI, el: ReturnType<cheerio.CheerioAPI>[number]): RawMetaTag {\n const attributes = collectAttributes($, el);\n return {\n name: attributes.name,\n property: attributes.property,\n httpEquiv: attributes[\"http-equiv\"],\n content: attributes.content,\n charset: attributes.charset,\n attributes,\n };\n}\n\nfunction toRawLink($: cheerio.CheerioAPI, el: ReturnType<cheerio.CheerioAPI>[number]): RawLinkTag {\n const attributes = collectAttributes($, el);\n return {\n rel: attributes.rel ?? \"\",\n href: attributes.href,\n hreflang: attributes.hreflang,\n type: attributes.type,\n sizes: attributes.sizes,\n media: attributes.media,\n crossorigin: attributes.crossorigin,\n attributes,\n };\n}\n\nfunction toRawScript(\n $: cheerio.CheerioAPI,\n el: ReturnType<cheerio.CheerioAPI>[number],\n): RawScriptTag {\n const attributes = collectAttributes($, el);\n return {\n type: attributes.type,\n src: attributes.src,\n content: trim($(el).text()) || undefined,\n attributes,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Generic meta\n// ---------------------------------------------------------------------------\n\nfunction parseGenericMeta(raw: RawHead, base: URL): GenericMeta {\n const meta: GenericMeta = {};\n\n if (raw.title) {\n meta.title = sourced(raw.title, { kind: \"title\" }, raw.title);\n }\n\n for (const m of raw.metas) {\n const name = m.name?.toLowerCase();\n if (name === \"description\" && m.content !== undefined) {\n meta.description = sourcedMeta(m.content, m);\n } else if (name === \"keywords\" && m.content !== undefined) {\n const list = m.content\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n meta.keywords = sourcedMeta(list, m);\n } else if (name === \"author\" && m.content !== undefined) {\n meta.author = sourcedMeta(m.content, m);\n } else if (name === \"theme-color\" && m.content !== undefined) {\n meta.themeColor = sourcedMeta(m.content, m);\n } else if (name === \"color-scheme\" && m.content !== undefined) {\n meta.colorScheme = sourcedMeta(m.content, m);\n } else if (name === \"viewport\" && m.content !== undefined) {\n meta.viewport = sourcedMeta(m.content, m);\n } else if (name === \"robots\" && m.content !== undefined) {\n meta.robots = sourcedMeta(m.content, m);\n } else if (name === \"googlebot\" && m.content !== undefined) {\n meta.googlebot = sourcedMeta(m.content, m);\n } else if (name === \"generator\" && m.content !== undefined) {\n meta.generator = sourcedMeta(m.content, m);\n } else if (name === \"referrer\" && m.content !== undefined) {\n meta.referrer = sourcedMeta(m.content, m);\n } else if (name === \"application-name\" && m.content !== undefined) {\n meta.applicationName = sourcedMeta(m.content, m);\n } else if (m.charset !== undefined) {\n meta.charset = sourced(m.charset, { kind: \"meta\", name: \"charset\" }, m.charset);\n } else if (m.httpEquiv?.toLowerCase() === \"content-type\" && m.content !== undefined) {\n const charset = /charset=([^;]+)/i.exec(m.content)?.[1]?.trim();\n if (charset && !meta.charset) {\n meta.charset = sourced(charset, { kind: \"meta\", httpEquiv: \"content-type\" }, m.content);\n }\n }\n }\n\n for (const l of raw.links) {\n const rel = l.rel?.toLowerCase();\n if (rel === \"canonical\" && l.href) {\n const resolved = safeResolve(l.href, base);\n meta.canonical = sourced(resolved, { kind: \"link\", rel: \"canonical\" }, l.href);\n }\n }\n\n return meta;\n}\n\n// ---------------------------------------------------------------------------\n// Open Graph\n// ---------------------------------------------------------------------------\n\nfunction parseOpenGraph(raw: RawHead): OpenGraph {\n const og: OpenGraph = {\n localeAlternates: [],\n images: [],\n unknown: [],\n };\n let current: OpenGraphImage | undefined;\n\n for (const m of raw.metas) {\n const property = m.property?.toLowerCase();\n if (!property?.startsWith(\"og:\") || m.content === undefined) continue;\n\n const value = m.content;\n switch (property) {\n case \"og:title\":\n og.title = sourcedMeta(value, m);\n break;\n case \"og:type\":\n og.type = sourcedMeta(value, m);\n break;\n case \"og:url\":\n og.url = sourcedMeta(value, m);\n break;\n case \"og:description\":\n og.description = sourcedMeta(value, m);\n break;\n case \"og:site_name\":\n og.siteName = sourcedMeta(value, m);\n break;\n case \"og:locale\":\n og.locale = sourcedMeta(value, m);\n break;\n case \"og:locale:alternate\":\n og.localeAlternates.push(sourcedMeta(value, m));\n break;\n case \"og:image\":\n case \"og:image:url\":\n current = { url: sourcedMeta(value, m) };\n og.images.push(current);\n break;\n case \"og:image:secure_url\":\n if (current) current.secureUrl = sourcedMeta(value, m);\n break;\n case \"og:image:type\":\n if (current) current.type = sourcedMeta(value, m);\n break;\n case \"og:image:width\": {\n const n = Number.parseInt(value, 10);\n if (current && Number.isFinite(n)) current.width = sourcedMeta(n, m);\n break;\n }\n case \"og:image:height\": {\n const n = Number.parseInt(value, 10);\n if (current && Number.isFinite(n)) current.height = sourcedMeta(n, m);\n break;\n }\n case \"og:image:alt\":\n if (current) current.alt = sourcedMeta(value, m);\n break;\n default:\n og.unknown.push({ property, value: sourcedMeta(value, m) });\n }\n }\n\n return og;\n}\n\n// ---------------------------------------------------------------------------\n// Twitter\n// ---------------------------------------------------------------------------\n\nfunction parseTwitter(raw: RawHead): TwitterCard {\n const tw: TwitterCard = {};\n for (const m of raw.metas) {\n const name = m.name?.toLowerCase();\n if (!name?.startsWith(\"twitter:\") || m.content === undefined) continue;\n switch (name) {\n case \"twitter:card\":\n tw.card = sourcedMeta(m.content, m);\n break;\n case \"twitter:site\":\n tw.site = sourcedMeta(m.content, m);\n break;\n case \"twitter:creator\":\n tw.creator = sourcedMeta(m.content, m);\n break;\n case \"twitter:title\":\n tw.title = sourcedMeta(m.content, m);\n break;\n case \"twitter:description\":\n tw.description = sourcedMeta(m.content, m);\n break;\n case \"twitter:image\":\n case \"twitter:image:src\":\n tw.image = sourcedMeta(m.content, m);\n break;\n case \"twitter:image:alt\":\n tw.imageAlt = sourcedMeta(m.content, m);\n break;\n }\n }\n return tw;\n}\n\n// ---------------------------------------------------------------------------\n// Links: hreflang, icons, manifest, feeds\n// ---------------------------------------------------------------------------\n\nfunction parseLinks(raw: RawHead, base: URL): ParsedLinks {\n const out: ParsedLinks = {\n alternates: [],\n icons: [],\n feeds: [],\n preconnects: [],\n dnsPrefetches: [],\n };\n\n for (const l of raw.links) {\n const rel = l.rel.toLowerCase();\n if (rel === \"canonical\" && l.href) {\n out.canonical = safeResolve(l.href, base);\n } else if (rel === \"alternate\" && l.hreflang && l.href) {\n const alt: HreflangAlternate = {\n hreflang: l.hreflang,\n href: safeResolve(l.href, base),\n isXDefault: l.hreflang.toLowerCase() === \"x-default\",\n };\n out.alternates.push(alt);\n } else if (rel === \"alternate\" && l.type && l.href && isFeedType(l.type)) {\n out.feeds.push({\n rel: \"alternate\",\n type: l.type.toLowerCase(),\n href: safeResolve(l.href, base),\n title: l.attributes.title,\n });\n } else if (\n (rel === \"icon\" || rel === \"apple-touch-icon\" || rel === \"shortcut icon\") &&\n l.href\n ) {\n const icon: IconLink = {\n rel,\n href: safeResolve(l.href, base),\n sizes: l.sizes,\n type: l.type,\n parsedSizes: parseSizes(l.sizes),\n };\n out.icons.push(icon);\n } else if (rel === \"manifest\" && l.href) {\n out.manifest = { href: safeResolve(l.href, base), crossorigin: l.crossorigin };\n } else if (rel === \"preconnect\" && l.href) {\n out.preconnects.push(safeResolve(l.href, base));\n } else if (rel === \"dns-prefetch\" && l.href) {\n out.dnsPrefetches.push(safeResolve(l.href, base));\n }\n }\n\n return out;\n}\n\nfunction isFeedType(type: string): boolean {\n const t = type.toLowerCase();\n return (\n t === \"application/rss+xml\" || t === \"application/atom+xml\" || t === \"application/feed+json\"\n );\n}\n\nfunction parseSizes(sizes: string | undefined): IconLink[\"parsedSizes\"] {\n if (!sizes) return [];\n return sizes\n .split(/\\s+/)\n .filter(Boolean)\n .map((token) => {\n if (token.toLowerCase() === \"any\") return \"any\" as const;\n const m = /^(\\d+)x(\\d+)$/i.exec(token);\n if (!m) return undefined;\n const w = Number.parseInt(m[1]!, 10);\n const h = Number.parseInt(m[2]!, 10);\n if (!Number.isFinite(w) || !Number.isFinite(h)) return undefined;\n return { width: w, height: h };\n })\n .filter((v): v is { width: number; height: number } | \"any\" => v !== undefined);\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction collectAttributes(\n _$: cheerio.CheerioAPI,\n el: ReturnType<cheerio.CheerioAPI>[number],\n): Record<string, string> {\n const result: Record<string, string> = {};\n const attribs = (el as { attribs?: Record<string, string> }).attribs;\n if (attribs) {\n for (const [k, v] of Object.entries(attribs)) {\n result[k.toLowerCase()] = v;\n }\n }\n return result;\n}\n\nfunction attr(el: ReturnType<cheerio.CheerioAPI>, name: string): string | undefined {\n const v = el.attr(name);\n return v === undefined || v === \"\" ? undefined : v;\n}\n\nfunction trim(s: string | undefined): string {\n return (s ?? \"\").replace(/\\s+/g, \" \").trim();\n}\n\nfunction resolveBase($: cheerio.CheerioAPI, requestUrl: string): URL {\n let base: URL;\n try {\n base = new URL(requestUrl);\n } catch {\n base = new URL(\"http://invalid.local/\");\n }\n const baseHref = $(\"head base\").first().attr(\"href\");\n if (baseHref) {\n try {\n base = new URL(baseHref, base);\n } catch {\n // Ignore unparseable <base href> — fall back to the request URL.\n }\n }\n return base;\n}\n\nfunction safeResolve(href: string, base: URL): string {\n try {\n return new URL(href, base).toString();\n } catch {\n return href;\n }\n}\n\nfunction sourced<T>(value: T, origin: TagOrigin, raw?: string): Sourced<T> {\n return { value, origin, raw };\n}\n\nfunction sourcedMeta<T>(value: T, m: RawMetaTag): Sourced<T> {\n const origin: TagOrigin = m.property\n ? { kind: \"meta\", property: m.property }\n : m.name\n ? { kind: \"meta\", name: m.name }\n : m.httpEquiv\n ? { kind: \"meta\", httpEquiv: m.httpEquiv }\n : { kind: \"meta\" };\n return { value, origin, raw: m.content };\n}\n","/**\n * Canonical Goflag data model.\n *\n * Everything the engine, rules, suggestions, previews, snapshots, diff, and\n * report layers consume comes from this single shape. The `Page` is the\n * entire deterministic output of fetching + parsing one URL — no behavior,\n * no side effects, fully serializable to JSON.\n *\n * Design constraints (see PLAN.md \"Architectural non-negotiables\"):\n *\n * - **No DOM, no React, no Next.js types here.** This module must be safely\n * importable from a Node CLI, a browser bundle, an edge worker, or a\n * future `@goflag/core` package consumed by a hosted SaaS.\n * - **Stable contract.** Adding fields is fine; removing or renaming is a\n * semver-major event. Snapshot files (Phase 9) and `--json` consumers\n * (Phase 11.5) depend on this shape.\n * - **Origins, not just values.** For most metadata we record both the\n * parsed value AND where it came from (which raw tag, attribute, or\n * JSON-LD path). This is what powers Phase 4's \"what if I remove\n * `og:image`?\" toggle and the Phase 9 diff classifier.\n */\n\n// ---------------------------------------------------------------------------\n// Atoms\n// ---------------------------------------------------------------------------\n\n/**\n * Where a parsed value originated. Used by previews to highlight which tag a\n * given preview consumed and by rules/suggestions to point the user back at\n * the exact line in the source.\n */\nexport type TagOrigin =\n | { kind: \"title\" }\n | { kind: \"meta\"; name?: string; property?: string; httpEquiv?: string }\n | { kind: \"link\"; rel: string }\n | { kind: \"html\"; attribute: string }\n | { kind: \"json-ld\"; path: string; index: number }\n | { kind: \"header\"; name: string }\n | { kind: \"computed\" };\n\n/**\n * A typed wrapper around a value that knows where it came from and which raw\n * source produced it. We do not unwrap until we render or diff.\n */\nexport interface Sourced<T> {\n value: T;\n origin: TagOrigin;\n /** The literal string as it appeared in the source, before any normalization. */\n raw?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Raw HEAD inventory\n// ---------------------------------------------------------------------------\n\nexport interface RawMetaTag {\n name?: string;\n property?: string;\n httpEquiv?: string;\n content?: string;\n charset?: string;\n /** All attributes verbatim, lowercased keys. Useful for unusual meta tags. */\n attributes: Record<string, string>;\n}\n\nexport interface RawLinkTag {\n rel: string;\n href?: string;\n hreflang?: string;\n type?: string;\n sizes?: string;\n media?: string;\n crossorigin?: string;\n attributes: Record<string, string>;\n}\n\nexport interface RawScriptTag {\n type?: string;\n src?: string;\n /** Inline script content, only kept for `application/ld+json`. */\n content?: string;\n attributes: Record<string, string>;\n}\n\nexport interface RawHead {\n /** Document `<title>` text content, trimmed. */\n title?: string;\n /** Outer-HTML `lang` attribute on `<html>`. */\n htmlLang?: string;\n /** Outer-HTML `dir` attribute on `<html>`. */\n htmlDir?: string;\n /** Document `base` href, when present. */\n baseHref?: string;\n metas: RawMetaTag[];\n links: RawLinkTag[];\n scripts: RawScriptTag[];\n}\n\n// ---------------------------------------------------------------------------\n// Parsed views — Open Graph, Twitter, generic meta\n// ---------------------------------------------------------------------------\n\nexport interface OpenGraphImage {\n url: Sourced<string>;\n secureUrl?: Sourced<string>;\n type?: Sourced<string>;\n width?: Sourced<number>;\n height?: Sourced<number>;\n alt?: Sourced<string>;\n}\n\nexport interface OpenGraph {\n title?: Sourced<string>;\n type?: Sourced<string>;\n url?: Sourced<string>;\n description?: Sourced<string>;\n siteName?: Sourced<string>;\n locale?: Sourced<string>;\n localeAlternates: Sourced<string>[];\n images: OpenGraphImage[];\n /** Any `og:*` property we didn't map explicitly — kept verbatim for rules. */\n unknown: Array<{ property: string; value: Sourced<string> }>;\n}\n\nexport type TwitterCardType = \"summary\" | \"summary_large_image\" | \"app\" | \"player\" | string;\n\nexport interface TwitterCard {\n card?: Sourced<TwitterCardType>;\n site?: Sourced<string>;\n creator?: Sourced<string>;\n title?: Sourced<string>;\n description?: Sourced<string>;\n image?: Sourced<string>;\n imageAlt?: Sourced<string>;\n}\n\n/**\n * The \"generic\" parsed surface — values that exist on every page regardless\n * of OG / Twitter coverage. Pulled from `<title>`, `<meta name=\"…\">`,\n * `<link rel=\"…\">`, `<html lang>`, etc.\n */\nexport interface GenericMeta {\n title?: Sourced<string>;\n description?: Sourced<string>;\n keywords?: Sourced<string[]>;\n author?: Sourced<string>;\n themeColor?: Sourced<string>;\n colorScheme?: Sourced<string>;\n viewport?: Sourced<string>;\n charset?: Sourced<string>;\n canonical?: Sourced<string>;\n robots?: Sourced<string>;\n googlebot?: Sourced<string>;\n generator?: Sourced<string>;\n referrer?: Sourced<string>;\n applicationName?: Sourced<string>;\n}\n\n// ---------------------------------------------------------------------------\n// Links: hreflang, icons, manifest, RSS, etc.\n// ---------------------------------------------------------------------------\n\nexport interface HreflangAlternate {\n hreflang: string;\n href: string;\n /** True when `hreflang === \"x-default\"`. */\n isXDefault: boolean;\n}\n\nexport interface IconLink {\n rel: string;\n href: string;\n sizes?: string;\n type?: string;\n /** Parsed `sizes` like `32x32` or `any`. Empty for unparseable values. */\n parsedSizes: Array<{ width: number; height: number } | \"any\">;\n}\n\nexport interface ManifestLink {\n href: string;\n crossorigin?: string;\n}\n\nexport interface FeedLink {\n rel: \"alternate\";\n type: string;\n href: string;\n title?: string;\n}\n\nexport interface ParsedLinks {\n canonical?: string;\n alternates: HreflangAlternate[];\n icons: IconLink[];\n manifest?: ManifestLink;\n feeds: FeedLink[];\n preconnects: string[];\n dnsPrefetches: string[];\n}\n\n// ---------------------------------------------------------------------------\n// JSON-LD\n// ---------------------------------------------------------------------------\n\n/** A single `<script type=\"application/ld+json\">` block, parsed if possible. */\nexport interface JsonLdBlock {\n /** Index in document order (0-based). */\n index: number;\n /** Verbatim script content (after trimming). */\n raw: string;\n /** Parsed JSON, or `null` if parsing failed. */\n data: unknown;\n parseError?: string;\n /** Top-level `@type` values found in the block (covers `@graph` too). */\n types: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Side-channel probes\n// ---------------------------------------------------------------------------\n\nexport interface RobotsProbe {\n url: string;\n status: number;\n found: boolean;\n raw?: string;\n /** Parsed `Sitemap:` declarations. */\n sitemaps: string[];\n /** True iff at least one `Disallow: /` line exists for `User-agent: *`. */\n blocksAll: boolean;\n}\n\nexport interface SitemapProbe {\n url: string;\n status: number;\n found: boolean;\n /** True when the body looks like a sitemap index (`<sitemapindex>`). */\n isIndex: boolean;\n /** Number of `<url>` or `<sitemap>` entries detected (cheap regex count). */\n entryCount: number;\n}\n\nexport interface ManifestProbe {\n url: string;\n status: number;\n found: boolean;\n raw?: string;\n parseError?: string;\n data?: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Network metadata\n// ---------------------------------------------------------------------------\n\nexport interface FetchMeta {\n /** The URL we were asked to fetch. */\n requestedUrl: string;\n /** The URL after following redirects, or `requestedUrl` if no redirect. */\n finalUrl: string;\n status: number;\n statusText: string;\n /** Lowercased HTTP headers from the final response. */\n headers: Record<string, string>;\n /** Number of redirects followed. */\n redirectCount: number;\n /** Total wall-clock time in milliseconds. */\n durationMs: number;\n /** Body size in bytes (UTF-8). */\n bodyBytes: number;\n /** Content type from `content-type`, lowercased, parameters stripped. */\n contentType?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Extractor mode + hydration delta\n// ---------------------------------------------------------------------------\n\n/**\n * Which extraction strategy produced this `Page`.\n *\n * - `\"static\"`: HTML was fetched once via `fetchStatic` and parsed as-is.\n * What we see is what a non-JS crawler sees (e.g. older social previewers,\n * historic Slackbot, plain `curl`).\n * - `\"headless\"`: HTML was rendered in Chromium and parsed after the\n * network went idle. What we see is what a JS-aware crawler sees\n * (modern Googlebot, X / Twitter, Discord).\n *\n * `escalated: true` means the orchestrator started in static mode, decided\n * the `<head>` looked suspiciously empty (likely client-rendered SPA), and\n * automatically re-ran the extractor in headless mode. `escalationReason`\n * carries the human-readable trigger (\"title missing, no og:*, no canonical\")\n * so rules and the UI can surface \"we had to render JS to see this page\".\n */\nexport type ExtractorMode = \"static\" | \"headless\";\n\nexport interface ExtractorMeta {\n mode: ExtractorMode;\n /** True when we started in static mode and auto-fell-through to headless. */\n escalated: boolean;\n /** Human-readable trigger for the escalation (only set when `escalated`). */\n escalationReason?: string;\n}\n\n/**\n * Difference between what a non-JS fetch saw and what Chromium saw after the\n * page hydrated.\n *\n * The lists are deliberately minimal subsets of `RawMetaTag` / `RawLinkTag` —\n * just enough to identify a tag and let Phase 5 rules say \"this `og:image`\n * is client-injected, Slack's previewer won't see it\". The full raw HTML for\n * both passes lives on `Page.html` if richer diffing is needed later.\n */\nexport interface HydrationDelta {\n /** Mode the static pass ran in (always \"static\" today; kept for symmetry). */\n fromMode: \"static\";\n /** Mode the rendered pass ran in (always \"headless\" today). */\n toMode: \"headless\";\n /** Did the document `<title>` text change after hydration? */\n titleChanged: boolean;\n /** Did the `<html lang>` attribute change after hydration? */\n htmlLangChanged: boolean;\n /** Metas present in the rendered HTML but not in the static HTML. */\n clientInjectedMetas: Array<{\n name?: string;\n property?: string;\n httpEquiv?: string;\n content?: string;\n }>;\n /** Metas present in the static HTML but removed by client JS. */\n clientRemovedMetas: Array<{\n name?: string;\n property?: string;\n httpEquiv?: string;\n content?: string;\n }>;\n /** Links present in the rendered HTML but not in the static HTML. */\n clientInjectedLinks: Array<{\n rel: string;\n href?: string;\n hreflang?: string;\n }>;\n /** Links present in the static HTML but removed by client JS. */\n clientRemovedLinks: Array<{\n rel: string;\n href?: string;\n hreflang?: string;\n }>;\n /** Number of new JSON-LD blocks introduced by client JS. */\n jsonLdBlocksAdded: number;\n}\n\nexport interface PageHtml {\n /**\n * Raw HTML returned by the static fetch, before any JS executed.\n * Always present.\n */\n static: string;\n /**\n * Final HTML after Chromium rendered the page and the network went idle.\n * Only present when the headless extractor ran (either by user request or\n * by auto-escalation).\n */\n rendered?: string;\n}\n\n// ---------------------------------------------------------------------------\n// The Page\n// ---------------------------------------------------------------------------\n\n/**\n * Engine-wide marker. Bumped manually whenever the `Page` shape changes in a\n * way that invalidates committed snapshots or `--json` consumers.\n */\nexport const PAGE_SCHEMA_VERSION = 2 as const;\n\nexport interface Page {\n /** Constant marker for schema migrations. */\n schemaVersion: typeof PAGE_SCHEMA_VERSION;\n /** When this `Page` was produced (ISO string, UTC). */\n fetchedAt: string;\n fetch: FetchMeta;\n /**\n * Which extraction strategy produced the `raw`/`meta`/`openGraph`/etc.\n * fields below. When `mode === \"headless\"`, the parsed views reflect the\n * post-hydration DOM; `html.static` still carries the original non-JS\n * markup so rules can compare the two.\n */\n extractor: ExtractorMeta;\n /** Raw HTML for both passes. `rendered` is only set in headless mode. */\n html: PageHtml;\n raw: RawHead;\n meta: GenericMeta;\n openGraph: OpenGraph;\n twitter: TwitterCard;\n links: ParsedLinks;\n jsonLd: JsonLdBlock[];\n /**\n * What changed between the static HTML and the rendered HTML. Only set\n * when both passes ran (i.e. `extractor.mode === \"headless\"`).\n */\n hydration?: HydrationDelta;\n /** Side-channel probes; populated when the engine is asked to do so. */\n probes: {\n robots?: RobotsProbe;\n sitemap?: SitemapProbe;\n manifest?: ManifestProbe;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Issues (forward declaration — Phase 5 fills in the rule engine)\n// ---------------------------------------------------------------------------\n\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\nexport interface Issue {\n ruleId: string;\n severity: Severity;\n message: string;\n /** Optional pointer back into the `Page` for UI highlighting. */\n origin?: TagOrigin;\n /** Optional fix snippet for \"Copy fix\" buttons and PR comments. */\n fix?: { title: string; snippet: string; language: string };\n /** Optional suggested replacement value. */\n suggestion?: string;\n /** Documentation link (relative path under `/rules/`). */\n docs?: string;\n}\n","import type { JsonLdBlock, RawScriptTag } from \"../types\";\n\n/**\n * Parse all `<script type=\"application/ld+json\">` blocks captured in `raw.scripts`.\n *\n * Resilient to:\n * - Trailing commas (warn-only, parse strictly)\n * - HTML entity-escaped content (`&` → `&`, `<` → `<`, etc.)\n * - Multiple roots in `@graph`\n * - Top-level arrays of types\n */\nexport function parseJsonLdScripts(scripts: RawScriptTag[]): JsonLdBlock[] {\n const blocks: JsonLdBlock[] = [];\n let visibleIndex = 0;\n\n for (const s of scripts) {\n if (s.type?.toLowerCase() !== \"application/ld+json\") continue;\n const raw = s.content?.trim() ?? \"\";\n if (raw.length === 0) {\n blocks.push({\n index: visibleIndex++,\n raw,\n data: null,\n parseError: \"empty\",\n types: [],\n });\n continue;\n }\n\n const decoded = decodeHtmlEntities(raw);\n let parsed: unknown;\n let parseError: string | undefined;\n try {\n parsed = JSON.parse(decoded);\n } catch (err) {\n parsed = null;\n parseError = err instanceof Error ? err.message : String(err);\n }\n\n blocks.push({\n index: visibleIndex++,\n raw,\n data: parsed,\n parseError,\n types: parsed === null ? [] : extractTypes(parsed),\n });\n }\n\n return blocks;\n}\n\n/** Walk the parsed JSON-LD value and pick up every `@type` we can find. */\nexport function extractTypes(node: unknown): string[] {\n const out = new Set<string>();\n visit(node, out);\n return [...out];\n}\n\nfunction visit(node: unknown, out: Set<string>): void {\n if (node === null || node === undefined) return;\n if (Array.isArray(node)) {\n for (const item of node) visit(item, out);\n return;\n }\n if (typeof node !== \"object\") return;\n\n const obj = node as Record<string, unknown>;\n const t = obj[\"@type\"];\n if (typeof t === \"string\") {\n out.add(t);\n } else if (Array.isArray(t)) {\n for (const v of t) {\n if (typeof v === \"string\") out.add(v);\n }\n }\n\n const graph = obj[\"@graph\"];\n if (Array.isArray(graph)) {\n for (const item of graph) visit(item, out);\n }\n\n // For nested entities like `mainEntity`, `author.@type`, etc.\n for (const [k, v] of Object.entries(obj)) {\n if (k === \"@type\" || k === \"@graph\") continue;\n if (v && typeof v === \"object\") visit(v, out);\n }\n}\n\nfunction decodeHtmlEntities(s: string): string {\n return s\n .replaceAll(\""\", '\"')\n .replaceAll(\"'\", \"'\")\n .replaceAll(\"'\", \"'\")\n .replaceAll(\"'\", \"'\")\n .replaceAll(\"<\", \"<\")\n .replaceAll(\">\", \">\")\n .replaceAll(\" \", \" \")\n .replaceAll(\"&\", \"&\");\n}\n","import type { FetchMeta } from \"../types\";\n\nexport interface FetchStaticOptions {\n /** Per-request timeout in ms. Defaults to 15_000. */\n timeoutMs?: number;\n /** Maximum redirects to follow. Defaults to 10. */\n maxRedirects?: number;\n /** Override the User-Agent header. Defaults to a Goflag UA. */\n userAgent?: string;\n /** Allow self-signed certificates (for `*.local` and tunnels). */\n allowInsecureTls?: boolean;\n /** Optional AbortSignal for caller-driven cancellation. */\n signal?: AbortSignal;\n}\n\nexport interface FetchStaticResult {\n meta: FetchMeta;\n body: string;\n}\n\nconst DEFAULT_UA = \"Goflag/0.0 (+https://github.com/tancredesimonin-indie/goflag)\";\n\nexport class FetchError extends Error {\n readonly url: string;\n override readonly cause?: unknown;\n constructor(message: string, url: string, options?: { cause?: unknown }) {\n super(message);\n this.name = \"FetchError\";\n this.url = url;\n this.cause = options?.cause;\n }\n}\n\n/**\n * Fetch an HTML document and return its body + observed network metadata.\n *\n * Implementation notes:\n * - We use `fetch` (Node 20+ built-in undici) with `redirect: \"manual\"` so we\n * can count redirects ourselves and still report the final URL.\n * - For `*.local` and other self-signed hosts the caller can opt into\n * `allowInsecureTls` which sets `NODE_TLS_REJECT_UNAUTHORIZED=0` for the\n * duration of this request only (process-global, restored on completion).\n * - All errors thrown by this module are `FetchError` instances with a clear\n * user-facing message.\n */\nexport async function fetchStatic(\n url: string,\n options: FetchStaticOptions = {},\n): Promise<FetchStaticResult> {\n const timeoutMs = options.timeoutMs ?? 15_000;\n const maxRedirects = options.maxRedirects ?? 10;\n const userAgent = options.userAgent ?? DEFAULT_UA;\n\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new FetchError(`Invalid URL: ${url}`, url);\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new FetchError(\n `Unsupported protocol \"${parsed.protocol}\" — only http(s) is supported`,\n url,\n );\n }\n\n const abort = new AbortController();\n const onCallerAbort = () => abort.abort(options.signal?.reason);\n options.signal?.addEventListener(\"abort\", onCallerAbort, { once: true });\n const timeout = setTimeout(\n () => abort.abort(new FetchError(\"Request timed out\", url)),\n timeoutMs,\n );\n\n const restoreTls = relaxTlsIfRequested(options.allowInsecureTls);\n const start = performance.now();\n\n try {\n let currentUrl = parsed.toString();\n let redirects = 0;\n let response: Response | undefined;\n while (true) {\n try {\n response = await fetch(currentUrl, {\n method: \"GET\",\n redirect: \"manual\",\n headers: {\n \"user-agent\": userAgent,\n accept: \"text/html,application/xhtml+xml;q=0.9,*/*;q=0.5\",\n },\n signal: abort.signal,\n });\n } catch (err) {\n throw new FetchError(`Network error fetching ${currentUrl}: ${describe(err)}`, url, {\n cause: err,\n });\n }\n\n if (isRedirect(response.status)) {\n const location = response.headers.get(\"location\");\n if (!location) {\n throw new FetchError(\n `Redirect ${response.status} from ${currentUrl} missing Location header`,\n url,\n );\n }\n if (redirects >= maxRedirects) {\n throw new FetchError(\n `Exceeded maximum of ${maxRedirects} redirects (last hop: ${currentUrl})`,\n url,\n );\n }\n // Drain the body so the underlying connection can be reused.\n await response.body?.cancel().catch(() => undefined);\n currentUrl = new URL(location, currentUrl).toString();\n redirects += 1;\n continue;\n }\n\n const body = await response.text();\n const headers = headersToObject(response.headers);\n const contentType = stripContentTypeParams(headers[\"content-type\"]);\n const meta: FetchMeta = {\n requestedUrl: parsed.toString(),\n finalUrl: currentUrl,\n status: response.status,\n statusText: response.statusText,\n headers,\n redirectCount: redirects,\n durationMs: Math.round(performance.now() - start),\n bodyBytes: Buffer.byteLength(body, \"utf8\"),\n contentType,\n };\n return { meta, body };\n }\n } finally {\n clearTimeout(timeout);\n options.signal?.removeEventListener(\"abort\", onCallerAbort);\n restoreTls();\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;\n}\n\nfunction headersToObject(headers: Headers): Record<string, string> {\n const out: Record<string, string> = {};\n headers.forEach((v, k) => {\n out[k.toLowerCase()] = v;\n });\n return out;\n}\n\nfunction stripContentTypeParams(value?: string): string | undefined {\n if (!value) return undefined;\n const semi = value.indexOf(\";\");\n return (semi === -1 ? value : value.slice(0, semi)).trim().toLowerCase();\n}\n\nfunction relaxTlsIfRequested(enabled: boolean | undefined): () => void {\n if (!enabled) return () => undefined;\n const previous = process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\";\n return () => {\n if (previous === undefined) {\n delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n } else {\n process.env.NODE_TLS_REJECT_UNAUTHORIZED = previous;\n }\n };\n}\n\nfunction describe(err: unknown): string {\n if (err instanceof Error) return err.message;\n return String(err);\n}\n","import type { FetchMeta } from \"../types\";\n\nexport interface HeadlessExtractOptions {\n /** Optional User-Agent override. Defaults to a Goflag UA. */\n userAgent?: string;\n /** Per-navigation timeout in ms. Defaults to 30_000 (Chromium can be slow). */\n timeoutMs?: number;\n /** Allow self-signed TLS certs (mirrors `fetchStatic`'s flag). */\n allowInsecureTls?: boolean;\n /** Wait condition after navigation. Defaults to `\"networkidle\"`. */\n waitUntil?: \"load\" | \"domcontentloaded\" | \"networkidle\" | \"commit\";\n /** Optional AbortSignal for caller-driven cancellation. */\n signal?: AbortSignal;\n /**\n * Override the launcher used to obtain a Chromium browser. Tests inject a\n * mock launcher to avoid downloading the real binary; production uses the\n * default which lazy-imports `playwright`.\n */\n launcher?: HeadlessLauncher;\n}\n\nexport interface HeadlessExtractResult {\n /** Final HTML after `waitUntil` was reached. */\n renderedHtml: string;\n /** Network metadata from the navigation. */\n fetch: FetchMeta;\n}\n\n/**\n * Minimal interface the headless extractor needs from a browser launcher.\n * Letting tests stub this means we never touch the real Playwright binary\n * in the unit suite — we only verify our orchestration. The real Playwright\n * implementation lives behind `defaultLauncher()` and is lazy-loaded.\n */\nexport interface HeadlessLauncher {\n launch(opts: { allowInsecureTls?: boolean }): Promise<HeadlessBrowser>;\n}\n\nexport interface HeadlessBrowser {\n newPage(opts: { userAgent?: string }): Promise<HeadlessPage>;\n close(): Promise<void>;\n}\n\nexport interface HeadlessPage {\n goto(\n url: string,\n opts: { waitUntil: HeadlessExtractOptions[\"waitUntil\"]; timeoutMs: number },\n ): Promise<HeadlessNavigation>;\n content(): Promise<string>;\n close(): Promise<void>;\n}\n\nexport interface HeadlessNavigation {\n status: number;\n statusText: string;\n finalUrl: string;\n headers: Record<string, string>;\n}\n\nconst DEFAULT_UA = \"Goflag/0.0 (+https://github.com/tancredesimonin-indie/goflag; headless)\";\n\n/**\n * Thrown when Playwright (or its Chromium binary) is not available locally.\n * The CLI catches this and prints a friendly install prompt instead of a\n * stack trace.\n */\nexport class HeadlessUnavailableError extends Error {\n override readonly cause?: unknown;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message);\n this.name = \"HeadlessUnavailableError\";\n this.cause = options?.cause;\n }\n}\n\n/**\n * Render `url` in Chromium and return the post-hydration HTML.\n *\n * Implementation notes:\n * - Playwright is loaded lazily via dynamic `import()` so that users who\n * only ever run `goflag inspect --static` never pay the import cost\n * (it pulls in ~80 MB of types and ~15 MB of runtime).\n * - If the import fails (`playwright` not installed) or the browser refuses\n * to launch (Chromium binary missing), we throw `HeadlessUnavailableError`\n * with a one-line install command. The CLI prints that and exits 2.\n * - We accept an `allowInsecureTls` flag for parity with `fetchStatic`, so\n * self-signed `*.local` hosts work the same in both modes.\n */\nexport async function extractHeadless(\n url: string,\n options: HeadlessExtractOptions = {},\n): Promise<HeadlessExtractResult> {\n const launcher = options.launcher ?? defaultLauncher();\n const timeoutMs = options.timeoutMs ?? 30_000;\n const userAgent = options.userAgent ?? DEFAULT_UA;\n const waitUntil = options.waitUntil ?? \"networkidle\";\n\n if (options.signal?.aborted) {\n throw new HeadlessUnavailableError(\"Headless render cancelled before launch\");\n }\n\n const start = performance.now();\n const browser = await launcher.launch({ allowInsecureTls: options.allowInsecureTls });\n let page: HeadlessPage | undefined;\n try {\n page = await browser.newPage({ userAgent });\n const nav = await page.goto(url, { waitUntil, timeoutMs });\n const body = await page.content();\n const meta: FetchMeta = {\n requestedUrl: url,\n finalUrl: nav.finalUrl,\n status: nav.status,\n statusText: nav.statusText,\n headers: nav.headers,\n redirectCount: 0,\n durationMs: Math.round(performance.now() - start),\n bodyBytes: Buffer.byteLength(body, \"utf8\"),\n contentType: nav.headers[\"content-type\"]?.split(\";\")[0]?.trim().toLowerCase(),\n };\n return { renderedHtml: body, fetch: meta };\n } finally {\n await page?.close().catch(() => undefined);\n await browser.close().catch(() => undefined);\n }\n}\n\n/**\n * Async loader for the `playwright` module. Defaulted to a real dynamic\n * import; tests pass a custom loader so they can simulate \"not installed\"\n * without uninstalling the package.\n */\nexport type PlaywrightLoader = () => Promise<typeof import(\"playwright\")>;\n\n/**\n * Default launcher backed by `playwright`. Lazy-imports the package so the\n * cost is only paid when the user actually asks for headless mode.\n *\n * `loader` is injectable so tests can exercise the friendly install-prompt\n * branches without needing to physically uninstall the package.\n */\nexport function defaultLauncher(\n loader: PlaywrightLoader = () => import(\"playwright\"),\n): HeadlessLauncher {\n return {\n async launch({ allowInsecureTls }) {\n let mod: typeof import(\"playwright\");\n try {\n mod = await loader();\n } catch (err) {\n throw new HeadlessUnavailableError(\n \"Headless mode requires the 'playwright' package. Install it with `pnpm add -D playwright` and `pnpm exec playwright install chromium`.\",\n { cause: err },\n );\n }\n\n let pw: import(\"playwright\").Browser;\n try {\n pw = await mod.chromium.launch({\n headless: true,\n args: allowInsecureTls ? [\"--ignore-certificate-errors\"] : [],\n });\n } catch (err) {\n throw new HeadlessUnavailableError(\n \"Could not launch Chromium. Install the browser binary with `pnpm exec playwright install chromium`.\",\n { cause: err },\n );\n }\n\n return {\n async newPage({ userAgent }) {\n const ctx = await pw.newContext({\n userAgent,\n ignoreHTTPSErrors: !!allowInsecureTls,\n });\n const p = await ctx.newPage();\n return {\n async goto(url, opts) {\n const response = await p.goto(url, {\n waitUntil: opts.waitUntil,\n timeout: opts.timeoutMs,\n });\n const status = response?.status() ?? 0;\n const statusText = response?.statusText() ?? \"\";\n const headers = response ? await response.allHeaders() : {};\n return {\n status,\n statusText,\n finalUrl: p.url(),\n headers,\n };\n },\n async content() {\n return p.content();\n },\n async close() {\n await ctx.close();\n },\n };\n },\n async close() {\n await pw.close();\n },\n };\n },\n };\n}\n","import type { Page } from \"../types\";\n\n/**\n * Decide whether a page that came back from the static fetcher looks like\n * its real metadata is injected by client-side JS (typical of Vue / React /\n * Angular SPAs that don't pre-render their `<head>`).\n *\n * The heuristic is intentionally conservative: a false negative means we\n * miss an SPA and the user has to pass `--headless` manually. A false\n * positive means we boot Chromium for a page that didn't need it, which\n * costs ~1–2 s and a Playwright browser. We bias toward avoiding the boot.\n *\n * A page is treated as \"looks client-rendered\" iff *every* discriminating\n * signal is missing — no title, no description, no canonical, no OG, no\n * Twitter, no JSON-LD, no hreflang. That excludes:\n *\n * - SSR'd pages with a real title and a description (the common case).\n * - Static landing pages that have at least one OG tag.\n * - Anything that ships a JSON-LD block (Wordpress, Astro, Hugo, etc.).\n *\n * It catches the textbook SPA case: `<head>` has charset + viewport +\n * a `<title>` like \"React App\" + a single `<div id=\"root\">` in the body.\n */\nexport interface ClientRenderedSignal {\n /** True if the page looks like a client-rendered SPA. */\n likely: boolean;\n /** Human-readable explanation, used in escalation logs and Page.extractor. */\n reason: string;\n}\n\nconst PLACEHOLDER_TITLES = new Set([\n \"react app\",\n \"vue app\",\n \"vite app\",\n \"create react app\",\n \"next.js\",\n \"nuxt\",\n \"angular\",\n \"ember\",\n \"webpack app\",\n \"document\",\n \"untitled\",\n \"\",\n]);\n\nexport function looksClientRendered(\n partial: Pick<Page, \"raw\" | \"meta\" | \"openGraph\" | \"twitter\" | \"links\" | \"jsonLd\">,\n): ClientRenderedSignal {\n const reasons: string[] = [];\n\n const titleRaw = partial.raw.title?.trim().toLowerCase() ?? \"\";\n const titleMissing = titleRaw === \"\";\n const titlePlaceholder = !titleMissing && PLACEHOLDER_TITLES.has(titleRaw);\n if (titleMissing) reasons.push(\"title missing\");\n else if (titlePlaceholder) reasons.push(`placeholder title \"${partial.raw.title}\"`);\n\n const descMissing = partial.meta.description === undefined;\n if (descMissing) reasons.push(\"no description\");\n\n const canonicalMissing = partial.meta.canonical === undefined;\n if (canonicalMissing) reasons.push(\"no canonical\");\n\n const ogMissing =\n partial.openGraph.title === undefined &&\n partial.openGraph.description === undefined &&\n partial.openGraph.images.length === 0 &&\n partial.openGraph.type === undefined &&\n partial.openGraph.url === undefined &&\n partial.openGraph.siteName === undefined &&\n partial.openGraph.unknown.length === 0;\n if (ogMissing) reasons.push(\"no og:*\");\n\n const twitterMissing =\n partial.twitter.card === undefined &&\n partial.twitter.title === undefined &&\n partial.twitter.image === undefined;\n if (twitterMissing) reasons.push(\"no twitter:*\");\n\n const jsonLdMissing = partial.jsonLd.length === 0;\n if (jsonLdMissing) reasons.push(\"no JSON-LD\");\n\n const hreflangMissing = partial.links.alternates.length === 0;\n if (hreflangMissing) reasons.push(\"no hreflang\");\n\n // Only escalate when *all* signals fire simultaneously — that's the SPA\n // shape. A page missing only the description but with a real OG card is\n // SSR'd, just incomplete; that's a job for the rule engine, not headless.\n const likely =\n (titleMissing || titlePlaceholder) &&\n descMissing &&\n canonicalMissing &&\n ogMissing &&\n twitterMissing &&\n jsonLdMissing &&\n hreflangMissing;\n\n return {\n likely,\n reason: reasons.join(\", \") || \"all metadata present\",\n };\n}\n","import type { HydrationDelta, RawLinkTag, RawMetaTag, Page } from \"../types\";\n\ntype ParsedSubset = Pick<Page, \"raw\" | \"jsonLd\">;\n\n/**\n * Compute what changed between the static-pass parse and the headless-pass\n * parse of the same URL.\n *\n * The output is intentionally lossless for tags but small in absolute size:\n * we keep just the identifying attributes (`name` / `property` / `httpEquiv`\n * / `rel` / `hreflang`) and the raw `content` / `href` so Phase 5 rules can\n * say things like \"your `og:image` is client-injected — Facebook's crawler\n * does not run JS\". The full HTML for both passes is still available on\n * `Page.html` for richer downstream uses.\n */\nexport function computeHydrationDelta(\n staticPass: ParsedSubset,\n renderedPass: ParsedSubset,\n): HydrationDelta {\n const staticMetaKeys = new Set(staticPass.raw.metas.map(metaKey));\n const renderedMetaKeys = new Set(renderedPass.raw.metas.map(metaKey));\n\n const clientInjectedMetas = renderedPass.raw.metas\n .filter((m) => !staticMetaKeys.has(metaKey(m)))\n .map(toMetaSummary);\n const clientRemovedMetas = staticPass.raw.metas\n .filter((m) => !renderedMetaKeys.has(metaKey(m)))\n .map(toMetaSummary);\n\n const staticLinkKeys = new Set(staticPass.raw.links.map(linkKey));\n const renderedLinkKeys = new Set(renderedPass.raw.links.map(linkKey));\n\n const clientInjectedLinks = renderedPass.raw.links\n .filter((l) => !staticLinkKeys.has(linkKey(l)))\n .map(toLinkSummary);\n const clientRemovedLinks = staticPass.raw.links\n .filter((l) => !renderedLinkKeys.has(linkKey(l)))\n .map(toLinkSummary);\n\n return {\n fromMode: \"static\",\n toMode: \"headless\",\n titleChanged: (staticPass.raw.title ?? \"\") !== (renderedPass.raw.title ?? \"\"),\n htmlLangChanged: (staticPass.raw.htmlLang ?? \"\") !== (renderedPass.raw.htmlLang ?? \"\"),\n clientInjectedMetas,\n clientRemovedMetas,\n clientInjectedLinks,\n clientRemovedLinks,\n jsonLdBlocksAdded: Math.max(0, renderedPass.jsonLd.length - staticPass.jsonLd.length),\n };\n}\n\nfunction metaKey(m: RawMetaTag): string {\n // Identity for \"is this the same meta tag?\" purposes. We deliberately\n // include `content` so that a tag whose value changed counts as both\n // a removal (old value) and an injection (new value) — that matches the\n // intent of the rules: \"the value Slackbot sees differs from the one a\n // browser sees\".\n return [\n \"n=\" + (m.name ?? \"\"),\n \"p=\" + (m.property ?? \"\"),\n \"he=\" + (m.httpEquiv ?? \"\"),\n \"ch=\" + (m.charset ?? \"\"),\n \"c=\" + (m.content ?? \"\"),\n ].join(\"|\");\n}\n\nfunction linkKey(l: RawLinkTag): string {\n return [\n \"r=\" + (l.rel ?? \"\"),\n \"h=\" + (l.href ?? \"\"),\n \"hl=\" + (l.hreflang ?? \"\"),\n \"t=\" + (l.type ?? \"\"),\n \"s=\" + (l.sizes ?? \"\"),\n ].join(\"|\");\n}\n\nfunction toMetaSummary(m: RawMetaTag) {\n const out: HydrationDelta[\"clientInjectedMetas\"][number] = {};\n if (m.name) out.name = m.name;\n if (m.property) out.property = m.property;\n if (m.httpEquiv) out.httpEquiv = m.httpEquiv;\n if (m.content !== undefined) out.content = m.content;\n return out;\n}\n\nfunction toLinkSummary(l: RawLinkTag) {\n const out: HydrationDelta[\"clientInjectedLinks\"][number] = { rel: l.rel };\n if (l.href !== undefined) out.href = l.href;\n if (l.hreflang !== undefined) out.hreflang = l.hreflang;\n return out;\n}\n","/**\n * Compose an optional caller-driven `AbortSignal` with a timeout deadline into\n * a single signal that triggers when either fires. Returns the combined signal\n * plus a `cleanup` callback that the caller MUST run in `finally` to clear the\n * timer and detach the listener.\n *\n * Lifted out of the individual probes so the abort plumbing has one tested\n * implementation and one set of branches the coverage gate can reason about.\n */\nexport function combineSignals(\n caller: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cleanup: () => void } {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n if (!caller) {\n return { signal: controller.signal, cleanup: () => clearTimeout(timer) };\n }\n if (caller.aborted) {\n controller.abort();\n return { signal: controller.signal, cleanup: () => clearTimeout(timer) };\n }\n const onAbort = () => controller.abort();\n caller.addEventListener(\"abort\", onAbort, { once: true });\n return {\n signal: controller.signal,\n cleanup: () => {\n clearTimeout(timer);\n caller.removeEventListener(\"abort\", onAbort);\n },\n };\n}\n","import type { ManifestProbe } from \"../types\";\nimport { combineSignals } from \"./abort\";\n\nexport interface ManifestProbeOptions {\n signal?: AbortSignal;\n timeoutMs?: number;\n}\n\n/**\n * Fetch a `<link rel=\"manifest\" href>` payload and try to parse it as JSON.\n * Returns a `ManifestProbe` with `found: false` for any non-2xx response or\n * network error; `parseError` is set if the body isn't valid JSON.\n */\nexport async function probeManifest(\n href: string,\n options: ManifestProbeOptions = {},\n): Promise<ManifestProbe> {\n const { signal, cleanup } = combineSignals(options.signal, options.timeoutMs ?? 5_000);\n try {\n const res = await fetch(href, { signal, redirect: \"follow\" });\n if (!res.ok) return { url: href, status: res.status, found: false };\n const raw = await res.text();\n let data: unknown;\n let parseError: string | undefined;\n try {\n data = JSON.parse(raw);\n } catch (err) {\n parseError = err instanceof Error ? err.message : String(err);\n }\n return { url: href, status: res.status, found: true, raw, data, parseError };\n } catch {\n return { url: href, status: 0, found: false };\n } finally {\n cleanup();\n }\n}\n","import type { RobotsProbe } from \"../types\";\nimport { combineSignals } from \"./abort\";\n\nexport interface RobotsProbeOptions {\n signal?: AbortSignal;\n timeoutMs?: number;\n}\n\nexport async function probeRobots(\n origin: string,\n options: RobotsProbeOptions = {},\n): Promise<RobotsProbe> {\n const url = new URL(\"/robots.txt\", origin).toString();\n const { signal, cleanup } = combineSignals(options.signal, options.timeoutMs ?? 5_000);\n\n try {\n const res = await fetch(url, { signal, redirect: \"follow\" });\n if (!res.ok) {\n return { url, status: res.status, found: false, sitemaps: [], blocksAll: false };\n }\n const body = await res.text();\n return {\n url,\n status: res.status,\n found: true,\n raw: body,\n sitemaps: extractSitemaps(body),\n blocksAll: blocksAllUserAgents(body),\n };\n } catch {\n return { url, status: 0, found: false, sitemaps: [], blocksAll: false };\n } finally {\n cleanup();\n }\n}\n\nexport function extractSitemaps(body: string): string[] {\n const out: string[] = [];\n for (const line of body.split(/\\r?\\n/)) {\n const m = /^\\s*Sitemap:\\s*(\\S+)\\s*$/i.exec(line);\n if (m?.[1]) out.push(m[1]);\n }\n return out;\n}\n\n/**\n * Detect whether the file disallows everything for `User-agent: *`.\n * Walks line-by-line, tracking the active group; returns true the moment we\n * see `Disallow: /` (with no path beyond `/`) inside a `*` group.\n */\nexport function blocksAllUserAgents(body: string): boolean {\n let inWildcard = false;\n for (const rawLine of body.split(/\\r?\\n/)) {\n const line = rawLine.replace(/#.*$/, \"\").trim();\n if (line.length === 0) continue;\n const ua = /^User-agent:\\s*(.+)$/i.exec(line);\n if (ua) {\n inWildcard = ua[1]!.trim() === \"*\";\n continue;\n }\n if (!inWildcard) continue;\n const dis = /^Disallow:\\s*(.*)$/i.exec(line);\n if (dis && dis[1]!.trim() === \"/\") return true;\n }\n return false;\n}\n","import type { SitemapProbe } from \"../types\";\nimport { combineSignals } from \"./abort\";\n\nexport interface SitemapProbeOptions {\n signal?: AbortSignal;\n timeoutMs?: number;\n}\n\nexport async function probeSitemap(\n origin: string,\n options: SitemapProbeOptions = {},\n): Promise<SitemapProbe> {\n const url = new URL(\"/sitemap.xml\", origin).toString();\n const { signal, cleanup } = combineSignals(options.signal, options.timeoutMs ?? 5_000);\n\n try {\n const res = await fetch(url, { signal, redirect: \"follow\" });\n if (!res.ok) return { url, status: res.status, found: false, isIndex: false, entryCount: 0 };\n const body = await res.text();\n return {\n url,\n status: res.status,\n found: true,\n isIndex: /<sitemapindex\\b/i.test(body),\n entryCount: countEntries(body),\n };\n } catch {\n return { url, status: 0, found: false, isIndex: false, entryCount: 0 };\n } finally {\n cleanup();\n }\n}\n\nexport function countEntries(body: string): number {\n const url = body.match(/<url\\b/gi)?.length ?? 0;\n const sm = body.match(/<sitemap\\b/gi)?.length ?? 0;\n return url + sm;\n}\n","import type { ExtractorMode, FetchMeta, Page } from \"./types\";\n\n/**\n * Inspection strategy passed to `inspect()`. Re-exported as a top-level\n * type so callers (CLI, App Router routes, future SDK) can import it\n * without reaching into individual extractor modules.\n */\nexport type InspectMode = \"auto\" | ExtractorMode;\nimport { extractStatic } from \"./extract/static\";\nimport { fetchStatic, type FetchStaticOptions } from \"./fetch/static\";\nimport {\n extractHeadless,\n HeadlessUnavailableError,\n type HeadlessExtractOptions,\n} from \"./extract/headless\";\nimport { looksClientRendered } from \"./extract/heuristics\";\nimport { computeHydrationDelta } from \"./extract/hydration\";\nimport { probeManifest } from \"./probes/manifest\";\nimport { probeRobots } from \"./probes/robots\";\nimport { probeSitemap } from \"./probes/sitemap\";\n\nexport interface InspectOptions extends FetchStaticOptions {\n /**\n * Whether to fetch `/robots.txt`, `/sitemap.xml`, and the linked manifest.\n * Defaults to `true`. Disable in tests that only care about the HTML.\n */\n probes?: boolean;\n /**\n * Extraction strategy:\n * - `\"static\"` — fetch + parse only, never boot Chromium. Fastest, but\n * misses anything injected by client JS.\n * - `\"headless\"` — boot Chromium and parse the post-hydration HTML.\n * Always sees client-injected tags. Pays ~1–2 s of browser boot.\n * - `\"auto\"` (default) — start in static mode; if the `<head>` looks\n * like an unhydrated SPA shell (no title / og / canonical / json-ld /\n * hreflang), automatically re-run in headless mode.\n */\n mode?: \"auto\" | ExtractorMode;\n /**\n * Options forwarded to the headless extractor when it runs. Tests inject\n * `launcher` here to avoid touching the real Playwright binary.\n */\n headless?: Pick<HeadlessExtractOptions, \"launcher\" | \"waitUntil\" | \"timeoutMs\">;\n}\n\n/**\n * High-level orchestrator: fetch the URL, parse the HTML, optionally render\n * it in Chromium for SPA support, run the side-channel probes, and stitch\n * everything into a complete `Page`.\n *\n * This is the single entry point shared by the CLI (`goflag inspect`),\n * the UI Server Action (Phase 3), and the snapshot/diff layers (Phases 9+).\n * Keeping it thin and pure means we never have to maintain two divergent\n * code paths between local UI and CI.\n */\nexport async function inspect(url: string, options: InspectOptions = {}): Promise<Page> {\n const {\n probes: enableProbes = true,\n mode = \"auto\",\n headless: headlessOpts,\n ...fetchOptions\n } = options;\n\n const fetchedAt = new Date().toISOString();\n\n // Headless-only mode skips the static fetch entirely. We still need a\n // FetchMeta, which the headless extractor provides from the navigation.\n if (mode === \"headless\") {\n return inspectHeadlessOnly({\n url,\n fetchedAt,\n enableProbes,\n fetchOptions,\n headlessOpts,\n });\n }\n\n // Both `\"static\"` and `\"auto\"` start with a static fetch + parse.\n const { meta: staticMeta, body: staticBody } = await fetchStatic(url, fetchOptions);\n const staticParsed = extractStatic(staticBody, { baseUrl: staticMeta.finalUrl });\n\n if (mode === \"static\") {\n const probes = await runProbes({\n enabled: enableProbes,\n origin: new URL(staticMeta.finalUrl).origin,\n manifestHref: staticParsed.links.manifest?.href,\n signal: fetchOptions.signal,\n });\n return {\n ...staticParsed,\n fetchedAt,\n fetch: staticMeta,\n extractor: { mode: \"static\", escalated: false },\n html: { static: staticBody },\n probes,\n };\n }\n\n // Auto: decide whether to escalate.\n const heuristic = looksClientRendered(staticParsed);\n if (!heuristic.likely) {\n const probes = await runProbes({\n enabled: enableProbes,\n origin: new URL(staticMeta.finalUrl).origin,\n manifestHref: staticParsed.links.manifest?.href,\n signal: fetchOptions.signal,\n });\n return {\n ...staticParsed,\n fetchedAt,\n fetch: staticMeta,\n extractor: { mode: \"static\", escalated: false },\n html: { static: staticBody },\n probes,\n };\n }\n\n // Escalation path: render in Chromium and reparse. If headless is\n // unavailable, fall back to the static result with a clear marker — the\n // CLI surfaces a hint, but we never crash a default `inspect` run just\n // because Playwright is missing.\n let renderedBody: string | undefined;\n let renderedFetch: FetchMeta | undefined;\n try {\n const result = await extractHeadless(url, {\n ...headlessOpts,\n allowInsecureTls: fetchOptions.allowInsecureTls,\n signal: fetchOptions.signal,\n });\n renderedBody = result.renderedHtml;\n renderedFetch = result.fetch;\n } catch (err) {\n if (!(err instanceof HeadlessUnavailableError)) throw err;\n const probes = await runProbes({\n enabled: enableProbes,\n origin: new URL(staticMeta.finalUrl).origin,\n manifestHref: staticParsed.links.manifest?.href,\n signal: fetchOptions.signal,\n });\n return {\n ...staticParsed,\n fetchedAt,\n fetch: staticMeta,\n extractor: {\n mode: \"static\",\n escalated: false,\n escalationReason: `would-have-escalated (${heuristic.reason}) but ${err.message}`,\n },\n html: { static: staticBody },\n probes,\n };\n }\n\n const renderedParsed = extractStatic(renderedBody, { baseUrl: renderedFetch.finalUrl });\n const hydration = computeHydrationDelta(staticParsed, renderedParsed);\n const probes = await runProbes({\n enabled: enableProbes,\n origin: new URL(renderedFetch.finalUrl).origin,\n manifestHref: renderedParsed.links.manifest?.href,\n signal: fetchOptions.signal,\n });\n\n return {\n ...renderedParsed,\n fetchedAt,\n fetch: renderedFetch,\n extractor: {\n mode: \"headless\",\n escalated: true,\n escalationReason: heuristic.reason,\n },\n html: { static: staticBody, rendered: renderedBody },\n hydration,\n probes,\n };\n}\n\nasync function inspectHeadlessOnly(args: {\n url: string;\n fetchedAt: string;\n enableProbes: boolean;\n fetchOptions: FetchStaticOptions;\n headlessOpts: InspectOptions[\"headless\"];\n}): Promise<Page> {\n const { url, fetchedAt, enableProbes, fetchOptions, headlessOpts } = args;\n const result = await extractHeadless(url, {\n ...headlessOpts,\n allowInsecureTls: fetchOptions.allowInsecureTls,\n signal: fetchOptions.signal,\n });\n const parsed = extractStatic(result.renderedHtml, { baseUrl: result.fetch.finalUrl });\n const probes = await runProbes({\n enabled: enableProbes,\n origin: new URL(result.fetch.finalUrl).origin,\n manifestHref: parsed.links.manifest?.href,\n signal: fetchOptions.signal,\n });\n return {\n ...parsed,\n fetchedAt,\n fetch: result.fetch,\n extractor: { mode: \"headless\", escalated: false },\n html: { static: \"\", rendered: result.renderedHtml },\n probes,\n };\n}\n\nasync function runProbes(args: {\n enabled: boolean;\n origin: string;\n manifestHref: string | undefined;\n signal: AbortSignal | undefined;\n}): Promise<Page[\"probes\"]> {\n if (!args.enabled) return {};\n const [robots, sitemap, manifest] = await Promise.all([\n probeRobots(args.origin, { signal: args.signal }),\n probeSitemap(args.origin, { signal: args.signal }),\n args.manifestHref\n ? probeManifest(args.manifestHref, { signal: args.signal })\n : Promise.resolve(undefined),\n ]);\n const probes: Page[\"probes\"] = { robots, sitemap };\n if (manifest) probes.manifest = manifest;\n return probes;\n}\n","/**\n * Tiny glob-to-regex matcher used by the Phase 7 crawler include /\n * exclude filters.\n *\n * Supports the small subset of patterns Goflag actually advertises:\n *\n * - `*` matches any run of characters, NOT crossing `/`\n * - `**` matches any run of characters, including `/`\n * - `?` matches a single character (not `/`)\n * - everything else is literal\n *\n * No brace expansion, no character classes, no negation. The matcher\n * is anchored: `*.html` only matches `*.html`, not `/foo.html`. Add a\n * leading `**` (`**.html`) when you actually want a \"any path ending\n * in .html\" filter.\n *\n * Patterns are pre-compiled to RegExps so the BFS hot loop only runs\n * `RegExp.test` per candidate instead of re-tokenising on every check.\n */\n\nconst cache = new Map<string, RegExp>();\n\nexport function compileGlob(pattern: string): RegExp {\n const cached = cache.get(pattern);\n if (cached) return cached;\n let re = \"^\";\n let i = 0;\n while (i < pattern.length) {\n const c = pattern[i]!;\n if (c === \"*\") {\n if (pattern[i + 1] === \"*\") {\n re += \".*\";\n i += 2;\n continue;\n }\n re += \"[^/]*\";\n i += 1;\n continue;\n }\n if (c === \"?\") {\n re += \"[^/]\";\n i += 1;\n continue;\n }\n if (/[.+^${}()|\\\\[\\]/]/.test(c)) {\n re += `\\\\${c}`;\n } else {\n re += c;\n }\n i += 1;\n }\n re += \"$\";\n const compiled = new RegExp(re);\n cache.set(pattern, compiled);\n return compiled;\n}\n\nexport function matchesAny(value: string, patterns: string[]): boolean {\n for (const p of patterns) {\n if (compileGlob(p).test(value)) return true;\n }\n return false;\n}\n","/**\n * Crawl-time link discovery.\n *\n * Given a fully-inspected `Page`, returns the set of candidate URLs\n * the BFS frontier should consider. Every candidate carries its\n * `source` so the crawler can decide whether to bypass include/exclude\n * filters (we always follow `hreflang` siblings, see `crawl.ts`).\n *\n * Sources, in order of trust:\n *\n * 1. `hreflang` — `Page.links.alternates`. Already parsed by\n * the static extractor with absolute hrefs.\n * 2. `head-link` — `<link rel=\"next\">` / `<link rel=\"prev\">`,\n * read from the raw inventory so we don't\n * have to re-parse the HTML.\n * 3. `body-anchor` — `<a href>` from the static body. Cheap\n * regex extraction; we deliberately don't\n * parse the body with cheerio because it'd\n * double the per-page CPU cost on a large\n * crawl and we don't need attributes beyond\n * `href`.\n *\n * Returned `href` is always absolute when possible (resolved against\n * `page.fetch.finalUrl`); the crawler runs canonicalisation again\n * before deduplication.\n */\n\nimport type { Page } from \"./types\";\n\nexport type CandidateSource = \"hreflang\" | \"head-link\" | \"body-anchor\";\n\nexport interface CandidateLink {\n href: string;\n source: CandidateSource;\n}\n\nconst ANCHOR_HREF_RE = /<a\\b[^>]*?\\bhref\\s*=\\s*(\"([^\"]*)\"|'([^']*)')/gi;\n\nexport function extractCandidateLinks(page: Page): CandidateLink[] {\n const out: CandidateLink[] = [];\n const seen = new Set<string>();\n const base = page.fetch.finalUrl;\n\n for (const alt of page.links.alternates) {\n push(out, seen, alt.href, \"hreflang\");\n }\n\n for (const link of page.raw.links) {\n const rel = link.rel.toLowerCase();\n if ((rel === \"next\" || rel === \"prev\") && link.href) {\n push(out, seen, link.href, \"head-link\");\n }\n }\n\n // Body anchors. We restrict to `<a href>` — relative URLs get\n // resolved against `base` (the page's final URL after redirects).\n if (page.html.static) {\n for (const match of page.html.static.matchAll(ANCHOR_HREF_RE)) {\n const raw = match[2] ?? match[3];\n if (!raw) continue;\n const trimmed = raw.trim();\n if (!trimmed) continue;\n let absolute: string;\n try {\n absolute = new URL(trimmed, base).toString();\n } catch {\n continue;\n }\n push(out, seen, absolute, \"body-anchor\");\n }\n }\n\n return out;\n}\n\nfunction push(\n out: CandidateLink[],\n seen: Set<string>,\n href: string,\n source: CandidateSource,\n): void {\n // Cheap dedupe per page; the BFS keeps a global visited set.\n if (seen.has(href)) return;\n seen.add(href);\n out.push({ href, source });\n}\n","/**\n * Phase 7 crawler.\n *\n * Walks a site BFS-style starting from `entryUrl`, calling `inspect()`\n * on every URL it visits. Discovery uses three sources, in this order\n * of trust:\n *\n * 1. `<link rel=\"alternate\" hreflang=\"...\">` siblings on every\n * visited page — Goflag's primary use-case is i18n surfaces, so\n * we always follow these even when they wouldn't pass an\n * include filter (otherwise the i18n matrix would have\n * systematic blind spots).\n * 2. `<a href>` anchors in the static HTML body. We deliberately\n * stay on the static document (no Chromium re-render per\n * candidate) — discovery doesn't need hydration; if a\n * candidate URL itself looks SPA-shaped, `inspect()` will\n * escalate to headless when we reach it.\n * 3. `<link rel=\"next\">` and `<link rel=\"prev\">` so paginated\n * indexes (blog list pages) link forward into article pages.\n *\n * Same-origin filter: only URLs whose origin matches `entryUrl`'s\n * origin are followed. Glob include/exclude are applied to the URL's\n * pathname (not the full URL) using a small Bash-like matcher\n * (`./glob.ts`). Trailing-slash variants and `#fragment` are\n * normalized away; query strings are kept (they often carry meaning\n * for hreflang variants like `?hl=fr`).\n *\n * The crawler is intentionally polite by default:\n *\n * - `concurrency: 4` parallel fetches.\n * - `maxPages: 200` hard cap so a misconfigured `--depth 99` can't\n * run forever.\n * - `respectRobots: true` (Phase 7.7 follow-up — for now we always\n * send the standard browser UA via `inspect`).\n */\n\nimport { inspect, type InspectOptions } from \"./inspect\";\nimport type { Page } from \"./types\";\nimport { matchesAny } from \"./glob\";\nimport { extractCandidateLinks } from \"./discover\";\n\nexport interface CrawlOptions {\n /** Entry-point URL the crawl starts from. Must be absolute http/https. */\n entryUrl: string;\n /**\n * Extra same-origin URLs to enqueue alongside the entry point, at depth 0.\n *\n * Discovery via links alone is circular for i18n: a site that declares no\n * `hreflang` gives the BFS nothing to follow into its other locales, so the\n * frontier never leaves the entry locale and the i18n matrix collapses to a\n * single column. Seeding from an independent artefact — the sitemap — breaks\n * that loop. Seeds bypass include/exclude for the same reason hreflang\n * siblings do: a filter must not reintroduce a systematic blind spot.\n */\n seedUrls?: readonly string[];\n /**\n * BFS depth. `0` means \"only the entry page\". `1` means \"entry +\n * its direct children\". Defaults to `1` (matches `goflag inspect\n * --crawl --depth 1`).\n */\n depth?: number;\n /** Glob include filter applied to the URL pathname. */\n include?: string[];\n /** Glob exclude filter applied to the URL pathname. */\n exclude?: string[];\n /** Maximum number of in-flight inspections. Defaults to 4. */\n concurrency?: number;\n /** Hard safety cap on visited pages. Defaults to 200. */\n maxPages?: number;\n /**\n * Whether to follow `<link rel=\"alternate\" hreflang=\"...\">` siblings\n * regardless of include/exclude filters. Defaults to `true` so the\n * i18n matrix is never artificially incomplete.\n */\n followHreflang?: boolean;\n /**\n * Optional progress callback fired after each successful inspect.\n * Useful for streaming progress lines to the CLI while the crawl\n * is still running.\n */\n onPage?: (page: Page, ctx: { visited: number; queued: number }) => void;\n /** Forwarded to every per-page `inspect()` call. */\n inspectOptions?: Omit<InspectOptions, \"signal\"> & { signal?: AbortSignal };\n}\n\nexport interface CrawlError {\n url: string;\n message: string;\n}\n\nexport interface CrawlResult {\n /** Pages that successfully inspected, in BFS visit order. */\n pages: Page[];\n /** Canonicalized URLs that were either visited or attempted. */\n visited: string[];\n /** Per-URL failures with the engine's error message. */\n errors: CrawlError[];\n /** True when the crawl stopped because `maxPages` was hit. */\n truncated: boolean;\n}\n\ninterface QueueItem {\n url: string;\n depth: number;\n}\n\n/**\n * Public entry point. Resolves once the BFS frontier is empty, the\n * `maxPages` cap is hit, or the abort signal fires. Never throws — all\n * per-page failures are collected into `result.errors` so a single\n * 500 deep in the site can't take the whole crawl down.\n */\nexport async function crawl(options: CrawlOptions): Promise<CrawlResult> {\n const depthLimit = options.depth ?? 1;\n const concurrency = Math.max(1, options.concurrency ?? 4);\n const maxPages = Math.max(1, options.maxPages ?? 200);\n const followHreflang = options.followHreflang !== false;\n const include = options.include ?? [];\n const exclude = options.exclude ?? [];\n\n const entry = canonicaliseUrl(options.entryUrl);\n if (!entry) {\n return {\n pages: [],\n visited: [],\n errors: [{ url: options.entryUrl, message: \"entry URL is not a valid http(s) URL\" }],\n truncated: false,\n };\n }\n const origin = new URL(entry).origin;\n\n const visitedSet = new Set<string>([entry]);\n const queue: QueueItem[] = [{ url: entry, depth: 0 }];\n\n // Seeds join the entry at depth 0 so their own children remain reachable\n // within `depth`. Off-origin and unparseable seeds are dropped silently —\n // a sitemap listing another host is the sitemap's problem, not the crawl's.\n for (const seed of options.seedUrls ?? []) {\n const normalised = canonicaliseUrl(seed);\n if (!normalised || visitedSet.has(normalised)) continue;\n if (new URL(normalised).origin !== origin) continue;\n visitedSet.add(normalised);\n queue.push({ url: normalised, depth: 0 });\n }\n\n const pages: Page[] = [];\n const errors: CrawlError[] = [];\n let truncated = false;\n\n // `visitedSet` holds the URLs we *asked* for. This one holds the URLs we were\n // *given*, and they are not the same set: a redirect makes two requests land\n // on one document. `/` → `/en` while `/en` is also in the sitemap, `/stet` →\n // `/stet/1.6.3`, `/privacy-policy` → `/en/privacy-policy`. Both requests\n // succeed, both return the same page, and without this the crawl keeps both.\n //\n // Keeping both is not cosmetic. The page is linted twice, so every finding on\n // it is emitted twice — with the same fingerprint, because the fingerprint is\n // built from the final URL. `--max-debt` counts the copies (32 on tancredo\n // for 31 real findings) while a baseline diff collapses them, so one of a\n // pair can disappear and the gate sees nothing. The duplicate also spends a\n // slot of the page budget on a document already audited: openfinanceguide\n // crawled 753 pages to cover 748.\n //\n // First answer wins. The pages are identical, so which one is arbitrary.\n //\n // Filled only from pages actually kept — seeding it with the entry URL would\n // pre-reject the document the entry resolves to, which is the one page every\n // crawl must have.\n const seenFinal = new Set<string>();\n\n while (queue.length > 0) {\n if (options.inspectOptions?.signal?.aborted) break;\n\n // Drain up to `concurrency` items per wave; preserves BFS order\n // because every item in this wave came from the same depth or\n // earlier than every item still in the queue.\n const wave = queue.splice(0, concurrency);\n type WaveResult =\n { kind: \"ok\"; item: QueueItem; page: Page } | { kind: \"err\"; item: QueueItem; error: string };\n const inspections: Promise<WaveResult>[] = wave.map(async (item) => {\n try {\n const page = await inspect(item.url, options.inspectOptions);\n return { kind: \"ok\" as const, item, page };\n } catch (err) {\n return {\n kind: \"err\" as const,\n item,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n });\n\n const results = await Promise.all(inspections);\n for (const result of results) {\n if (result.kind === \"err\") {\n errors.push({ url: result.item.url, message: result.error });\n continue;\n }\n\n // A document already answered for. Its links were extracted from the\n // copy we kept, so there is nothing here the frontier has not seen.\n //\n // That holds because the queue is BFS and never goes back up a level: the\n // copy we kept is always the shallowest one, so it is the copy that had\n // the most depth budget left to follow links with. Drop that ordering and\n // this becomes a way to lose a subtree.\n const finalUrl = canonicaliseUrl(result.page.fetch.finalUrl) ?? result.page.fetch.finalUrl;\n if (seenFinal.has(finalUrl)) continue;\n seenFinal.add(finalUrl);\n\n pages.push(result.page);\n if (options.onPage) {\n options.onPage(result.page, { visited: pages.length, queued: queue.length });\n }\n if (pages.length >= maxPages) {\n truncated = true;\n break;\n }\n\n if (result.item.depth >= depthLimit) continue;\n\n const candidates = extractCandidateLinks(result.page);\n for (const candidate of candidates) {\n const normalised = canonicaliseUrl(candidate.href, result.page.fetch.finalUrl);\n if (!normalised) continue;\n if (visitedSet.has(normalised)) continue;\n const parsed = new URL(normalised);\n if (parsed.origin !== origin) continue;\n\n const passesIncludeExclude = filterAllows(parsed.pathname, include, exclude);\n const isHreflang = candidate.source === \"hreflang\";\n if (!passesIncludeExclude && !(followHreflang && isHreflang)) continue;\n\n visitedSet.add(normalised);\n queue.push({ url: normalised, depth: result.item.depth + 1 });\n }\n }\n\n if (truncated) break;\n }\n\n return {\n pages,\n visited: [...visitedSet],\n errors,\n truncated,\n };\n}\n\nfunction filterAllows(pathname: string, include: string[], exclude: string[]): boolean {\n if (exclude.length > 0 && matchesAny(pathname, exclude)) return false;\n if (include.length === 0) return true;\n return matchesAny(pathname, include);\n}\n\nexport interface CanonicaliseOptions {\n /**\n * Keep a trailing slash instead of dropping it.\n *\n * Dropping it is right for the crawl frontier: `/about` and `/about/` are\n * almost always the same page, and collapsing them halves the work. It is\n * wrong for link checking, where the job is to answer \"does the URL the\n * author wrote resolve?\" — and the slash is significant on plenty of\n * servers. EUR-Lex is the case that caught us: `/legal-content/FR/TXT/?uri=…`\n * returns 200 and `/legal-content/FR/TXT?uri=…` returns 404, so normalising\n * turned 159 healthy citations into phantom broken links.\n */\n preserveTrailingSlash?: boolean;\n}\n\n/**\n * Resolve `href` against `base` (when given), strip the fragment, drop\n * a trailing slash unless the path is just `/`, and return the\n * canonical string. Returns `null` for non-http(s), invalid, or\n * obviously useless URLs (mailto:, tel:, javascript:).\n */\nexport function canonicaliseUrl(\n href: string,\n base?: string,\n options: CanonicaliseOptions = {},\n): string | null {\n let url: URL;\n try {\n url = new URL(href, base);\n } catch {\n return null;\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return null;\n url.hash = \"\";\n if (!options.preserveTrailingSlash && url.pathname.length > 1 && url.pathname.endsWith(\"/\")) {\n url.pathname = url.pathname.slice(0, -1);\n }\n return url.toString();\n}\n","/**\n * The rule runner — evaluates descriptors against one `Extraction`.\n *\n * Pure: `Extraction` → `RuleFinding[]`, one finding per rule, passing and\n * `na` verdicts included. This unfiltered list *is* the conformance view\n * (plan §10); `findingsToIssues` narrows it to violations and converts to\n * the legacy `Issue` shape at the report boundary.\n *\n * Crash isolation is preserved from the old engine: a rule that throws is\n * reported as a crash, never as a verdict, and never takes the run down —\n * the CLI exit code depends on a successful walk over every rule.\n */\n\nimport type { Issue } from \"../core/types\";\nimport type { Band, Extraction, Rule, RuleFinding, ScoredRule } from \"./types\";\n\n/** A rule whose evaluator threw. Not a verdict — a bug in the rule. */\nexport interface RuleCrash {\n ruleId: string;\n message: string;\n}\n\nexport interface EvaluationResult {\n /** One finding per rule that evaluated cleanly, in registry order. */\n findings: RuleFinding[];\n crashes: RuleCrash[];\n}\n\n/**\n * Place a measurement in a scored rule's bands. Inside `ideal` passes;\n * inside `acceptable` (but not `ideal`) warns gently; outside both is\n * `poor`. Exported for rule evaluators so every scored rule bands the\n * same way.\n */\nexport function bandFor(\n value: number,\n bands: ScoredRule[\"bands\"],\n): { status: \"pass\" | \"warn\"; band: Band } {\n const inside = ([min, max]: [number, number]) => value >= min && value <= max;\n if (inside(bands.ideal)) return { status: \"pass\", band: \"ideal\" };\n if (inside(bands.acceptable)) return { status: \"warn\", band: \"acceptable\" };\n return { status: \"warn\", band: \"poor\" };\n}\n\n/** Evaluate every rule against one page's extraction. */\nexport function evaluateRules(\n extraction: Extraction,\n rules: ReadonlyArray<Rule>,\n): EvaluationResult {\n const findings: RuleFinding[] = [];\n const crashes: RuleCrash[] = [];\n\n for (const rule of rules) {\n try {\n if (rule.kind === \"boolean\") {\n const verdict = rule.evaluate(extraction);\n findings.push({\n ruleId: rule.id,\n kind: rule.kind,\n status: verdict.status,\n severity: verdict.status === \"fail\" ? rule.severity : undefined,\n observed: verdict.observed,\n expected: rule.expected,\n message: verdict.message,\n origin: verdict.origin,\n rigor: rule.rigor,\n sources: rule.sources,\n });\n } else {\n const verdict = rule.evaluate(extraction);\n findings.push({\n ruleId: rule.id,\n kind: rule.kind,\n status: verdict.status,\n band: verdict.band,\n severity:\n verdict.status === \"warn\" && verdict.band && verdict.band !== \"ideal\"\n ? rule.severityByBand[verdict.band]\n : undefined,\n observed: verdict.observed,\n expected: rule.expected,\n message: verdict.message,\n origin: verdict.origin,\n rigor: rule.rigor,\n sources: rule.sources,\n });\n }\n } catch (err) {\n crashes.push({\n ruleId: rule.id,\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return { findings, crashes };\n}\n\n/**\n * Narrow findings to violations and convert to the `Issue` shape the\n * report/CLI boundary consumes. Passing and `na` findings drop out;\n * crashes become the synthetic `engine.rule-crashed` info issue. `rules`\n * must be the same list the findings were evaluated with — it resolves\n * each finding's remediation snippet.\n */\nexport function findingsToIssues(result: EvaluationResult, rules: ReadonlyArray<Rule>): Issue[] {\n const byId = new Map(rules.map((rule) => [rule.id, rule]));\n const issues: Issue[] = [];\n\n for (const finding of result.findings) {\n if (finding.status !== \"fail\" && finding.status !== \"warn\") continue;\n issues.push({\n ruleId: finding.ruleId,\n severity: finding.severity ?? \"info\",\n message: finding.message ?? `Expected ${finding.expected}.`,\n origin: finding.origin,\n fix: byId.get(finding.ruleId)?.fix,\n });\n }\n\n for (const crash of result.crashes) {\n issues.push({\n ruleId: \"engine.rule-crashed\",\n severity: \"info\",\n message: `Rule \\`${crash.ruleId}\\` threw: ${crash.message}`,\n });\n }\n\n return issues;\n}\n","/**\n * Rule registry — the SEO metadata policies goflag ships, as sourced\n * descriptors (rules-catalog plan §6).\n *\n * Each rule declares what it enforces (`title`, `why`), how authoritative\n * the requirement is (`rigor` + cited `sources` from `./sources`), which\n * extraction paths it reads, and a pure evaluator over the `Extraction`\n * observation model. Rules never see raw HTML or the engine's `Page`.\n *\n * Deliberately small. These cover the metadata mistakes that are (a)\n * high-impact for search/social and (b) invisible to a human eyeballing a\n * page in a browser. i18n reciprocity (\"missing translations\") is\n * intentionally NOT a rule here — it is a cross-page concern computed once\n * in `../core/i18n.ts` and surfaced in the report's `missingTranslations`\n * section. Cross-page rules live in `./site-rules.ts`.\n */\n\nimport { bandFor } from \"./evaluate\";\nimport type { BooleanRule, Rule, ScoredRule, TagOrigin } from \"./types\";\n\nconst TITLE_IDEAL: [number, number] = [10, 60];\nconst TITLE_ACCEPTABLE: [number, number] = [5, 70];\nconst DESC_IDEAL: [number, number] = [50, 160];\nconst DESC_ACCEPTABLE: [number, number] = [25, 200];\n\nfunction tokens(value: string | undefined): string[] {\n if (!value) return [];\n return value\n .split(\",\")\n .map((t) => t.trim().toLowerCase())\n .filter(Boolean);\n}\n\nconst titleMissing: BooleanRule = {\n id: \"title.missing\",\n kind: \"boolean\",\n category: \"document\",\n severity: \"error\",\n title: \"Every page needs a non-empty `<title>`\",\n why:\n \"The title element is required by the HTML standard and is the primary \" +\n \"label search engines, browser tabs, bookmarks, and screen readers use \" +\n \"for the page.\",\n rigor: \"spec-required\",\n sources: [\"whatwg-html-title\"],\n reads: [\"document.title\"],\n expected: \"a non-empty `<title>` element\",\n relates: [\"title.length\", \"og.title.missing\"],\n fix: {\n title: \"Add a <title> to <head>\",\n snippet: [\n \"// app/…/page.tsx — App Router owns the <head>; never hand-write the tag.\",\n 'export const metadata = { title: \"Page name — Site name\" };',\n ].join(\"\\n\"),\n language: \"tsx\",\n },\n evaluate: (ex) => {\n const observed = ex.document.title?.value?.trim();\n if (observed) return { status: \"pass\", observed };\n return {\n status: \"fail\",\n observed: observed ?? null,\n message: \"Page is missing a `<title>` element (or it is empty).\",\n origin: { kind: \"title\" },\n };\n },\n};\n\nconst titleLength: ScoredRule = {\n id: \"title.length\",\n kind: \"scored\",\n category: \"document\",\n title: `Keep \\`<title>\\` between ${TITLE_IDEAL[0]} and ${TITLE_IDEAL[1]} characters`,\n why:\n \"Long titles get truncated in search results and short ones waste the \" +\n \"one line every result gets. The window is display folklore, not spec — \" +\n \"Google states title length is not a ranking factor.\",\n rigor: \"heuristic\",\n sources: [\"google-title-link\", \"moz-title-tag\"],\n reads: [\"document.title\"],\n bands: { ideal: TITLE_IDEAL, acceptable: TITLE_ACCEPTABLE },\n severityByBand: { acceptable: \"warning\", poor: \"warning\" },\n expected: `${TITLE_IDEAL[0]}–${TITLE_IDEAL[1]} characters`,\n relates: [\"title.missing\"],\n evaluate: (ex) => {\n const value = ex.document.title?.value?.trim();\n if (!value) return { status: \"na\", observed: 0 };\n const { status, band } = bandFor(value.length, {\n ideal: TITLE_IDEAL,\n acceptable: TITLE_ACCEPTABLE,\n });\n if (status === \"pass\") return { status, band, observed: value.length };\n const direction = value.length < TITLE_IDEAL[0] ? \"short\" : \"long\";\n return {\n status,\n band,\n observed: value.length,\n message: `Title is ${value.length} characters — ${direction} of the recommended ${TITLE_IDEAL[0]}–${TITLE_IDEAL[1]} window.`,\n origin: { kind: \"title\" },\n };\n },\n};\n\nconst descriptionMissing: BooleanRule = {\n id: \"description.missing\",\n kind: \"boolean\",\n category: \"meta\",\n severity: \"warning\",\n title: 'Provide a `<meta name=\"description\">` on every indexable page',\n why:\n \"The description feeds the search-result snippet. Without one, engines \" +\n \"improvise from page text — usually worse than a sentence you wrote.\",\n rigor: \"spec-recommended\",\n sources: [\"whatwg-html-standard-metadata-names\", \"google-snippet\"],\n reads: [\"meta.description\"],\n expected: 'a non-empty `<meta name=\"description\">`',\n relates: [\"description.length\", \"og.description.missing\"],\n fix: {\n title: \"Add a meta description\",\n snippet: [\n \"// app/…/page.tsx\",\n \"export const metadata = {\",\n ' description: \"One sentence that promises what this page delivers.\",',\n \"};\",\n ].join(\"\\n\"),\n language: \"tsx\",\n },\n evaluate: (ex) => {\n const observed = ex.meta.description?.value?.trim();\n if (observed) return { status: \"pass\", observed };\n return {\n status: \"fail\",\n observed: observed ?? null,\n message: 'Page has no `<meta name=\"description\">`.',\n origin: { kind: \"meta\", name: \"description\" },\n };\n },\n};\n\nconst descriptionLength: ScoredRule = {\n id: \"description.length\",\n kind: \"scored\",\n category: \"meta\",\n title: `Keep descriptions between ${DESC_IDEAL[0]} and ${DESC_IDEAL[1]} characters`,\n why:\n \"Snippets truncate around 160 characters and very short descriptions \" +\n \"get replaced by improvised page text. Display folklore, not spec.\",\n rigor: \"heuristic\",\n sources: [\"google-snippet\", \"moz-meta-description\"],\n reads: [\"meta.description\"],\n bands: { ideal: DESC_IDEAL, acceptable: DESC_ACCEPTABLE },\n severityByBand: { acceptable: \"warning\", poor: \"warning\" },\n expected: `${DESC_IDEAL[0]}–${DESC_IDEAL[1]} characters`,\n relates: [\"description.missing\"],\n evaluate: (ex) => {\n const value = ex.meta.description?.value?.trim();\n if (!value) return { status: \"na\", observed: 0 };\n const { status, band } = bandFor(value.length, {\n ideal: DESC_IDEAL,\n acceptable: DESC_ACCEPTABLE,\n });\n if (status === \"pass\") return { status, band, observed: value.length };\n const direction = value.length < DESC_IDEAL[0] ? \"short\" : \"long\";\n return {\n status,\n band,\n observed: value.length,\n message: `Description is ${value.length} characters — ${direction} of the recommended ${DESC_IDEAL[0]}–${DESC_IDEAL[1]} window.`,\n origin: { kind: \"meta\", name: \"description\" },\n };\n },\n};\n\nconst canonicalMissing: BooleanRule = {\n id: \"canonical.missing\",\n kind: \"boolean\",\n category: \"canonical\",\n severity: \"warning\",\n title: 'Declare a `<link rel=\"canonical\">` so search engines pick the right URL',\n why:\n \"Without a canonical, engines choose among duplicate URLs (tracking \" +\n \"params, http/https, trailing slashes) themselves — and may split \" +\n \"ranking signals across them.\",\n rigor: \"vendor-spec\",\n sources: [\"ietf-rfc6596\", \"google-canonicalization\"],\n reads: [\"meta.canonical\"],\n expected: 'a `<link rel=\"canonical\">` declaration',\n relates: [\"canonical.absolute\"],\n fix: {\n title: \"Declare the canonical URL\",\n snippet: [\n \"// app/…/page.tsx — relative canonicals resolve against metadataBase,\",\n \"// which defaults to localhost and silently breaks in production.\",\n \"export const metadata = {\",\n \" metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL!),\",\n ' alternates: { canonical: \"/the-page\" },',\n \"};\",\n ].join(\"\\n\"),\n language: \"tsx\",\n },\n evaluate: (ex) => {\n const observed = ex.meta.canonical?.value;\n if (observed) return { status: \"pass\", observed };\n return {\n status: \"fail\",\n observed: null,\n message: 'Page is missing `<link rel=\"canonical\">`.',\n origin: { kind: \"link\", rel: \"canonical\" },\n };\n },\n};\n\nconst canonicalAbsolute: BooleanRule = {\n id: \"canonical.absolute\",\n kind: \"boolean\",\n category: \"canonical\",\n severity: \"error\",\n title: '`rel=\"canonical\"` must be an absolute, fully-qualified URL',\n why:\n \"Consumers read the literal attribute value, not the browser-resolved \" +\n \"URL. A relative canonical means every consumer resolves it against \" +\n \"whatever base it assumes — including the wrong origin.\",\n rigor: \"vendor-spec\",\n sources: [\"ietf-rfc6596\", \"google-canonicalization\", \"whatwg-url\"],\n reads: [\"meta.canonical\"],\n expected: \"an absolute `http(s)://` canonical URL\",\n relates: [\"canonical.missing\"],\n evaluate: (ex) => {\n const canonical = ex.meta.canonical;\n const raw = canonical?.raw?.trim();\n if (!raw) return { status: \"na\", observed: null };\n if (/^https?:\\/\\//i.test(raw)) return { status: \"pass\", observed: raw };\n return {\n status: \"fail\",\n observed: raw,\n message: `Canonical is \"${raw}\" — must be an absolute http(s) URL (consumers see the raw value, not the resolved \"${canonical?.value ?? raw}\").`,\n origin: { kind: \"link\", rel: \"canonical\" },\n };\n },\n};\n\nconst viewportMissing: BooleanRule = {\n id: \"viewport.missing\",\n kind: \"boolean\",\n category: \"meta\",\n severity: \"warning\",\n title: 'Declare a `<meta name=\"viewport\">` so mobile browsers render at the right scale',\n why:\n \"Without a viewport declaration mobile browsers render at desktop \" +\n \"width and scale down — unreadable text and a mobile-usability demotion.\",\n rigor: \"guideline\",\n sources: [\"mdn-viewport\", \"whatwg-html-standard-metadata-names\"],\n reads: [\"meta.viewport\"],\n expected: 'a `<meta name=\"viewport\">` declaration',\n fix: {\n title: \"Add a viewport meta\",\n snippet: [\n \"// app/layout.tsx — a dedicated export, not part of `metadata`.\",\n 'export const viewport = { width: \"device-width\", initialScale: 1 };',\n ].join(\"\\n\"),\n language: \"tsx\",\n },\n evaluate: (ex) => {\n const observed = ex.meta.viewport?.value?.trim();\n if (observed) return { status: \"pass\", observed };\n return {\n status: \"fail\",\n observed: null,\n message:\n 'Page has no `<meta name=\"viewport\">` — mobile browsers will render at desktop width.',\n origin: { kind: \"meta\", name: \"viewport\" },\n };\n },\n};\n\nconst ogTitleMissing: BooleanRule = {\n id: \"og.title.missing\",\n kind: \"boolean\",\n category: \"opengraph\",\n severity: \"warning\",\n title: \"Set an explicit `og:title` instead of relying on `<title>` fallback\",\n why:\n \"og:title is one of the four properties the Open Graph protocol \" +\n \"requires. Consumers that fall back to `<title>` drag site-name \" +\n \"boilerplate into the unfurl.\",\n rigor: \"vendor-spec\",\n sources: [\"ogp\"],\n reads: [\"openGraph.title\"],\n expected: \"an explicit `og:title`\",\n relates: [\"title.missing\", \"og.image.missing\"],\n evaluate: (ex) => {\n const observed = ex.openGraph.title?.value?.trim();\n if (observed) return { status: \"pass\", observed };\n return {\n status: \"fail\",\n observed: null,\n message: \"Page has no `og:title`; consumers will fall back to `<title>` (or nothing).\",\n origin: { kind: \"meta\", property: \"og:title\" },\n };\n },\n};\n\nconst ogDescriptionMissing: BooleanRule = {\n id: \"og.description.missing\",\n kind: \"boolean\",\n category: \"opengraph\",\n severity: \"info\",\n title: \"Set an explicit `og:description` for richer link unfurls\",\n why:\n \"A page that opted into Open Graph but omitted og:description gets \" +\n \"unfurls that fall back to the meta description — or to nothing.\",\n rigor: \"vendor-spec\",\n sources: [\"ogp\", \"meta-og-sharing\"],\n reads: [\"openGraph.description\", \"openGraph.title\", \"openGraph.images\", \"openGraph.url\"],\n expected: \"an `og:description` alongside the other `og:*` tags\",\n relates: [\"description.missing\"],\n evaluate: (ex) => {\n const observed = ex.openGraph.description?.value?.trim();\n if (observed) return { status: \"pass\", observed };\n // Only meaningful when the page bothered with any other OG tag.\n const hasOtherOg = Boolean(\n ex.openGraph.title?.value || ex.openGraph.images.length > 0 || ex.openGraph.url?.value,\n );\n if (!hasOtherOg) return { status: \"na\", observed: null };\n return {\n status: \"fail\",\n observed: null,\n message:\n \"Page has `og:*` tags but no `og:description`; unfurls will fall back to the meta description (or nothing).\",\n origin: { kind: \"meta\", property: \"og:description\" },\n };\n },\n};\n\nconst ogImageMissing: BooleanRule = {\n id: \"og.image.missing\",\n kind: \"boolean\",\n category: \"opengraph\",\n severity: \"warning\",\n title: \"Provide at least one `og:image` so links unfurl with a preview\",\n why:\n \"og:image is one of the four required Open Graph properties, and the \" +\n \"one with the most visible payoff: without it, unfurls are text-only \" +\n \"or pick a random body image.\",\n rigor: \"vendor-spec\",\n sources: [\"ogp\", \"meta-og-sharing\"],\n reads: [\"openGraph.images\"],\n expected: \"at least one `og:image`\",\n relates: [\"og.title.missing\"],\n fix: {\n title: \"Add an og:image\",\n snippet: [\n \"// app/…/page.tsx\",\n \"export const metadata = {\",\n ' openGraph: { images: [{ url: \"/og.png\", width: 1200, height: 630 }] },',\n \"};\",\n \"// Or generate one per page: app/…/opengraph-image.tsx with ImageResponse.\",\n ].join(\"\\n\"),\n language: \"tsx\",\n },\n evaluate: (ex) => {\n if (ex.openGraph.images.length > 0) {\n return { status: \"pass\", observed: ex.openGraph.images.map((i) => i.url.value) };\n }\n return {\n status: \"fail\",\n observed: [],\n message:\n \"Page has no `og:image`. Link unfurls will fall back to text-only or a random body image.\",\n origin: { kind: \"meta\", property: \"og:image\" },\n };\n },\n};\n\nconst robotsConflict: BooleanRule = {\n id: \"robots.conflict\",\n kind: \"boolean\",\n category: \"robots\",\n severity: \"error\",\n title: \"`robots`, `googlebot`, and `X-Robots-Tag` must not contradict each other\",\n why:\n \"When directives conflict, crawlers resolve them with the most \" +\n \"restrictive interpretation — a stray `noindex` wins over the `index` \" +\n \"you meant, and the page silently drops out of search.\",\n rigor: \"vendor-spec\",\n sources: [\"google-robots-meta\"],\n reads: [\"meta.robots\", \"meta.googlebot\", \"http.headers\"],\n expected: \"consistent indexing directives across meta tags and headers\",\n evaluate: (ex) => {\n const candidates: Array<{ name: string; tokens: Set<string>; origin: TagOrigin }> = [\n {\n name: \"meta robots\",\n tokens: new Set(tokens(ex.meta.robots?.value)),\n origin: { kind: \"meta\", name: \"robots\" },\n },\n {\n name: \"meta googlebot\",\n tokens: new Set(tokens(ex.meta.googlebot?.value)),\n origin: { kind: \"meta\", name: \"googlebot\" },\n },\n {\n name: \"X-Robots-Tag header\",\n tokens: new Set(tokens(ex.http.headers[\"x-robots-tag\"])),\n origin: { kind: \"header\", name: \"x-robots-tag\" },\n },\n ];\n const declarations = candidates.filter((d) => d.tokens.size > 0);\n\n const observed = Object.fromEntries(declarations.map((d) => [d.name, [...d.tokens].sort()]));\n if (declarations.length < 2) return { status: \"na\", observed };\n\n const conflicts: string[] = [];\n let origin: TagOrigin | undefined;\n for (const [negative, positive] of [\n [\"noindex\", \"index\"],\n [\"nofollow\", \"follow\"],\n ] as const) {\n const sayNo = declarations.filter((d) => d.tokens.has(negative));\n const sayYes = declarations.filter((d) => d.tokens.has(positive) && !d.tokens.has(negative));\n if (sayNo.length > 0 && sayYes.length > 0) {\n conflicts.push(\n `${sayNo.map((d) => d.name).join(\", \")} say \\`${negative}\\`, ${sayYes.map((d) => d.name).join(\", \")} say \\`${positive}\\``,\n );\n origin ??= sayNo[0]!.origin;\n }\n }\n\n if (conflicts.length === 0) return { status: \"pass\", observed };\n return {\n status: \"fail\",\n observed,\n message: `Conflicting robots directives: ${conflicts.join(\"; \")}.`,\n origin,\n };\n },\n};\n\n/** The full set of rules, alphabetised by id (stable output order). */\nexport const RULES: ReadonlyArray<Rule> = [\n canonicalAbsolute,\n canonicalMissing,\n descriptionLength,\n descriptionMissing,\n ogDescriptionMissing,\n ogImageMissing,\n ogTitleMissing,\n robotsConflict,\n titleLength,\n titleMissing,\n viewportMissing,\n];\n\nconst RULE_BY_ID: Map<string, Rule> = new Map(RULES.map((r) => [r.id, r]));\n\nexport function getRule(id: string): Rule | undefined {\n return RULE_BY_ID.get(id);\n}\n\nexport type { BooleanRule, Rule, RuleFinding, ScoredRule } from \"./types\";\n","/**\n * The extraction model — goflag's per-page observation contract.\n *\n * This is the single normalized snapshot that deterministic rules and AI\n * agents both read (rules-catalog plan §7). Rules never touch raw HTML;\n * they read the documented fields below, which makes them testable in\n * isolation and makes run-to-run diffing tractable.\n *\n * Design constraints:\n *\n * - **Versioned.** `extractionVersion` is bumped whenever a field is removed\n * or reshaped. Adding optional fields is not a bump.\n * - **Provenance-carrying.** Scalar observations are `Fact`s: value + which\n * tag produced it + the literal string before normalization. An agent can\n * always answer \"says who?\" from the extraction alone.\n * - **Self-contained and serializable.** Plain JSON data, no methods, no\n * references back into the engine. The shapes mirror `core/types.ts`\n * (`Page`) deliberately — the adapter in `./from-page.ts` is mostly a\n * projection — but they are declared here so the contract can hold still\n * while the engine's internal `Page` evolves.\n * - **Per-page.** Site-level artefacts (robots.txt, the sitemap tree) get\n * their own observation models — `RobotsExtraction` / `SitemapExtraction`,\n * specified in `docs/sitemap-robots-plan.md` — in Phase G. Site-computed\n * i18n knowledge (locale cluster, reciprocity) and document structure\n * (headings, landmarks) are reserved for future additive fields.\n */\n\nimport type { TagOrigin } from \"../../core/types\";\n\nexport type { TagOrigin };\n\n/**\n * Bumped whenever the shape changes incompatibly (field removed or\n * reshaped). Consumers that persist extractions (baselines, fingerprints)\n * key on it.\n */\nexport const EXTRACTION_VERSION = 1 as const;\n\n/**\n * One observed value and where it came from. `raw` is the literal string as\n * it appeared in the source, kept whenever normalization (trimming, URL\n * resolution, number parsing) changed it.\n */\nexport interface Fact<T> {\n value: T;\n origin: TagOrigin;\n raw?: string;\n}\n\n/** The network-level observation: what the server said, before any parsing. */\nexport interface ExtractionHttp {\n /** The URL the audit asked for. */\n requestedUrl: string;\n /** The URL after following redirects (equals `requestedUrl` when none). */\n finalUrl: string;\n status: number;\n /** Lowercased response headers from the final response. */\n headers: Record<string, string>;\n /** Number of redirects followed to reach `finalUrl`. */\n redirects: number;\n /** From `content-type`, lowercased, parameters stripped. */\n contentType?: string;\n}\n\n/**\n * How the observed DOM was produced. `static` is what a non-JS crawler sees;\n * `headless` is the post-hydration DOM a JS-aware crawler sees. `escalated`\n * means goflag started static, found the head suspiciously empty, and re-ran\n * in headless mode — itself evidence that the page client-renders its\n * metadata.\n */\nexport interface ExtractionRendering {\n mode: \"static\" | \"headless\";\n escalated: boolean;\n escalationReason?: string;\n}\n\n/** Document-level observations: the root element and head-wide declarations. */\nexport interface ExtractionDocument {\n /** `<title>` text, trimmed. */\n title?: Fact<string>;\n /** `lang` attribute on `<html>`. */\n lang?: Fact<string>;\n /** `dir` attribute on `<html>`. */\n dir?: Fact<string>;\n /** Declared character encoding (`<meta charset>` or http-equiv). */\n charset?: Fact<string>;\n /** `<base href>`, when present. */\n base?: Fact<string>;\n}\n\n/**\n * Named metadata: `<meta name=\"…\">` values plus the canonical link, which\n * lives here (not under `links`) because rules consume it as a single\n * page-level declaration. `canonical` is resolved to an absolute URL when\n * possible; `raw` keeps what the tag literally said.\n */\nexport interface ExtractionMeta {\n description?: Fact<string>;\n viewport?: Fact<string>;\n robots?: Fact<string>;\n googlebot?: Fact<string>;\n canonical?: Fact<string>;\n themeColor?: Fact<string>;\n colorScheme?: Fact<string>;\n referrer?: Fact<string>;\n generator?: Fact<string>;\n applicationName?: Fact<string>;\n author?: Fact<string>;\n keywords?: Fact<string[]>;\n}\n\n/** One `og:image` structured object with its sub-properties. */\nexport interface ExtractionOpenGraphImage {\n url: Fact<string>;\n secureUrl?: Fact<string>;\n type?: Fact<string>;\n width?: Fact<number>;\n height?: Fact<number>;\n alt?: Fact<string>;\n}\n\n/** The `og:*` surface (ogp.me vocabulary). */\nexport interface ExtractionOpenGraph {\n title?: Fact<string>;\n type?: Fact<string>;\n url?: Fact<string>;\n description?: Fact<string>;\n siteName?: Fact<string>;\n locale?: Fact<string>;\n localeAlternates: Fact<string>[];\n images: ExtractionOpenGraphImage[];\n /** Any `og:*` property not mapped above, kept verbatim. */\n other: Array<{ property: string; value: Fact<string> }>;\n}\n\n/** The `twitter:*` card surface. X falls back to `og:*` for absent fields. */\nexport interface ExtractionTwitter {\n card?: Fact<string>;\n site?: Fact<string>;\n creator?: Fact<string>;\n title?: Fact<string>;\n description?: Fact<string>;\n image?: Fact<string>;\n imageAlt?: Fact<string>;\n}\n\n/** One `<link rel=\"alternate\" hreflang>` annotation, href resolved. */\nexport interface ExtractionHreflang {\n hreflang: string;\n href: string;\n isXDefault: boolean;\n}\n\n/** One icon link (`icon`, `apple-touch-icon`, …). */\nexport interface ExtractionIcon {\n rel: string;\n href: string;\n sizes?: string;\n type?: string;\n /** Parsed `sizes` entries; empty for unparseable values. */\n parsedSizes: Array<{ width: number; height: number } | \"any\">;\n}\n\n/** One feed advertisement (`rel=\"alternate\"` with a feed content type). */\nexport interface ExtractionFeed {\n type: string;\n href: string;\n title?: string;\n}\n\n/**\n * Head link observations other than canonical (which lives under `meta`).\n * These are already-normalized collections rather than `Fact`s: their origin\n * is unambiguous (the `link` tag their `rel` names).\n */\nexport interface ExtractionLinks {\n hreflang: ExtractionHreflang[];\n icons: ExtractionIcon[];\n manifest?: { href: string };\n feeds: ExtractionFeed[];\n}\n\n/** One `<script type=\"application/ld+json\">` block. */\nexport interface ExtractionJsonLd {\n /** Position in document order (0-based). */\n index: number;\n /** Top-level `@type` values found in the block (covers `@graph`). */\n types: string[];\n /** Parsed JSON, or `null` when parsing failed (see `parseError`). */\n data: unknown;\n parseError?: string;\n /** Verbatim script content — the evidence, especially on parse errors. */\n raw: string;\n}\n\n/**\n * The per-page observation. Everything a page rule (or an agent judging a\n * prose rule) may read; nothing else.\n */\nexport interface Extraction {\n extractionVersion: typeof EXTRACTION_VERSION;\n /** When the underlying fetch happened (ISO string, UTC). */\n fetchedAt: string;\n http: ExtractionHttp;\n rendering: ExtractionRendering;\n document: ExtractionDocument;\n meta: ExtractionMeta;\n openGraph: ExtractionOpenGraph;\n twitter: ExtractionTwitter;\n links: ExtractionLinks;\n jsonLd: ExtractionJsonLd[];\n}\n","/**\n * `Page` → `Extraction` adapter.\n *\n * The engine's `Page` already carries provenance (`Sourced<T>` is\n * structurally identical to `Fact<T>`), so most of this is a projection:\n * pick the documented fields, leave behind the engine internals (raw HTML\n * blobs, hydration deltas, probes). The only real work is promoting the\n * root-element attributes (`lang`, `dir`, `<base href>`), which `Page` keeps\n * raw-only, into `Fact`s with an origin.\n *\n * The output shares object references with the input — both are immutable\n * by convention — and is fully JSON-serializable (`Page.html` never leaks).\n */\n\nimport type { Page } from \"../../core/types\";\nimport type {\n Extraction,\n ExtractionDocument,\n ExtractionLinks,\n ExtractionOpenGraph,\n Fact,\n} from \"./types\";\nimport { EXTRACTION_VERSION } from \"./types\";\n\n/** Wrap a raw root-element value as a `Fact`, or propagate its absence. */\nfunction htmlFact(value: string | undefined, attribute: string): Fact<string> | undefined {\n if (value === undefined) return undefined;\n return { value, origin: { kind: \"html\", attribute }, raw: value };\n}\n\nfunction documentFromPage(page: Page): ExtractionDocument {\n return {\n title: page.meta.title,\n lang: htmlFact(page.raw.htmlLang, \"lang\"),\n dir: htmlFact(page.raw.htmlDir, \"dir\"),\n charset: page.meta.charset,\n // `<base>` is an element, not an attribute of `<html>`; `TagOrigin` has\n // no dedicated kind for it, so the origin names the element instead.\n base: htmlFact(page.raw.baseHref, \"base\"),\n };\n}\n\nfunction openGraphFromPage(page: Page): ExtractionOpenGraph {\n const og = page.openGraph;\n return {\n title: og.title,\n type: og.type,\n url: og.url,\n description: og.description,\n siteName: og.siteName,\n locale: og.locale,\n localeAlternates: og.localeAlternates,\n images: og.images,\n other: og.unknown,\n };\n}\n\nfunction linksFromPage(page: Page): ExtractionLinks {\n return {\n hreflang: page.links.alternates,\n icons: page.links.icons,\n manifest: page.links.manifest ? { href: page.links.manifest.href } : undefined,\n feeds: page.links.feeds.map(({ type, href, title }) => ({ type, href, title })),\n };\n}\n\n/** Project one observed `Page` onto the versioned extraction contract. */\nexport function extractionFromPage(page: Page): Extraction {\n return {\n extractionVersion: EXTRACTION_VERSION,\n fetchedAt: page.fetchedAt,\n http: {\n requestedUrl: page.fetch.requestedUrl,\n finalUrl: page.fetch.finalUrl,\n status: page.fetch.status,\n headers: page.fetch.headers,\n redirects: page.fetch.redirectCount,\n contentType: page.fetch.contentType,\n },\n rendering: {\n mode: page.extractor.mode,\n escalated: page.extractor.escalated,\n escalationReason: page.extractor.escalationReason,\n },\n document: documentFromPage(page),\n meta: {\n description: page.meta.description,\n viewport: page.meta.viewport,\n robots: page.meta.robots,\n googlebot: page.meta.googlebot,\n canonical: page.meta.canonical,\n themeColor: page.meta.themeColor,\n colorScheme: page.meta.colorScheme,\n referrer: page.meta.referrer,\n generator: page.meta.generator,\n applicationName: page.meta.applicationName,\n author: page.meta.author,\n keywords: page.meta.keywords,\n },\n openGraph: openGraphFromPage(page),\n twitter: {\n card: page.twitter.card,\n site: page.twitter.site,\n creator: page.twitter.creator,\n title: page.twitter.title,\n description: page.twitter.description,\n image: page.twitter.image,\n imageAlt: page.twitter.imageAlt,\n },\n links: linksFromPage(page),\n jsonLd: page.jsonLd.map(({ index, types, data, parseError, raw }) => ({\n index,\n types,\n data,\n parseError,\n raw,\n })),\n };\n}\n","/**\n * The Goflag rule runner.\n *\n * Pure function: `Page` → ordered `Issue[]`. No I/O, no caching, no\n * mutation. Every consumer of the engine — the Issues panel, the CLI\n * `goflag lint` command, snapshot diffs (Phase 9), the future hosted\n * audit log — calls this single entry point so behaviour stays\n * identical across surfaces.\n *\n * Ordering is part of the public contract:\n *\n * 1. Severity, descending: `error` > `warning` > `info`. The Issues\n * panel groups by severity but also relies on this so \"the worst\n * thing first\" lines up with the CLI's stdout order.\n * 2. Within a severity, by `ruleId` ascending. Stable, alphabetical,\n * and category-grouped (since ids are dotted).\n * 3. Within a single rule's emissions, the order the rule returned\n * them (rules can produce one issue per offending tag).\n *\n * Errors thrown inside a rule's `evaluate()` are caught and converted\n * into a synthetic `info`-severity issue with id `engine.rule-crashed`.\n * We never let one buggy rule take the whole lint run down — the CLI\n * exit code and the UI both depend on a successful walk over every rule.\n */\n\nimport type { Issue, Page, Severity } from \"./types\";\nimport { RULES, type Rule } from \"../rules\";\nimport { evaluateRules, findingsToIssues } from \"../rules/evaluate\";\nimport { extractionFromPage } from \"../rules/extraction/from-page\";\n\nconst SEVERITY_RANK: Record<Severity, number> = {\n error: 0,\n warning: 1,\n info: 2,\n};\n\n/**\n * Run the rule registry against a `Page`: project the page onto the\n * extraction contract, evaluate every descriptor, and narrow the findings\n * to violations. The optional `rules` override is used by the per-rule\n * contract test harness so it can isolate one rule against a fixture\n * without the noise of the others.\n *\n * Consumers that want the full conformance view (every rule's status,\n * passing ones included) call `evaluateRules` directly.\n */\nexport function lint(page: Page, rules: ReadonlyArray<Rule> = RULES): Issue[] {\n const extraction = extractionFromPage(page);\n const result = evaluateRules(extraction, rules);\n return sortIssues(findingsToIssues(result, rules));\n}\n\n/**\n * Stable ordering used by every consumer. Exported for tests.\n *\n * Generic over the issue type so `lint-site.ts` can reuse the exact same\n * severity/ruleId contract on `Issue & { pageUrl }` without a cast — and so\n * that any extra field a caller carries survives the sort.\n */\nexport function sortIssues<T extends Issue>(issues: T[]): T[] {\n return [...issues].sort((a, b) => {\n const sev = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];\n if (sev !== 0) return sev;\n return a.ruleId.localeCompare(b.ruleId);\n });\n}\n\n/** Convenience: count issues per severity for the UI summary chip. */\nexport function summariseIssues(issues: Issue[]): Record<Severity, number> {\n const out: Record<Severity, number> = { error: 0, warning: 0, info: 0 };\n for (const issue of issues) out[issue.severity] += 1;\n return out;\n}\n","/**\n * Hreflang reciprocity + matrix derivation (Phase 7.4 / 7.5).\n *\n * Given a set of crawled `Page`s, builds two derived views:\n *\n * 1. A locale → route grid (`I18nMatrix`) the UI renders as a\n * green/red heatmap. Routes are derived from the canonical or\n * hreflang group, locales from the URL pathname segment when it\n * looks like a known BCP 47 tag (`/fr`, `/en-us`, …).\n *\n * 2. Per-page reciprocity findings (`reciprocityIssues`) — when\n * `/fr/about` declares an `<link rel=\"alternate\" hreflang=\"en\">`\n * pointing at `/en/about`, but `/en/about` does not advertise\n * `/fr/about` back, that's a Google-flagged broken cluster.\n *\n * Both helpers are pure: they accept a `Page[]` and return data, no\n * I/O. The crawler in `crawl.ts` calls them once after the BFS\n * finishes to populate the inspect-cache i18n payload.\n *\n * Locale code validity: we accept anything that matches BCP 47's\n * basic shape (`[a-z]{2,3}(-[A-Z]{2})?`) plus `x-default`. Anything\n * else flags as `locale.invalid`.\n */\n\nimport type { Page } from \"./types\";\n\nexport type ReciprocityCode =\n \"missing-back-link\" | \"self-mismatch\" | \"x-default-missing\" | \"locale.invalid\";\n\nexport interface ReciprocityIssue {\n code: ReciprocityCode;\n /** URL of the page where the issue is observed. */\n url: string;\n /** Locale tag involved in the issue (or `x-default`). */\n locale?: string;\n /** Sister URL involved when the issue is cross-page. */\n peerUrl?: string;\n message: string;\n}\n\nexport interface I18nCell {\n /** Absolute URL serving this (route, locale) cell. `null` when\n * no page in the crawl filled this cell. */\n url: string | null;\n /** True when the page was actually inspected (vs. discovered only). */\n inspected: boolean;\n}\n\nexport interface I18nMatrix {\n /** Stable locale axis, `x-default` first when present, then BCP 47\n * tags sorted alphabetically. */\n locales: string[];\n /** Stable route axis. Each route is a \"canonical pathname\" derived\n * by stripping the leading locale segment. Sorted alphabetically. */\n routes: string[];\n /** `cells[route][locale]` lookup. Always populated for every\n * (route, locale) pair, with `url: null` for missing alternates. */\n cells: Record<string, Record<string, I18nCell>>;\n}\n\n// hreflang / lang values are case-insensitive per the HTML spec, so a\n// lowercase region subtag like `pt-br` is just as valid as canonical\n// `pt-BR`. The `i` flag keeps us from flagging real, common tags as invalid.\nconst BCP47_LOOSE = /^[a-z]{2,3}(-[a-z]{2}|-\\d{3})?$/i;\n\n/**\n * Fold a language tag to the form the matrix keys on.\n *\n * BCP 47 §2.1.1 makes tags case-insensitive, and says the conventional\n * capitalisation \"MUST NOT be taken to carry meaning\". This file already knew\n * that — it is why `BCP47_LOOSE` carries the `i` flag — but it applied it to\n * *validation* only. Identity kept the raw string, so a site routing on\n * `/pt-br/` while declaring `hreflang=\"pt-BR\"` grew two columns for one\n * language, and every route in it reported a translation hole in a language\n * the site already served.\n *\n * That combination is not exotic. It is what Next.js and next-intl produce by\n * default: lowercase URL segments, canonically-cased tags.\n */\nexport function localeIdentity(tag: string): string {\n return tag.trim().toLowerCase();\n}\n\n/** True when a URL path segment looks like a locale tag (`/fr`, `/pt-br`, …). */\nexport function looksLikeLocaleSegment(segment: string): boolean {\n return BCP47_LOOSE.test(segment);\n}\n\nexport function isValidLocale(tag: string): boolean {\n if (tag === \"x-default\") return true;\n return BCP47_LOOSE.test(tag);\n}\n\n/**\n * Build the (route, locale) grid from a set of inspected pages.\n *\n * Route extraction: for each URL pathname, strip the leading segment\n * if it parses as a BCP 47 tag (`/fr/about` → `/about`,\n * `/en-us/blog/post` → `/blog/post`). When no leading segment\n * matches, the full pathname is used (so unprefixed pages map to\n * their own route slot).\n *\n * Locale extraction: the same leading segment becomes the locale\n * axis. When the segment isn't a BCP 47 tag, the locale defaults to\n * `x-default` so unprefixed pages still show up in the matrix.\n */\nexport interface BuildI18nMatrixOptions {\n /**\n * Absolute URLs the site *declares* it serves (today: the sitemap's `<loc>`\n * entries) but which the crawl may not have reached. They fill their\n * (route, locale) cell with `inspected: false`.\n *\n * This is what stops a site with no `hreflang` from looking monolingual:\n * the crawler cannot discover `/fr/about` with no alternate link pointing\n * at it, but the sitemap names it outright.\n */\n declaredUrls?: readonly string[];\n /**\n * Locales that must appear on the axis even when nothing filled them. An\n * empty column is exactly what a translation hole looks like, so forcing\n * the axis is how `--locales fr,en` makes a missing `/en` visible instead\n * of silently shrinking the grid to the locales that happen to exist.\n */\n locales?: readonly string[];\n}\n\nexport function buildI18nMatrix(pages: Page[], options: BuildI18nMatrixOptions = {}): I18nMatrix {\n const inspectedByUrl = new Map<string, Page>();\n for (const page of pages) inspectedByUrl.set(page.fetch.finalUrl, page);\n\n type Slot = { url: string; inspected: boolean };\n const grid = new Map<string, Map<string, Slot>>();\n const locales = new Set<string>();\n const routes = new Set<string>();\n\n for (const locale of options.locales ?? []) {\n const tag = locale.trim().toLowerCase();\n if (tag) locales.add(tag);\n }\n\n function record(route: string, locale: string, url: string): void {\n routes.add(route);\n locales.add(locale);\n let row = grid.get(route);\n if (!row) {\n row = new Map();\n grid.set(route, row);\n }\n if (!row.has(locale)) {\n row.set(locale, { url, inspected: inspectedByUrl.has(url) });\n }\n }\n\n for (const page of pages) {\n const url = new URL(page.fetch.finalUrl);\n const { route: selfRoute, locale: selfLocale } = splitRoute(url.pathname);\n record(selfRoute, selfLocale, page.fetch.finalUrl);\n\n for (const alt of page.links.alternates) {\n let altUrl: URL;\n try {\n altUrl = new URL(alt.href);\n } catch {\n continue;\n }\n const { route } = splitRoute(altUrl.pathname);\n const locale = alt.isXDefault ? \"x-default\" : localeIdentity(alt.hreflang);\n // Use the alternate's *route* but trust the declared locale —\n // some sites colocate hreflang URLs without a locale prefix\n // (e.g. `/about` for both en and x-default).\n record(route || selfRoute, locale, altUrl.toString());\n }\n }\n\n // Declared-but-uncrawled URLs land last so a real inspected page always wins\n // the cell (`record` keeps the first writer).\n for (const declared of options.declaredUrls ?? []) {\n let url: URL;\n try {\n url = new URL(declared);\n } catch {\n continue;\n }\n const { route, locale } = splitRoute(url.pathname);\n record(route, locale, url.toString());\n }\n\n const sortedLocales = sortLocales([...locales]);\n const sortedRoutes = [...routes].sort((a, b) => a.localeCompare(b));\n const cells: I18nMatrix[\"cells\"] = {};\n for (const route of sortedRoutes) {\n cells[route] = {};\n for (const locale of sortedLocales) {\n const slot = grid.get(route)?.get(locale);\n cells[route][locale] = slot\n ? { url: slot.url, inspected: slot.inspected }\n : { url: null, inspected: false };\n }\n }\n return { locales: sortedLocales, routes: sortedRoutes, cells };\n}\n\n/**\n * Returns reciprocity findings across the crawl.\n *\n * - Every alternate `A → B` must be matched by `B → A` (both must\n * point at each other). When `B` was crawled and lacks the back\n * link, emit `missing-back-link`.\n * - When `A` declares `hreflang=\"fr\"` pointing at itself but `A`'s\n * own URL says `/en/...`, emit `self-mismatch`.\n * - When a route has 2+ locales but no `x-default`, emit\n * `x-default-missing` once per route on the canonical entry.\n * - When a locale tag fails BCP 47, emit `locale.invalid`.\n */\nexport function reciprocityIssues(pages: Page[]): ReciprocityIssue[] {\n const issues: ReciprocityIssue[] = [];\n const byUrl = new Map<string, Page>();\n for (const page of pages) byUrl.set(page.fetch.finalUrl, page);\n\n for (const page of pages) {\n const url = page.fetch.finalUrl;\n const seenLocalesOnPage = new Set<string>();\n for (const alt of page.links.alternates) {\n // Folded for identity, raw for the finding. Counting `pt-BR` and\n // `pt-br` as two locales is the defect; but reporting a *rejected* tag\n // in a form the page never wrote would be judging what we altered\n // rather than what the site declared.\n const raw = alt.isXDefault ? \"x-default\" : alt.hreflang;\n const locale = alt.isXDefault ? \"x-default\" : localeIdentity(alt.hreflang);\n seenLocalesOnPage.add(locale);\n if (!isValidLocale(locale)) {\n issues.push({\n code: \"locale.invalid\",\n url,\n locale: raw,\n message: `\\`hreflang=\"${raw}\"\\` is not a valid BCP 47 tag.`,\n });\n continue;\n }\n let altUrl: string;\n try {\n altUrl = new URL(alt.href).toString();\n } catch {\n continue;\n }\n\n // Reciprocity only fires when we actually crawled the peer.\n const peer = byUrl.get(altUrl);\n if (!peer) continue;\n const back = peer.links.alternates.find((p) => {\n let peerHref: string;\n try {\n peerHref = new URL(p.href).toString();\n } catch {\n return false;\n }\n return peerHref === url;\n });\n if (!back) {\n issues.push({\n code: \"missing-back-link\",\n url,\n peerUrl: altUrl,\n locale,\n message: `\\`${url}\\` declares an alternate to \\`${altUrl}\\` but the peer does not link back.`,\n });\n }\n }\n\n // x-default requirement only kicks in when at least 2 locales are\n // advertised — single-locale sites don't need x-default.\n const nonDefaultLocales = [...seenLocalesOnPage].filter((l) => l !== \"x-default\");\n if (nonDefaultLocales.length >= 2 && !seenLocalesOnPage.has(\"x-default\")) {\n issues.push({\n code: \"x-default-missing\",\n url,\n message: `Page advertises ${nonDefaultLocales.length} locales but no \\`hreflang=\"x-default\"\\`.`,\n });\n }\n }\n\n // A page can repeat the same hreflang tag (or the same broken cluster can\n // be observed twice); collapse exact duplicates so counts and fingerprints\n // stay honest.\n return dedupeBy(\n issues,\n (i) => `${i.code}\\u0000${i.url}\\u0000${i.peerUrl ?? \"\"}\\u0000${i.locale ?? \"\"}`,\n );\n}\n\nfunction dedupeBy<T>(items: T[], key: (item: T) => string): T[] {\n const seen = new Set<string>();\n const out: T[] = [];\n for (const item of items) {\n const k = key(item);\n if (seen.has(k)) continue;\n seen.add(k);\n out.push(item);\n }\n return out;\n}\n\n/**\n * Split a pathname into its locale-free route and its locale segment.\n * `/fr/about` → `{ route: \"/about\", locale: \"fr\" }`; an unprefixed path keeps\n * its full pathname and reports `x-default`. Exported so cross-page rules key\n * the `<head>` and the sitemap on the same route derivation — comparing two\n * artefacts is only meaningful if both are normalised identically.\n */\nexport function splitRoute(pathname: string): { route: string; locale: string } {\n const segments = pathname.split(\"/\").filter(Boolean);\n if (segments.length > 0 && looksLikeLocaleSegment(segments[0]!)) {\n const locale = localeIdentity(segments[0]!);\n const rest = segments.slice(1).join(\"/\");\n return { route: rest ? `/${rest}` : \"/\", locale };\n }\n return { route: pathname || \"/\", locale: \"x-default\" };\n}\n\nfunction sortLocales(locales: string[]): string[] {\n const xDefault = locales.includes(\"x-default\") ? [\"x-default\"] : [];\n const rest = locales.filter((l) => l !== \"x-default\").sort((a, b) => a.localeCompare(b));\n return [...xDefault, ...rest];\n}\n","/**\n * Cross-page rule registry — the hreflang policies goflag ships.\n *\n * These are the checks that a per-page `Rule` structurally cannot make, and\n * they are the ones that catch the failure modes we actually observed in the\n * wild:\n *\n * - a site serving four locales with not a single `hreflang` tag anywhere\n * (`hreflang.missing`), which the old crawl-derived matrix reported as\n * \"0 missing translations\" because the absence of alternates is what made\n * the other locales undiscoverable in the first place;\n * - a `<head>` and a `sitemap.xml` that disagree about which locales a route\n * exists in (`hreflang.sitemap-mismatch`) — two artefacts derived\n * independently from the same intent, drifting apart silently.\n *\n * Both are gated on `localeAxis.multilingual`: on a single-locale site they\n * are noise, not findings.\n */\n\nimport { splitRoute } from \"../core/i18n\";\nimport type { Page } from \"../core/types\";\nimport type { SiteContext, SiteRule } from \"./site-types\";\n\n/** A site is only subject to hreflang policy when it serves 2+ locales. */\nfunction isMultilingual(site: SiteContext): boolean {\n return site.localeAxis.multilingual;\n}\n\n/** Locale tags a page declares via `<link rel=\"alternate\" hreflang>`, minus `x-default`. */\nfunction declaredLocales(page: Page): Set<string> {\n const out = new Set<string>();\n for (const alt of page.links.alternates) {\n if (alt.isXDefault) continue;\n const tag = alt.hreflang?.trim().toLowerCase();\n if (tag) out.add(tag);\n }\n return out;\n}\n\n/** Route → locales the sitemap lists a URL for. */\nfunction sitemapLocalesByRoute(site: SiteContext): Map<string, Set<string>> {\n const byRoute = new Map<string, Set<string>>();\n for (const entry of site.discovery?.urls ?? []) {\n let pathname: string;\n try {\n pathname = new URL(entry.loc).pathname;\n } catch {\n continue;\n }\n const { route, locale } = splitRoute(pathname);\n if (locale === \"x-default\") continue;\n const set = byRoute.get(route) ?? new Set<string>();\n set.add(locale.toLowerCase());\n byRoute.set(route, set);\n }\n return byRoute;\n}\n\nfunction sorted(set: Set<string>): string[] {\n return [...set].sort((a, b) => a.localeCompare(b));\n}\n\n/**\n * A multilingual site whose pages advertise no alternates at all.\n *\n * This is the headline blind spot. Google has no way to associate the locale\n * variants of a route, so they compete with each other instead of consolidating\n * — and the tool that was supposed to notice was itself relying on the missing\n * tags to know the locales existed.\n */\nconst hreflangMissing: SiteRule = {\n id: \"hreflang.missing\",\n severity: \"error\",\n summary: \"Pages on a multilingual site must advertise their locale alternates\",\n appliesTo: isMultilingual,\n check: ({ site, issue }) => {\n const locales = site.localeAxis.locales.join(\", \");\n return site.pages\n .filter((page) => page.links.alternates.length === 0)\n .map((page) =>\n issue({\n pageUrl: page.fetch.finalUrl,\n message:\n `Page declares no \\`hreflang\\` alternates, but the site serves ` +\n `${site.localeAxis.locales.length} locales (${locales}, per the ` +\n `${site.localeAxis.source}). Locale variants of this route cannot be ` +\n `associated with each other.`,\n origin: { kind: \"link\", rel: \"alternate\" },\n fix: {\n title: \"Emit alternates from generateMetadata()\",\n snippet: [\n \"// app/[locale]/…/page.tsx\",\n \"export async function generateMetadata({ params }) {\",\n \" const { locale } = await params;\",\n \" return {\",\n \" alternates: {\",\n \" canonical: `${baseUrl}/${locale}${path}`,\",\n \" languages: {\",\n ...site.localeAxis.locales.map(\n (l) => ` \"${l}\": \\`\\${baseUrl}/${l}\\${path}\\`,`,\n ),\n ' \"x-default\": `${baseUrl}/${defaultLocale}${path}`,',\n \" },\",\n \" },\",\n \" };\",\n \"}\",\n ].join(\"\\n\"),\n language: \"tsx\",\n },\n }),\n );\n },\n};\n\n/**\n * The `<head>` and the sitemap disagree about a route's locale coverage.\n *\n * Both are declarations of the same intent, produced by different code paths,\n * so they drift. Under-declaring in the `<head>` hides real translations from\n * search engines; over-declaring points `hreflang` at URLs the site itself does\n * not list, which Google treats as a broken cluster.\n *\n * Pages with no alternates at all are skipped: that is `hreflang.missing`'s\n * finding, and reporting both would double-count the same defect.\n */\nconst hreflangSitemapMismatch: SiteRule = {\n id: \"hreflang.sitemap-mismatch\",\n severity: \"warning\",\n summary: \"`<head>` alternates and sitemap locale coverage must agree\",\n appliesTo: (site) => isMultilingual(site) && (site.discovery?.urls.length ?? 0) > 0,\n check: ({ site, issue }) => {\n const bySitemap = sitemapLocalesByRoute(site);\n const findings = [];\n\n for (const page of site.pages) {\n const head = declaredLocales(page);\n if (head.size === 0) continue;\n\n let pathname: string;\n try {\n pathname = new URL(page.fetch.finalUrl).pathname;\n } catch {\n continue;\n }\n const { route } = splitRoute(pathname);\n const inSitemap = bySitemap.get(route);\n if (!inSitemap || inSitemap.size === 0) continue;\n\n const onlyInSitemap = sorted(new Set([...inSitemap].filter((l) => !head.has(l))));\n const onlyInHead = sorted(new Set([...head].filter((l) => !inSitemap.has(l))));\n if (onlyInSitemap.length === 0 && onlyInHead.length === 0) continue;\n\n const parts: string[] = [];\n if (onlyInSitemap.length > 0) {\n parts.push(\n `the sitemap lists ${onlyInSitemap.join(\", \")} but the \\`<head>\\` does not advertise ${onlyInSitemap.length > 1 ? \"them\" : \"it\"}`,\n );\n }\n if (onlyInHead.length > 0) {\n parts.push(\n `the \\`<head>\\` advertises ${onlyInHead.join(\", \")} but the sitemap has no entry for ${onlyInHead.length > 1 ? \"them\" : \"it\"}`,\n );\n }\n\n findings.push(\n issue({\n pageUrl: page.fetch.finalUrl,\n message: `Route \\`${route}\\`: ${parts.join(\"; \")}. Both are derived from the same intent and must not disagree.`,\n origin: { kind: \"link\", rel: \"alternate\" },\n fix: {\n title: \"Derive both from one locale-availability source\",\n snippet: [\n \"// Compute availability once, feed both the <head> and the sitemap.\",\n \"const localesFor = (slug: string) =>\",\n \" allDocs.filter((d) => d.slug === slug && !d.draft).map((d) => d.locale);\",\n \"\",\n \"// generateMetadata(): alternates.languages ← localesFor(slug)\",\n \"// sitemap.ts: alternates.languages ← localesFor(slug)\",\n ].join(\"\\n\"),\n language: \"ts\",\n },\n }),\n );\n }\n\n return findings;\n },\n};\n\n/** Directive tokens from a page's `<meta name=\"robots\">`. */\nfunction metaRobotsTokens(page: Page): Set<string> {\n const raw = page.meta.robots?.value ?? \"\";\n return new Set(\n raw\n .split(\",\")\n .map((t) => t.trim().toLowerCase())\n .filter(Boolean),\n );\n}\n\n/**\n * `robots.txt` forbids the whole site.\n *\n * The most expensive misconfiguration a site can carry, and the one nothing\n * in the tool was watching: tancrede.eu served `User-agent: * / Disallow: /`\n * in production while every page carried `<meta name=\"robots\"\n * content=\"index, follow\">`. The pages ask to be indexed; the file forbids the\n * crawl that would read them — and because robots.txt wins, the request never\n * happens and the meta tag is never seen.\n *\n * Severity depends on whether anything contradicts the block. A staging site\n * that disallows everything and says nothing else is doing exactly what it\n * means to, so that is a warning. A site that blocks the crawl *and* asks to\n * be indexed cannot have meant both — that is an error.\n */\nconst robotsBlocksSite: SiteRule = {\n id: \"robots.blocks-site\",\n severity: \"error\",\n summary: \"`robots.txt` must not forbid crawling a site that asks to be indexed\",\n appliesTo: (site) => site.robots?.found === true && site.robots.blocksAll,\n check: ({ site, issue }) => {\n const robotsUrl = site.robots?.url ?? `${site.origin}/robots.txt`;\n\n // An explicit `index` is a statement of intent, not the mere absence of\n // `noindex` — which every page has by default and would make this fire\n // on every blocked staging environment.\n const asking = site.pages.filter((page) => metaRobotsTokens(page).has(\"index\"));\n\n const detail = asking.length\n ? `but ${asking.length} crawled page${asking.length === 1 ? \"\" : \"s\"} declare ` +\n '`<meta name=\"robots\" content=\"index\">`. Both cannot be true: robots.txt ' +\n \"wins, so the pages are never fetched and the meta tag is never read.\"\n : \"so no search engine will crawl any page on this origin. If this is a \" +\n \"staging or preview environment, that is correct — otherwise the site is \" +\n \"invisible.\";\n\n return issue({\n pageUrl: robotsUrl,\n severity: asking.length > 0 ? \"error\" : \"warning\",\n message: `\\`robots.txt\\` disallows the whole site for \\`User-agent: *\\`, ${detail}`,\n origin: { kind: \"computed\" },\n fix: {\n title: \"Gate the disallow on the deployed environment\",\n snippet: [\n \"// app/robots.ts — the flag must be readable at build AND at runtime,\",\n \"// or a production container silently serves the staging rules.\",\n 'const isProduction = process.env.APP_ENV === \"production\";',\n \"\",\n \"export default function robots(): MetadataRoute.Robots {\",\n ' if (!isProduction) return { rules: { userAgent: \"*\", disallow: \"/\" } };',\n \" return {\",\n ' rules: { userAgent: \"*\", allow: \"/\", disallow: [\"/api/\", \"/_next/\"] },',\n \" sitemap: `${baseUrl}/sitemap.xml`,\",\n \" };\",\n \"}\",\n ].join(\"\\n\"),\n language: \"ts\",\n },\n });\n },\n};\n\n/** Ordered registry. Ids are unique; the runner relies on that for lookup. */\nexport const SITE_RULES: ReadonlyArray<SiteRule> = [\n hreflangMissing,\n hreflangSitemapMismatch,\n robotsBlocksSite,\n];\n\nexport function getSiteRule(id: string): SiteRule | undefined {\n return SITE_RULES.find((rule) => rule.id === id);\n}\n","/**\n * The Goflag site-rule runner.\n *\n * The cross-page counterpart to `lint()`: pure function `SiteContext` →\n * ordered `SiteIssueDraft[]`. It mirrors `lint.ts` deliberately — same\n * severity-then-id ordering, same crash isolation — so the two registries\n * behave identically from every consumer's point of view and a finding's\n * provenance never changes how it is rendered or fingerprinted.\n *\n * Ordering contract, on top of `lint()`'s:\n * 1. Severity descending, then `ruleId` ascending (as per-page rules).\n * 2. Within one rule, by `pageUrl` ascending, so a rule that fires on 24\n * pages produces a stable, diffable sequence rather than crawl order.\n */\n\nimport { sortIssues } from \"./lint\";\nimport { SITE_RULES } from \"../rules/site-rules\";\nimport type { SiteContext, SiteIssueDraft, SiteRule } from \"../rules/site-types\";\n\n/**\n * Run the site-rule registry against a `SiteContext`.\n *\n * The `rules` override exists for the per-rule contract tests, which isolate\n * one policy against a fixture without the noise of the others.\n */\nexport function lintSite(\n site: SiteContext,\n rules: ReadonlyArray<SiteRule> = SITE_RULES,\n): SiteIssueDraft[] {\n const drafts: SiteIssueDraft[] = [];\n\n for (const rule of rules) {\n if (rule.appliesTo && !rule.appliesTo(site)) continue;\n\n let result: SiteIssueDraft[] | SiteIssueDraft | undefined | void;\n try {\n result = rule.check({\n site,\n issue: (input) => ({\n ruleId: rule.id,\n severity: input.severity ?? rule.severity,\n message: input.message,\n origin: input.origin,\n fix: input.fix,\n suggestion: input.suggestion,\n pageUrl: input.pageUrl,\n }),\n });\n } catch (err) {\n // One buggy policy must never take the whole audit down: the exit code\n // and every downstream consumer depend on a complete walk.\n drafts.push({\n ruleId: \"engine.site-rule-crashed\",\n severity: \"info\",\n message: `Site rule \\`${rule.id}\\` threw: ${err instanceof Error ? err.message : String(err)}`,\n pageUrl: site.origin,\n });\n continue;\n }\n\n if (!result) continue;\n if (Array.isArray(result)) drafts.push(...result);\n else drafts.push(result);\n }\n\n return sortSiteIssues(drafts);\n}\n\n/** Stable ordering used by every consumer. Exported for tests. */\nexport function sortSiteIssues(drafts: SiteIssueDraft[]): SiteIssueDraft[] {\n // `sortIssues` is stable and already encodes the severity/ruleId contract;\n // pre-sorting by pageUrl makes the third key fall out of that stability.\n const byPage = [...drafts].sort((a, b) => a.pageUrl.localeCompare(b.pageUrl));\n return sortIssues(byPage);\n}\n","/**\n * Locale-axis derivation.\n *\n * Answering \"is this site multilingual, and in which locales?\" is the\n * precondition for every hreflang check — and getting it from the wrong\n * source is what made goflag blind, then what made it noisy.\n *\n * **Blind.** The original axis came from the pages the crawler reached plus the\n * `hreflang` those pages declared. On a site declaring none, the BFS never left\n * the entry locale, the axis collapsed to one column, and every hreflang check\n * passed vacuously. Sitemap seeding fixed that.\n *\n * **Noisy.** Falling back to \"any leading path segment shaped like a language\n * tag\" then invented locales. On tancrede.eu, `/cv` (a CV page, served in\n * French) became a locale — `cv` is a real ISO 639-1 code, Chuvash — and\n * produced 31 phantom translation holes. Shape is not membership.\n *\n * So the axis is only ever taken from a source that *declares* it:\n *\n * 1. `explicit` — the operator passed `--locales fr,en,pt-br`.\n * 2. `sitemap` — locale prefixes across the sitemap's `<loc>` entries. The\n * sitemap is a different artefact from the `<head>`, so using it to judge\n * the `<head>` is not circular.\n *\n * With neither, goflag **does not guess**. It reports `candidates` — prefixes\n * that look like locales, each carrying the evidence for and against — and\n * leaves the axis empty, which gates every hreflang rule off. A tool that\n * invents a locale is worse than one that admits it does not know: the first\n * costs you an afternoon chasing 31 findings that were never real.\n *\n * The candidate evidence is deliberately the same cross-check the rules\n * themselves use — two independent declarations compared — so promoting a\n * candidate to the axis later (an interactive `--detect-locales`, an MCP\n * method) needs no new machinery, only a decision.\n */\n\nimport { looksLikeLocaleSegment } from \"./i18n\";\nimport type { Page } from \"./types\";\n\n/** How the locale axis was established. */\nexport type LocaleAxisSource = \"explicit\" | \"sitemap\" | \"none\";\n\n/**\n * A path prefix that *might* be a locale, with what we know about it. Never\n * promoted automatically — this is a suggestion for the operator, not a\n * finding.\n */\nexport interface LocaleCandidate {\n /** The leading path segment, lowercased. */\n tag: string;\n /** Number of crawled pages sitting under this prefix. */\n pages: number;\n /** The tag is a registered ISO 639-1 language (kills `api`, `faq`, `doc`). */\n isKnownLanguage: boolean;\n /**\n * `<html lang>` declared by pages under this prefix agrees with the tag\n * (kills `cv`, whose pages declare `lang=\"fr\"`). `undefined` when no page\n * under the prefix declared a lang at all.\n */\n htmlLangAgrees?: boolean;\n /** Distinct `<html lang>` values seen under the prefix, for the report. */\n observedLangs: string[];\n}\n\nexport interface LocaleAxis {\n /** Locale tags the site is declared to serve, sorted, without `x-default`. */\n locales: string[];\n /** Where `locales` came from. `none` when nothing authoritative existed. */\n source: LocaleAxisSource;\n /**\n * True when the site is *declared* to serve more than one locale. Gates\n * every hreflang rule: a monolingual site — or one we cannot vouch for —\n * must never be told it is missing translations.\n */\n multilingual: boolean;\n /**\n * Prefixes that look like locales but were never declared anywhere. Empty\n * unless `source` is `none`; populated so the CLI can tell the operator what\n * to pass to `--locales` instead of guessing on their behalf.\n */\n candidates: LocaleCandidate[];\n}\n\nexport interface DeriveLocaleAxisInput {\n /** Locales passed explicitly by the operator (`--locales`). */\n explicit?: readonly string[];\n /** Absolute URLs from the discovered sitemap, if any. */\n sitemapUrls?: readonly string[];\n /** Crawled pages, used only to describe candidates when nothing is declared. */\n pages?: readonly Page[];\n}\n\n/**\n * ISO 639-1 two-letter language codes. A closed list on purpose: the point is\n * membership, not shape, and the set has not changed in years.\n */\nconst ISO_639_1 = new Set(\n (\n \"aa ab ae af ak am an ar as av ay az ba be bg bh bi bm bn bo br bs ca ce ch co cr cs cu cv cy \" +\n \"da de dv dz ee el en eo es et eu fa ff fi fj fo fr fy ga gd gl gn gu gv ha he hi ho hr ht hu \" +\n \"hy hz ia id ie ig ii ik io is it iu ja jv ka kg ki kj kk kl km kn ko kr ks ku kv kw ky la lb \" +\n \"lg li ln lo lt lu lv mg mh mi mk ml mn mr ms mt my na nb nd ne ng nl nn no nr nv ny oc oj om \" +\n \"or os pa pi pl ps pt qu rm rn ro ru rw sa sc sd se sg si sk sl sm sn so sq sr ss st su sv sw \" +\n \"ta te tg th ti tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa wo xh yi yo za zh zu\"\n ).split(\" \"),\n);\n\n/** True when the primary subtag is a registered ISO 639-1 language. */\nexport function isKnownLanguageTag(tag: string): boolean {\n const primary = tag.toLowerCase().split(\"-\")[0] ?? \"\";\n return ISO_639_1.has(primary);\n}\n\n/** Leading path segment of `url` when it is shaped like a locale tag, else null. */\nexport function localePrefixOf(url: string): string | null {\n let pathname: string;\n try {\n pathname = new URL(url).pathname;\n } catch {\n return null;\n }\n const first = pathname.split(\"/\").filter(Boolean)[0];\n if (first && looksLikeLocaleSegment(first)) return first.toLowerCase();\n return null;\n}\n\n/** Collect the distinct locale-shaped prefixes across a set of URLs. */\nfunction prefixesIn(urls: readonly string[] | undefined): Set<string> {\n const out = new Set<string>();\n for (const url of urls ?? []) {\n const locale = localePrefixOf(url);\n if (locale) out.add(locale);\n }\n out.delete(\"x-default\");\n return out;\n}\n\n/** Describe every locale-shaped prefix seen in the crawl, with its evidence. */\nfunction describeCandidates(pages: readonly Page[]): LocaleCandidate[] {\n const byTag = new Map<string, { pages: number; langs: Set<string> }>();\n\n for (const page of pages) {\n const tag = localePrefixOf(page.fetch.finalUrl);\n if (!tag || tag === \"x-default\") continue;\n const entry = byTag.get(tag) ?? { pages: 0, langs: new Set<string>() };\n entry.pages += 1;\n const lang = page.raw.htmlLang?.trim().toLowerCase();\n if (lang) entry.langs.add(lang);\n byTag.set(tag, entry);\n }\n\n const candidates: LocaleCandidate[] = [];\n for (const [tag, { pages: n, langs }] of byTag) {\n const observedLangs = [...langs].sort();\n // A prefix that really is a locale serves pages declaring that language.\n // `/cv` serving `lang=\"fr\"` is a route, not a Chuvash edition.\n const htmlLangAgrees =\n observedLangs.length === 0\n ? undefined\n : observedLangs.some((l) => l.split(\"-\")[0] === tag.split(\"-\")[0]);\n candidates.push({\n tag,\n pages: n,\n isKnownLanguage: isKnownLanguageTag(tag),\n htmlLangAgrees,\n observedLangs,\n });\n }\n\n // Most plausible first: agreeing langs, then registered languages, then size.\n return candidates.sort(\n (a, b) =>\n Number(b.htmlLangAgrees ?? false) - Number(a.htmlLangAgrees ?? false) ||\n Number(b.isKnownLanguage) - Number(a.isKnownLanguage) ||\n b.pages - a.pages ||\n a.tag.localeCompare(b.tag),\n );\n}\n\n/**\n * Build the axis from declared sources only.\n *\n * `x-default` is excluded throughout: it is a fallback *pointer*, never a\n * locale a page can be \"missing a translation\" in. On the axis it would make\n * every unprefixed route look like a hole.\n */\nexport function deriveLocaleAxis(input: DeriveLocaleAxisInput): LocaleAxis {\n const explicit = new Set(\n (input.explicit ?? []).map((l) => l.trim().toLowerCase()).filter(Boolean),\n );\n explicit.delete(\"x-default\");\n const fromSitemap = prefixesIn(input.sitemapUrls);\n\n if (explicit.size > 0 || fromSitemap.size > 0) {\n // Union, not precedence: an explicit list narrows intent, it does not hide\n // what the sitemap demonstrably declares.\n const all = new Set([...explicit, ...fromSitemap]);\n return {\n locales: [...all].sort((a, b) => a.localeCompare(b)),\n source: explicit.size > 0 ? \"explicit\" : \"sitemap\",\n multilingual: all.size >= 2,\n candidates: [],\n };\n }\n\n return {\n locales: [],\n source: \"none\",\n multilingual: false,\n candidates: describeCandidates(input.pages ?? []),\n };\n}\n\n/** The `--locales` value we would suggest, or null when nothing is plausible. */\nexport function suggestedLocales(axis: LocaleAxis): string | null {\n const plausible = axis.candidates.filter((c) => c.isKnownLanguage && c.htmlLangAgrees !== false);\n if (plausible.length < 2) return null;\n return plausible\n .map((c) => c.tag)\n .sort((a, b) => a.localeCompare(b))\n .join(\",\");\n}\n","import { gunzipSync } from \"node:zlib\";\nimport { combineSignals } from \"../probes/abort\";\nimport { probeRobots } from \"../probes/robots\";\nimport { crawl } from \"../crawl\";\nimport { parseSitemap } from \"./parse\";\nimport type {\n SiteDiscovery,\n SiteDiscoverySource,\n SitemapDiagnostics,\n SitemapUrlEntry,\n} from \"./types\";\n\nexport interface DiscoverSitemapOptions {\n /** Caller-driven cancellation. */\n signal?: AbortSignal;\n /** Per-request timeout in ms. Defaults to 8_000. */\n timeoutMs?: number;\n /** Hard cap on collected page URLs. Defaults to 5_000. */\n maxUrls?: number;\n /** Hard cap on child sitemaps followed from an index. Defaults to 50. */\n maxSitemaps?: number;\n /** Allow self-signed TLS (localhost / tunnels). */\n allowInsecureTls?: boolean;\n /** Run the BFS crawler when no sitemap is found. Defaults to true. */\n crawlFallback?: boolean;\n /** Crawl fallback depth. Defaults to 1. */\n crawlDepth?: number;\n /** Crawl fallback page cap. Defaults to 100. */\n crawlMaxPages?: number;\n}\n\nconst DEFAULTS = {\n // A sitemap is one document and it can be large: this repository's own\n // openfinanceguide serves 3.5 MB across 4008 URLs, and that took 1.2 s on a\n // good connection. Eight seconds was tight enough that the fetch failed\n // intermittently, and every failure silently dropped the crawl to link\n // following. Twenty is generous for a document fetched once per run.\n timeoutMs: 20_000,\n maxUrls: 5_000,\n maxSitemaps: 50,\n crawlDepth: 1,\n crawlMaxPages: 100,\n};\n\ninterface SitemapCandidate {\n url: string;\n source: SiteDiscoverySource;\n declaredInRobots: boolean;\n}\n\ninterface FetchedDoc {\n status: number;\n body?: string;\n /**\n * Why the document could not be read, when the failure was the network\n * rather than the server. A 404 leaves this unset: the site answered, and\n * what it answered is that there is nothing there.\n */\n error?: string;\n}\n\n/**\n * Locate and load a site's sitemap starting from any URL on that site.\n *\n * Trust order:\n * 1. `Sitemap:` declarations in `robots.txt` (most authoritative).\n * 2. Well-known paths `/sitemap.xml` then `/sitemap_index.xml`.\n * 3. BFS crawl fallback (depth 1) when nothing else yields URLs.\n *\n * A `<sitemapindex>` root is followed into its children (bounded by\n * `maxSitemaps` / `maxUrls`). Gzipped sitemaps (`.xml.gz` or\n * `content-encoding: gzip`) are inflated transparently.\n *\n * Never throws — every failure mode collapses into an empty-but-shaped\n * `SiteDiscovery` whose `diagnostics` explain what went wrong.\n */\nexport async function discoverSitemap(\n baseUrl: string,\n options: DiscoverSitemapOptions = {},\n): Promise<SiteDiscovery> {\n const origin = new URL(baseUrl).origin;\n const maxUrls = options.maxUrls ?? DEFAULTS.maxUrls;\n const maxSitemaps = options.maxSitemaps ?? DEFAULTS.maxSitemaps;\n\n const diagnostics: SitemapDiagnostics = {\n found: false,\n status: 0,\n declaredInRobots: false,\n robotsFound: false,\n atWellKnownPath: false,\n wellFormed: false,\n isIndex: false,\n childSitemapCount: 0,\n childSitemapErrors: 0,\n urlCount: 0,\n warnings: [],\n };\n\n // 1. Consult robots.txt for declared sitemaps.\n const robots = await probeRobots(origin, { signal: options.signal });\n diagnostics.robotsFound = robots.found;\n\n const candidates: SitemapCandidate[] = [];\n for (const declared of robots.sitemaps) {\n candidates.push({ url: declared, source: \"robots\", declaredInRobots: true });\n }\n candidates.push({\n url: new URL(\"/sitemap.xml\", origin).toString(),\n source: \"well-known\",\n declaredInRobots: robots.sitemaps.includes(new URL(\"/sitemap.xml\", origin).toString()),\n });\n candidates.push({\n url: new URL(\"/sitemap_index.xml\", origin).toString(),\n source: \"well-known\",\n declaredInRobots: false,\n });\n\n // 2. Try each candidate until one parses into a urlset or index.\n for (const candidate of candidates) {\n const doc = await fetchDoc(candidate.url, options);\n if (!doc.body) {\n // Remember the first observed status so the UI can show a 404 etc.\n if (diagnostics.status === 0) diagnostics.status = doc.status;\n if (doc.error) {\n diagnostics.unreachable ??= `${candidate.url}: ${doc.error}`;\n diagnostics.warnings.push(`Sitemap unreachable at ${candidate.url} — ${doc.error}`);\n }\n continue;\n }\n const parsed = parseSitemap(doc.body);\n if (parsed.kind === \"unknown\") {\n if (diagnostics.status === 0) diagnostics.status = doc.status;\n diagnostics.warnings.push(`Malformed or unrecognised sitemap at ${candidate.url}`);\n continue;\n }\n\n // Found a usable root document.\n diagnostics.found = true;\n diagnostics.status = doc.status;\n diagnostics.wellFormed = true;\n diagnostics.sitemapUrl = candidate.url;\n diagnostics.declaredInRobots = candidate.declaredInRobots;\n diagnostics.atWellKnownPath = isWellKnownPath(candidate.url);\n\n const collected: SitemapUrlEntry[] = [];\n const seen = new Set<string>();\n let truncated = false;\n\n if (parsed.kind === \"urlset\") {\n truncated = pushEntries(collected, seen, parsed.urls, maxUrls);\n } else {\n diagnostics.isIndex = true;\n const children = parsed.sitemaps.slice(0, maxSitemaps);\n diagnostics.childSitemapCount = parsed.sitemaps.length;\n if (parsed.sitemaps.length > maxSitemaps) {\n truncated = true;\n diagnostics.warnings.push(\n `Sitemap index has ${parsed.sitemaps.length} children; only the first ${maxSitemaps} were followed.`,\n );\n }\n for (const childUrl of children) {\n if (collected.length >= maxUrls) {\n truncated = true;\n break;\n }\n const childDoc = await fetchDoc(childUrl, options);\n if (!childDoc.body) {\n diagnostics.childSitemapErrors += 1;\n diagnostics.warnings.push(`Child sitemap unreachable: ${childUrl}`);\n continue;\n }\n const childParsed = parseSitemap(childDoc.body);\n if (childParsed.kind !== \"urlset\") {\n diagnostics.childSitemapErrors += 1;\n diagnostics.warnings.push(`Child sitemap not a urlset: ${childUrl}`);\n continue;\n }\n if (pushEntries(collected, seen, childParsed.urls, maxUrls)) truncated = true;\n }\n }\n\n diagnostics.urlCount = collected.length;\n\n // A sitemap that exists but lists nothing is useless for navigation —\n // fall back to crawling so the user still gets a page list (common for\n // freshly-deployed sites that ship an empty `<urlset>`).\n if (collected.length === 0 && options.crawlFallback !== false) {\n return crawlSite(baseUrl, origin, options, diagnostics);\n }\n\n return {\n origin,\n baseUrl,\n source: candidate.source,\n urls: collected,\n diagnostics,\n truncated,\n };\n }\n\n // 3. No sitemap — optionally crawl.\n if (options.crawlFallback !== false) {\n return crawlSite(baseUrl, origin, options, diagnostics);\n }\n\n diagnostics.warnings.push(\"No sitemap found.\");\n return { origin, baseUrl, source: \"well-known\", urls: [], diagnostics, truncated: false };\n}\n\n/**\n * Crawl the site as a navigation source, preserving whatever sitemap\n * diagnostics were already gathered. Used both when no sitemap exists\n * and when the located sitemap was empty.\n */\nasync function crawlSite(\n baseUrl: string,\n origin: string,\n options: DiscoverSitemapOptions,\n diagnostics: SitemapDiagnostics,\n): Promise<SiteDiscovery> {\n const result = await crawl({\n entryUrl: baseUrl,\n depth: options.crawlDepth ?? DEFAULTS.crawlDepth,\n maxPages: options.crawlMaxPages ?? DEFAULTS.crawlMaxPages,\n inspectOptions: {\n mode: \"static\",\n probes: false,\n allowInsecureTls: options.allowInsecureTls,\n signal: options.signal,\n },\n });\n const urls = dedupe(result.visited).map((loc) => ({ loc }));\n diagnostics.urlCount = urls.length;\n diagnostics.warnings.push(\n diagnostics.found\n ? \"Sitemap found but lists no URLs — crawled the site instead.\"\n : \"No sitemap found — URLs were discovered by crawling links.\",\n );\n return {\n origin,\n baseUrl,\n source: \"crawl\",\n urls,\n diagnostics,\n truncated: result.truncated,\n };\n}\n\n/** Append entries up to `cap`; returns true when the cap was reached. */\nfunction pushEntries(\n out: SitemapUrlEntry[],\n seen: Set<string>,\n entries: SitemapUrlEntry[],\n cap: number,\n): boolean {\n for (const entry of entries) {\n if (out.length >= cap) return true;\n if (seen.has(entry.loc)) continue;\n seen.add(entry.loc);\n out.push(entry);\n }\n return out.length >= cap;\n}\n\nfunction dedupe(urls: string[]): string[] {\n return Array.from(new Set(urls));\n}\n\nfunction isWellKnownPath(url: string): boolean {\n try {\n const path = new URL(url).pathname.toLowerCase();\n return path === \"/sitemap.xml\" || path === \"/sitemap_index.xml\";\n } catch {\n return false;\n }\n}\n\n/**\n * Fetch one sitemap document, inflating gzip when the URL ends in `.gz`\n * or the server flags `content-encoding: gzip`. Returns `{ status }`\n * with no body on any failure so the caller can fall through to the\n * next candidate without exception handling at every call site.\n */\nasync function fetchDoc(url: string, options: DiscoverSitemapOptions): Promise<FetchedDoc> {\n const { signal, cleanup } = combineSignals(\n options.signal,\n options.timeoutMs ?? DEFAULTS.timeoutMs,\n );\n const restoreTls = relaxTlsIfRequested(options.allowInsecureTls);\n try {\n const res = await fetch(url, {\n signal,\n redirect: \"follow\",\n headers: {\n accept: \"application/xml,text/xml,application/gzip;q=0.9,*/*;q=0.5\",\n },\n });\n if (!res.ok) return { status: res.status };\n\n const encoding = res.headers.get(\"content-encoding\")?.toLowerCase() ?? \"\";\n const looksGzip = url.toLowerCase().endsWith(\".gz\") || encoding.includes(\"gzip\");\n if (looksGzip) {\n // undici transparently decodes a `content-encoding: gzip` response,\n // so only inflate manually for `.gz` payloads served as raw bytes.\n const buf = Buffer.from(await res.arrayBuffer());\n try {\n return { status: res.status, body: gunzipSync(buf).toString(\"utf8\") };\n } catch {\n // Already-decoded (or not really gzip) — fall back to raw text.\n return { status: res.status, body: buf.toString(\"utf8\") };\n }\n }\n return { status: res.status, body: await res.text() };\n } catch (err) {\n // Timeouts land here, and a timeout is not a 404. Conflating them is what\n // let a 3.5 MB sitemap intermittently read as \"this site has no sitemap\",\n // which silently cost the crawl its seeds and 90% of its coverage.\n const reason = err instanceof Error ? err.message : String(err);\n return { status: 0, error: reason || \"network error\" };\n } finally {\n cleanup();\n restoreTls();\n }\n}\n\nfunction relaxTlsIfRequested(enabled: boolean | undefined): () => void {\n if (!enabled) return () => undefined;\n const previous = process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\";\n return () => {\n if (previous === undefined) delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n else process.env.NODE_TLS_REJECT_UNAUTHORIZED = previous;\n };\n}\n","import * as cheerio from \"cheerio\";\nimport type { SitemapUrlEntry } from \"./types\";\n\n/**\n * Outcome of parsing a single sitemap document. `kind` distinguishes a\n * leaf `<urlset>` (carries page URLs) from a `<sitemapindex>` (carries\n * child sitemap locations the caller must follow). `wellFormed` is false\n * when the body could not be parsed as XML or matched neither root tag.\n */\nexport type ParsedSitemap =\n | { kind: \"urlset\"; wellFormed: true; urls: SitemapUrlEntry[] }\n | { kind: \"index\"; wellFormed: true; sitemaps: string[] }\n | { kind: \"unknown\"; wellFormed: false; urls: []; sitemaps: [] };\n\n/**\n * Parse a sitemap XML body into either a list of page URLs (`<urlset>`)\n * or a list of child sitemap URLs (`<sitemapindex>`).\n *\n * We reuse `cheerio` (already a dependency) in `xmlMode` so namespaced\n * sitemaps and self-closing tags are handled by the same battle-tested\n * parser the extractor relies on. Tag lookups are case-insensitive and\n * namespace-agnostic (we match on the local name) because real-world\n * sitemaps mix `<loc>`, `<url:loc>`, and odd casings.\n */\nexport function parseSitemap(body: string): ParsedSitemap {\n const trimmed = body.trim();\n if (trimmed.length === 0) {\n return { kind: \"unknown\", wellFormed: false, urls: [], sitemaps: [] };\n }\n\n let $: cheerio.CheerioAPI;\n try {\n $ = cheerio.load(trimmed, { xmlMode: true });\n } catch {\n return { kind: \"unknown\", wellFormed: false, urls: [], sitemaps: [] };\n }\n\n // A sitemap index references other sitemaps; prefer it when present so a\n // root index isn't mistaken for a (likely empty) urlset.\n if ($(\"sitemapindex\").length > 0) {\n const sitemaps: string[] = [];\n const seen = new Set<string>();\n $(\"sitemap > loc\").each((_, el) => {\n const loc = $(el).text().trim();\n if (loc && !seen.has(loc)) {\n seen.add(loc);\n sitemaps.push(loc);\n }\n });\n return { kind: \"index\", wellFormed: true, sitemaps };\n }\n\n if ($(\"urlset\").length > 0 || $(\"url\").length > 0) {\n const urls: SitemapUrlEntry[] = [];\n const seen = new Set<string>();\n $(\"url\").each((_, el) => {\n const node = $(el);\n const loc = node.find(\"loc\").first().text().trim();\n if (!loc || seen.has(loc)) return;\n seen.add(loc);\n const entry: SitemapUrlEntry = { loc };\n const lastmod = node.find(\"lastmod\").first().text().trim();\n if (lastmod) entry.lastmod = lastmod;\n const changefreq = node.find(\"changefreq\").first().text().trim();\n if (changefreq) entry.changefreq = changefreq;\n const priority = node.find(\"priority\").first().text().trim();\n if (priority) entry.priority = priority;\n urls.push(entry);\n });\n return { kind: \"urlset\", wellFormed: true, urls };\n }\n\n return { kind: \"unknown\", wellFormed: false, urls: [], sitemaps: [] };\n}\n","/**\n * Choosing which pages to audit, by the shape of the site rather than by a cap.\n *\n * `--max-pages` answers \"how many\" and never \"which\". On a site of 4451 pages\n * built from about thirty templates it audits the first two hundred the crawl\n * reaches, which is four templates out of thirty — and it cuts through the\n * middle of a family, so the two hundred are neither a sample nor a subset of\n * anything meaningful.\n *\n * This picks by structure instead: every page that stands alone, and a handful\n * from each family of pages that share a template. Design and the measurements\n * behind it: `docs/coverage-plan.md`.\n */\n\n/** A group of URLs that share a path shape, and therefore a template. */\nexport interface RouteFamily {\n /** The shape, with variable segments replaced: `/{locale}/blog/{2}`. */\n pattern: string;\n /** How many URLs matched it. */\n size: number;\n /** How many of them were selected. */\n sampled: number;\n}\n\nexport interface CoverageSelection {\n /** The URLs to crawl and audit, in the order they were given. */\n urls: string[];\n /** Every family found, largest first. Families of one are omitted. */\n families: RouteFamily[];\n /** Total URLs considered. */\n total: number;\n}\n\nexport interface SelectOptions {\n /** Locale segments to fold into `{locale}`, so translations share a family. */\n locales?: readonly string[];\n /** How many URLs to keep per family. */\n perFamily?: number;\n /** Below this many URLs, a group is not a family and is kept whole. */\n threshold?: number;\n}\n\n/**\n * `k=3`, and the reasoning is the whole reason it is not 1.\n *\n * A family is homogeneous in what its template produces — the canonical, the\n * hreflang cluster, whether there is an `og:image`. It is not homogeneous in\n * what the content brings: `title.length` and `description.length` judge the\n * copy, and one sample sits wherever it happens to sit in that distribution.\n * Three catch a tail that one misses.\n */\nconst DEFAULT_PER_FAMILY = 3;\n\n/**\n * Eight, because below it the saving does not pay for the weakened promise.\n * Sampling three of five pages costs the guarantee on two and saves two\n * fetches.\n */\nconst DEFAULT_THRESHOLD = 8;\n\n/** Path segments, without the leading empty one. */\nfunction segments(url: string): string[] {\n try {\n return new URL(url).pathname.split(\"/\").filter(Boolean);\n } catch {\n return [];\n }\n}\n\n/**\n * The shape of a URL, with locale segments folded.\n *\n * Folding the locale is what makes `/en/blog/x` and `/fr/blog/x` one family\n * rather than two, which matters because they are one template. They are still\n * sampled separately — see `keyOf` — since the copy differs and the copy is\n * what the length rules judge.\n */\nfunction shapeOf(parts: readonly string[], locales: ReadonlySet<string>): string[] {\n return parts.map((part) => (locales.has(part.toLowerCase()) ? \"{locale}\" : part));\n}\n\n/**\n * Group URLs by the shape they share once their variable segment is blanked.\n *\n * A segment is variable at depth `d` when many URLs agree on everything before\n * `d` and disagree at `d`. That is inferred rather than declared, because\n * goflag audits sites it did not produce and cannot ask them for their routes.\n */\nfunction patternOf(parts: readonly string[], varDepths: ReadonlySet<string>): string {\n const out: string[] = [];\n for (let i = 0; i < parts.length; i += 1) {\n // Keyed on the raw prefix, which is how `childrenByPrefix` indexed it. An\n // earlier substitution must not change the key, or every depth past the\n // first variable one stops matching and nothing collapses.\n const prefix = parts.slice(0, i).join(\"/\");\n out.push(varDepths.has(`${i}:${prefix}`) ? `{${i}}` : parts[i]!);\n }\n return `/${out.join(\"/\")}`;\n}\n\n/**\n * Pick representatives from a sorted list: first, last, and evenly spaced\n * middles.\n *\n * Deterministic on purpose. A random sample would make two runs of the same\n * site incomparable, which is the defect this whole file exists to avoid\n * repeating — the report has to be diffable against a baseline or it is not a\n * gate.\n */\nfunction representatives(sorted: readonly string[], k: number): string[] {\n if (sorted.length <= k) return [...sorted];\n if (k <= 1) return [sorted[0]!];\n\n const picked: string[] = [];\n for (let i = 0; i < k; i += 1) {\n picked.push(sorted[Math.round((i * (sorted.length - 1)) / (k - 1))]!);\n }\n return [...new Set(picked)];\n}\n\nexport function selectByStructure(\n urls: readonly string[],\n options: SelectOptions = {},\n): CoverageSelection {\n const perFamily = options.perFamily ?? DEFAULT_PER_FAMILY;\n const threshold = options.threshold ?? DEFAULT_THRESHOLD;\n const locales = new Set((options.locales ?? []).map((l) => l.toLowerCase()));\n\n const parsed = urls.map((url) => ({ url, parts: shapeOf(segments(url), locales) }));\n\n // Which (depth, prefix) pairs vary. A prefix whose children are many and\n // distinct is a dynamic segment; one with a handful of children is a\n // hand-written menu and every item deserves its own look.\n const childrenByPrefix = new Map<string, Set<string>>();\n for (const { parts } of parsed) {\n for (let i = 0; i < parts.length; i += 1) {\n const key = `${i}:${parts.slice(0, i).join(\"/\")}`;\n const set = childrenByPrefix.get(key) ?? new Set<string>();\n set.add(parts[i]!);\n childrenByPrefix.set(key, set);\n }\n }\n\n // The first real segment is never variable, however many children it has.\n //\n // Those are the site's top-level sections — `/blog`, `/glossary`, `/stet`.\n // A dozen of them crosses the threshold and would collapse into one pattern,\n // which reads as one family and is a dozen different templates.\n //\n // Which depth that is depends on the site: on a localised one the locale\n // occupies depth 0, so the sections are at depth 1; without a locale prefix\n // they are at depth 0. Protecting depth 1 unconditionally would exempt\n // `/blog/{slug}` on every unlocalised site and the feature would do nothing\n // there.\n const varDepths = new Set<string>();\n for (const [key, children] of childrenByPrefix) {\n const colon = key.indexOf(\":\");\n const depth = Number(key.slice(0, colon));\n const prefix = key.slice(colon + 1);\n\n const isFirstSection = depth === 0 || (depth === 1 && prefix === \"{locale}\");\n if (isFirstSection) continue;\n if (children.size >= threshold) varDepths.add(key);\n }\n\n // Group, then sample. The locale is part of the grouping key even though it\n // is not part of the pattern: one template, four translations, and the copy\n // rules judge each translation on its own words.\n const groups = new Map<string, { pattern: string; urls: string[] }>();\n for (const { url, parts } of parsed) {\n const pattern = patternOf(parts, varDepths);\n const localePart = segments(url).find((p) => locales.has(p.toLowerCase())) ?? \"\";\n const key = `${localePart}|${pattern}`;\n const group = groups.get(key) ?? { pattern, urls: [] };\n group.urls.push(url);\n groups.set(key, group);\n }\n\n const keep = new Set<string>();\n const families = new Map<string, RouteFamily>();\n\n for (const { pattern, urls: groupUrls } of groups.values()) {\n // No shallowness test here. The root and the first section are already\n // protected above, by never being marked variable — so they each get a\n // pattern of their own, a group of one, and are kept by the size test\n // below. A second guard on pattern depth looked like the same rule and was\n // not: `/blog/{slug}` is two segments deep on a site with no locale\n // prefix, and it exempted exactly the family the feature exists to sample.\n const sorted = [...groupUrls].sort();\n const chosen = sorted.length < threshold ? sorted : representatives(sorted, perFamily);\n for (const url of chosen) keep.add(url);\n\n if (sorted.length >= threshold) {\n const seen = families.get(pattern);\n families.set(pattern, {\n pattern,\n size: (seen?.size ?? 0) + sorted.length,\n sampled: (seen?.sampled ?? 0) + chosen.length,\n });\n }\n }\n\n return {\n urls: urls.filter((url) => keep.has(url)),\n families: [...families.values()].sort((a, b) => b.size - a.size),\n total: urls.length,\n };\n}\n","/**\n * Advisory collection — turning prose rules into evidence bundles.\n *\n * Pure: `Extraction` → `AdvisoryFinding[]`, one per prose rule whose subject\n * this page actually has. The gate is presence only (`ProseRule.appliesTo`):\n * asking \"does the description summarize the page?\" where there is no\n * description is noise on top of the deterministic finding that already\n * covers it. Nothing beyond presence may silence a question — a rule skipped\n * because the text \"looked fine\" would be goflag making exactly the judgment\n * it declined to make.\n *\n * The only real work is resolving the declared extraction paths. Absence is\n * recorded as `null`, never as a missing key — \"this page has no og:image\"\n * is evidence, and a missing key could not be told apart from a path that\n * pointed nowhere.\n */\n\nimport type { AdvisoryFinding, Extraction, ProseRule } from \"./types\";\n\n/**\n * Read a dotted path out of the extraction: `meta.description`,\n * `openGraph.images`, `http.finalUrl`.\n *\n * Returns `null` for any path that runs off the end of the data — an absent\n * optional field and a typo look identical at runtime, which is why every\n * declared path is checked against a maximal fixture in `prose.test.ts`\n * rather than trusted here.\n *\n * Values come back as they sit in the model: a `Fact` (value + origin + raw)\n * for scalar observations, a plain array or string for the already-\n * normalized collections and `http` fields. Handing an agent the `Fact` is\n * the point — it can answer \"says which tag?\" without a second call.\n */\nexport function resolveEvidencePath(extraction: Extraction, path: string): unknown {\n let current: unknown = extraction;\n for (const segment of path.split(\".\")) {\n if (current === null || current === undefined) return null;\n if (typeof current !== \"object\") return null;\n current = (current as Record<string, unknown>)[segment];\n }\n return current ?? null;\n}\n\n/** Resolve every declared path into the bundle an agent receives. */\nexport function evidenceFor(\n extraction: Extraction,\n paths: ReadonlyArray<string>,\n): Record<string, unknown> {\n const bundle: Record<string, unknown> = {};\n for (const path of paths) bundle[path] = resolveEvidencePath(extraction, path);\n return bundle;\n}\n\n/**\n * Build the advisory bundle for one page. `verdict` is hard-coded to\n * `needs-judgment` by construction: there is no code path in goflag that\n * can set it to anything else, which is the guarantee the whole design\n * rests on.\n */\nexport function collectAdvisories(\n extraction: Extraction,\n rules: ReadonlyArray<ProseRule>,\n): AdvisoryFinding[] {\n return rules\n .filter((rule) => rule.appliesTo?.(extraction) ?? true)\n .map((rule) => ({\n ruleId: rule.id,\n kind: \"prose\",\n prose: rule.prose,\n rigor: rule.rigor,\n sources: rule.sources,\n evidence: evidenceFor(extraction, rule.reads),\n verdict: \"needs-judgment\",\n }));\n}\n","/**\n * Profile registry and the overlay that applies one (plan §9).\n *\n * The shipped profiles answer a question the rule registry deliberately\n * cannot: *how much does this matter to you?* A rule's `rigor` says how\n * authoritative the requirement is; a profile says what your build should\n * do about it. Those are different questions, and conflating them is how\n * linters end up either shouting about folklore or shrugging at spec\n * violations.\n *\n * Applying a profile is pure and total: `Rule[] → Rule[]`, no mutation of\n * the input descriptors, resolved once per run.\n */\n\nimport { RULES } from \"../index\";\nimport type { Rule, Severity } from \"../types\";\nimport type { Profile, RuleOverride } from \"./types\";\n\n/** The profile in force when `--profile` is not given. */\nexport const DEFAULT_PROFILE = \"default\";\n\n/**\n * The shipped profiles.\n *\n * Order matters only for the `--help` listing: default first, then\n * increasingly opinionated.\n */\nexport const PROFILES: Readonly<Record<string, Profile>> = {\n default: {\n name: \"default\",\n description: \"rule severities exactly as authored\",\n },\n\n strict: {\n name: \"strict\",\n description: \"every spec-backed rule fails the build\",\n // The line is drawn at \"is there a document that says so\", not at \"does\n // it feel important\". Heuristics stay warnings even here — promoting\n // SERP-length folklore to an error would be exactly the dishonesty the\n // rigor axis exists to prevent.\n byRigor: {\n \"spec-required\": { severity: \"error\" },\n \"spec-recommended\": { severity: \"error\" },\n \"vendor-spec\": { severity: \"error\" },\n guideline: { severity: \"warning\" },\n heuristic: { severity: \"warning\" },\n },\n },\n\n \"spec-only\": {\n name: \"spec-only\",\n description: \"heuristic rules switched off entirely\",\n // Not a severity change: a heuristic finding is switched off, so it is\n // absent from the conformance view rather than sitting there as a\n // suspiciously quiet `pass`.\n byRigor: {\n heuristic: { enabled: false },\n },\n },\n\n marketing: {\n name: \"marketing\",\n description: \"snippet and unfurl metadata gaps are errors\",\n // The recommended/optional distinction inverts for a page whose job is\n // to be shared: a missing description or preview image costs a click,\n // whatever the spec calls it.\n rules: {\n \"description.missing\": { severity: \"error\" },\n \"description.length\": { severity: \"warning\" },\n \"og.title.missing\": { severity: \"error\" },\n \"og.image.missing\": { severity: \"error\" },\n \"og.description.missing\": { severity: \"warning\" },\n },\n },\n};\n\n/** Profile names, in listing order. */\nexport const PROFILE_NAMES: readonly string[] = Object.keys(PROFILES);\n\n/**\n * Look up a profile by name. `undefined` resolves to `default`, so callers\n * can pass an optional flag straight through. Throws with the full list on\n * an unknown name — a typo must never silently audit under a different\n * policy than the one asked for.\n */\nexport function resolveProfile(name: string = DEFAULT_PROFILE): Profile {\n const profile = PROFILES[name];\n if (!profile) {\n throw new Error(`unknown profile \"${name}\" — expected one of: ${PROFILE_NAMES.join(\", \")}`);\n }\n return profile;\n}\n\n/** The effective override for one rule: rigor band first, per-rule id last. */\nfunction overrideFor(rule: Rule, profile: Profile): RuleOverride {\n return { ...profile.byRigor?.[rule.rigor], ...profile.rules?.[rule.id] };\n}\n\n/** Re-stamp a rule's severity, respecting the shape of its `kind`. */\nfunction withSeverity(rule: Rule, severity: Severity): Rule {\n if (rule.kind === \"boolean\") return { ...rule, severity };\n return { ...rule, severityByBand: { acceptable: severity, poor: severity } };\n}\n\n/**\n * Overlay a profile onto a rule set. Disabled rules drop out; the rest are\n * returned in registry order, as fresh objects — the shared `RULES`\n * descriptors are never mutated, so two audits under different profiles in\n * one process cannot contaminate each other.\n */\nexport function applyProfile(rules: ReadonlyArray<Rule>, profile: Profile): ReadonlyArray<Rule> {\n const effective: Rule[] = [];\n for (const rule of rules) {\n const override = overrideFor(rule, profile);\n if (override.enabled === false) continue;\n effective.push(override.severity ? withSeverity(rule, override.severity) : rule);\n }\n return effective;\n}\n\n/**\n * Resolve a profile name and apply it in one step — what `runAudit` calls.\n * The `rules` parameter exists for tests; production always overlays the\n * full registry.\n */\nexport function rulesForProfile(\n name?: string,\n rules: ReadonlyArray<Rule> = RULES,\n): ReadonlyArray<Rule> {\n return applyProfile(rules, resolveProfile(name));\n}\n\nexport type { Profile, RuleOverride } from \"./types\";\n","/**\n * Prose rule registry — the policies goflag can state but must not judge\n * (plan §8).\n *\n * Every rule here is one goflag could *fake*: it could count words, match a\n * boilerplate string, run a readability score, and emit a confident verdict\n * about whether a title \"describes the page\". It deliberately does not. A\n * fabricated judgment on a question of meaning is worse than no judgment —\n * it is unfalsifiable noise a developer learns to ignore, and an agent\n * cannot tell it apart from a real finding.\n *\n * So a prose rule ships three things and stops: the question, the sources\n * that make it a real requirement rather than an opinion, and the observed\n * facts the answer turns on. `./advisory.ts` resolves the facts; an agent\n * supplies the verdict.\n *\n * The bar for adding one: it must be *checkable by a reader looking at the\n * evidence bundle alone*. \"Is the title compelling?\" fails that bar — no\n * evidence settles it. \"Does the title describe the content this page\n * actually serves?\" passes: title, description and URL are on the table.\n */\n\nimport type { ProseRule } from \"./types\";\n\nconst titleDescriptive: ProseRule = {\n id: \"title.descriptive\",\n kind: \"prose\",\n category: \"document\",\n title: \"The `<title>` describes this specific page\",\n why:\n \"A title that repeats the site name, or describes the section rather \" +\n \"than the page, gives a searcher no way to tell two results apart — and \" +\n \"gives Google a reason to rewrite it into something you did not choose.\",\n rigor: \"guideline\",\n sources: [\"google-title-link\", \"bing-webmaster-guidelines\"],\n reads: [\"document.title\", \"meta.description\", \"openGraph.title\", \"http.finalUrl\"],\n relates: [\"title.missing\", \"title.length\"],\n appliesTo: (ex) => Boolean(ex.document.title?.value?.trim()),\n prose:\n \"Does the title describe what is on THIS page specifically — not the \" +\n \"site, not the section — and would it be distinguishable from the other \" +\n \"pages of this site in a list of search results?\",\n};\n\nconst descriptionAccurate: ProseRule = {\n id: \"description.accurate\",\n kind: \"prose\",\n category: \"meta\",\n title: \"The meta description summarizes this page truthfully\",\n why:\n \"The description is the one sentence you get to write in a search \" +\n \"result. A boilerplate line repeated site-wide, or one that promises \" +\n \"something the page does not deliver, gets replaced by improvised page \" +\n \"text — or gets the click and loses the visitor.\",\n rigor: \"guideline\",\n sources: [\"google-snippet\", \"moz-meta-description\"],\n reads: [\"meta.description\", \"document.title\", \"http.finalUrl\"],\n relates: [\"description.missing\", \"description.length\"],\n appliesTo: (ex) => Boolean(ex.meta.description?.value?.trim()),\n prose:\n \"Does the description accurately summarize this page's content, and is \" +\n \"it written for this page rather than copied across the site?\",\n};\n\nconst langMatchesContent: ProseRule = {\n id: \"lang.matches-content\",\n kind: \"prose\",\n category: \"i18n\",\n title: \"The declared `lang` is the language the page is actually written in\",\n why:\n \"A wrong `lang` is worse than a missing one: screen readers switch to \" +\n \"the wrong pronunciation rules, browsers offer to translate a page that \" +\n \"is already in the reader's language, and search engines file the page \" +\n \"under the wrong audience. Nothing in the markup contradicts it, so no \" +\n \"mechanical check can catch it.\",\n rigor: \"spec-required\",\n sources: [\"whatwg-html-lang\", \"w3c-i18n-language-tags\"],\n reads: [\"document.lang\", \"document.title\", \"meta.description\", \"openGraph.locale\"],\n appliesTo: (ex) => Boolean(ex.document.lang?.value?.trim()),\n prose:\n \"Is the text of this page actually written in the language its `lang` \" + \"attribute declares?\",\n};\n\nconst ogImageRepresentative: ProseRule = {\n id: \"og.image.representative\",\n kind: \"prose\",\n category: \"opengraph\",\n title: \"The `og:image` represents this page and survives the unfurl crop\",\n why:\n \"The preview image is the whole payload of a shared link. A generic \" +\n \"site-wide banner, or artwork whose subject sits outside the ~1.91:1 \" +\n \"crop, is present enough to pass every mechanical check and still \" +\n \"communicate nothing.\",\n rigor: \"guideline\",\n sources: [\"ogp\", \"meta-og-sharing\"],\n reads: [\"openGraph.images\", \"openGraph.title\", \"document.title\", \"http.finalUrl\"],\n relates: [\"og.image.missing\"],\n appliesTo: (ex) => ex.openGraph.images.length > 0,\n prose:\n \"Does the og:image represent what this page is about, rather than being \" +\n \"a site-wide default, and does its subject survive being cropped to the \" +\n \"1.91:1 aspect ratio consumers render?\",\n};\n\n/** The full set of prose rules, alphabetised by id (stable output order). */\nexport const PROSE_RULES: ReadonlyArray<ProseRule> = [\n descriptionAccurate,\n langMatchesContent,\n ogImageRepresentative,\n titleDescriptive,\n];\n\nconst PROSE_RULE_BY_ID: Map<string, ProseRule> = new Map(PROSE_RULES.map((r) => [r.id, r]));\n\nexport function getProseRule(id: string): ProseRule | undefined {\n return PROSE_RULE_BY_ID.get(id);\n}\n\nexport type { AdvisoryFinding, ProseRule } from \"./types\";\n","/**\n * Shared low-level fetch primitive.\n *\n * A politer, more general cousin of the private `fetchDoc` inside\n * `sitemap/discover.ts`. The link engine (`links/check.ts`,\n * `links/audit.ts`) and the strengthened sitemap engine both need a\n * single \"fetch a URL and tell me its status / final URL / body\" helper\n * that:\n *\n * - composes a caller `AbortSignal` with a timeout (`combineSignals`),\n * - relaxes TLS verification on request (the `relaxTlsIfRequested`\n * pattern from `discover.ts`),\n * - sends a real browser User-Agent by default so bot-detection\n * middleware doesn't return false-positive 403s, and\n * - never throws — every failure collapses into a shaped result with a\n * `status: 0` and a `reason`, mirroring the rest of the engine.\n *\n * Like everything under `src/lib/core/**`, this module is plain and\n * JSON-serializable with no Next.js / React / DOM coupling so it can\n * ship as part of `@goflag/core`.\n */\n\nimport { combineSignals } from \"../probes/abort\";\n\n/** Why a fetch produced no usable HTTP response. */\nexport type FetchFailureReason = \"timeout\" | \"dns\" | \"tls\" | \"abort\" | \"network\";\n\nexport interface FetchUrlOptions {\n /** Caller-driven cancellation, composed with the timeout. */\n signal?: AbortSignal;\n /** Per-request timeout in ms. Defaults to 8_000. */\n timeoutMs?: number;\n /** HTTP method. Defaults to \"GET\". HEAD never returns a body. */\n method?: \"GET\" | \"HEAD\";\n /** Allow self-signed / invalid TLS (localhost, tunnels). */\n allowInsecureTls?: boolean;\n /** Override the User-Agent. Defaults to a real browser UA. */\n userAgent?: string;\n /**\n * Redirect handling. \"follow\" (default) lets the platform follow\n * redirects and reports the final URL. \"manual\" performs a single hop\n * and resolves the `Location` header into `finalUrl` so a caller can\n * drive its own redirect loop (used by `checkLink`).\n */\n redirect?: \"follow\" | \"manual\";\n /**\n * Cap on the number of body bytes read for GET. Defaults to 3 MB.\n * Bodies are truncated (not omitted) at the cap so HTML scans still\n * get most of the document.\n */\n maxBytes?: number;\n /** Override the Accept header. */\n accept?: string;\n}\n\nexport interface FetchUrlResult {\n /** The URL we were asked to fetch. */\n requestedUrl: string;\n /** The URL after redirects (== requestedUrl when none / on error). */\n finalUrl: string;\n /** HTTP status. `0` on network error. */\n status: number;\n /** True when at least one redirect was observed. */\n redirected: boolean;\n /**\n * In \"manual\" mode, the single next-hop `Location` (resolved\n * absolute) when the response was a redirect; otherwise empty.\n */\n redirectChain: string[];\n /** Lower-cased content-type sans parameters, when the server sent one. */\n contentType?: string;\n /** Raw `Retry-After` header value, when present (for 429 / 503 backoff). */\n retryAfter?: string;\n /** Decoded text body. Omitted for HEAD, non-text, or network errors. */\n body?: string;\n /** True when the body was truncated at `maxBytes`. */\n truncated?: boolean;\n /** Failure classification when `status === 0`. */\n reason?: FetchFailureReason;\n /** Wall-clock duration of the attempt in ms. */\n durationMs: number;\n}\n\n/**\n * A current, real desktop Chrome UA string. Bot-detection middleware on\n * many hosts returns 403/429 for obviously-scripted UAs; presenting a\n * believable browser keeps the link checker's false-positive rate low.\n */\nexport const DEFAULT_USER_AGENT =\n \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \" +\n \"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\";\n\nconst DEFAULT_TIMEOUT_MS = 8_000;\nconst DEFAULT_MAX_BYTES = 3 * 1024 * 1024;\nconst DEFAULT_ACCEPT =\n \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,\" + \"image/webp,*/*;q=0.8\";\n\n/** Content-type families we will decode into `body`. */\nconst TEXTY = /(text\\/|application\\/(xhtml\\+xml|xml|json|.*\\+xml|.*\\+json|javascript))/;\n\nexport async function fetchUrl(\n url: string,\n options: FetchUrlOptions = {},\n): Promise<FetchUrlResult> {\n const method = options.method ?? \"GET\";\n const redirect = options.redirect ?? \"follow\";\n const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n const startedAt = Date.now();\n\n const { signal, cleanup } = combineSignals(\n options.signal,\n options.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n );\n const restoreTls = relaxTlsIfRequested(options.allowInsecureTls);\n\n try {\n const res = await fetch(url, {\n method,\n signal,\n redirect,\n headers: {\n \"user-agent\": options.userAgent ?? DEFAULT_USER_AGENT,\n accept: options.accept ?? DEFAULT_ACCEPT,\n },\n });\n\n const contentType = normaliseContentType(res.headers.get(\"content-type\"));\n const result: FetchUrlResult = {\n requestedUrl: url,\n finalUrl: res.url || url,\n status: res.status,\n redirected: res.redirected,\n redirectChain: [],\n contentType,\n durationMs: Date.now() - startedAt,\n };\n const retryAfter = res.headers.get(\"retry-after\");\n if (retryAfter) result.retryAfter = retryAfter;\n\n if (redirect === \"manual\" && isRedirectStatus(res.status)) {\n const location = res.headers.get(\"location\");\n if (location) {\n try {\n const next = new URL(location, url).toString();\n result.finalUrl = next;\n result.redirectChain = [next];\n result.redirected = true;\n } catch {\n // A malformed Location header — leave finalUrl as requested.\n }\n }\n // Drain so the socket can be reused; never read a redirect body.\n await res.body?.cancel().catch(() => undefined);\n return result;\n }\n\n if (method === \"HEAD\") {\n await res.body?.cancel().catch(() => undefined);\n return result;\n }\n\n if (contentType && !TEXTY.test(contentType)) {\n await res.body?.cancel().catch(() => undefined);\n return result;\n }\n\n const { text, truncated } = await readCappedText(res, maxBytes);\n result.body = text;\n if (truncated) result.truncated = true;\n return result;\n } catch (err) {\n return {\n requestedUrl: url,\n finalUrl: url,\n status: 0,\n redirected: false,\n redirectChain: [],\n reason: classifyError(err, options.signal),\n durationMs: Date.now() - startedAt,\n };\n } finally {\n cleanup();\n restoreTls();\n }\n}\n\nfunction isRedirectStatus(status: number): boolean {\n return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;\n}\n\nfunction normaliseContentType(raw: string | null): string | undefined {\n if (!raw) return undefined;\n const semi = raw.indexOf(\";\");\n return (semi === -1 ? raw : raw.slice(0, semi)).trim().toLowerCase() || undefined;\n}\n\n/**\n * Read a response body as UTF-8 text, stopping once `maxBytes` have been\n * consumed. Falls back to a buffered read if the body isn't a stream.\n */\nasync function readCappedText(\n res: Response,\n maxBytes: number,\n): Promise<{ text: string; truncated: boolean }> {\n const reader = res.body?.getReader();\n if (!reader) {\n const text = await res.text();\n if (text.length > maxBytes) {\n return { text: text.slice(0, maxBytes), truncated: true };\n }\n return { text, truncated: false };\n }\n\n const chunks: Uint8Array[] = [];\n let received = 0;\n let truncated = false;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (!value) continue;\n chunks.push(value);\n received += value.byteLength;\n if (received >= maxBytes) {\n truncated = true;\n await reader.cancel().catch(() => undefined);\n break;\n }\n }\n\n const buf = Buffer.concat(chunks.map((c) => Buffer.from(c)));\n const sliced = buf.byteLength > maxBytes ? buf.subarray(0, maxBytes) : buf;\n return { text: sliced.toString(\"utf8\"), truncated };\n}\n\n/** Map a thrown fetch error onto a coarse, reportable reason. */\nfunction classifyError(err: unknown, callerSignal?: AbortSignal): FetchFailureReason {\n if (isAbortError(err)) {\n // combineSignals fires the same controller for both the timeout and a\n // caller cancel, so distinguish by inspecting the caller's signal.\n return callerSignal?.aborted ? \"abort\" : \"timeout\";\n }\n const code = errorCode(err);\n if (code) {\n if (code === \"ENOTFOUND\" || code === \"EAI_AGAIN\") return \"dns\";\n if (code.startsWith(\"CERT_\") || code.includes(\"SELF_SIGNED\") || code.includes(\"CERT\")) {\n return \"tls\";\n }\n }\n const message = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();\n if (message.includes(\"certificate\") || message.includes(\"tls\") || message.includes(\"ssl\")) {\n return \"tls\";\n }\n return \"network\";\n}\n\nfunction isAbortError(err: unknown): boolean {\n return err instanceof Error && err.name === \"AbortError\";\n}\n\nfunction errorCode(err: unknown): string | undefined {\n if (err && typeof err === \"object\") {\n const direct = (err as { code?: unknown }).code;\n if (typeof direct === \"string\") return direct;\n const cause = (err as { cause?: unknown }).cause;\n if (cause && typeof cause === \"object\") {\n const causeCode = (cause as { code?: unknown }).code;\n if (typeof causeCode === \"string\") return causeCode;\n }\n }\n return undefined;\n}\n\n/**\n * Temporarily disable Node's TLS verification when the caller opted in\n * (localhost / self-signed tunnels). Returns a restore callback the\n * caller MUST run in `finally`. Mirrors `discover.ts`.\n */\nfunction relaxTlsIfRequested(enabled: boolean | undefined): () => void {\n if (!enabled) return () => undefined;\n const previous = process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\";\n return () => {\n if (previous === undefined) delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n else process.env.NODE_TLS_REJECT_UNAUTHORIZED = previous;\n };\n}\n","/**\n * Link extraction for the link checker.\n *\n * Unlike `extractCandidateLinks` in `src/lib/core/discover.ts` — which is\n * crawl-scoped (same-origin only, regex, `href` only) — the link checker\n * needs *all* links with reporting metadata. We parse with cheerio\n * (already a dependency) for accuracy.\n *\n * Default scope is `<a href>` only (matches the user's \"all links\"\n * intent). An `includeAssets` toggle widens the net to `<img src>`,\n * `<script src>`, `<link href>`, and `<iframe src>` so broken\n * images/assets surface too.\n *\n * Every URL is resolved against the page's (post-redirect) base URL,\n * then canonicalised with `canonicaliseUrl` (reused from `crawl.ts`,\n * which strips the fragment, preserves trailing slashes and rejects\n * `mailto:` / `tel:` / `javascript:`). Non-canonicalisable links (e.g.\n * `mailto:`) are still reported — with their `rawHref` — so the checker\n * can mark them `skipped` rather than silently dropping them.\n */\n\nimport * as cheerio from \"cheerio\";\nimport { canonicaliseUrl } from \"../crawl\";\nimport type { LinkKind, LinkRef, LinkSource } from \"./types\";\n\nexport interface ExtractLinksOptions {\n /** Base URL to resolve relative links against (the page's final URL). */\n baseUrl: string;\n /** Include `<img>/<script>/<link>/<iframe>` sources. Defaults to false. */\n includeAssets?: boolean;\n}\n\ninterface ElementSpec {\n selector: string;\n attr: \"href\" | \"src\";\n source: LinkSource;\n}\n\nconst ANCHOR_SPEC: ElementSpec = { selector: \"a[href]\", attr: \"href\", source: \"a\" };\nconst ASSET_SPECS: ElementSpec[] = [\n { selector: \"img[src]\", attr: \"src\", source: \"img\" },\n { selector: \"script[src]\", attr: \"src\", source: \"script\" },\n { selector: \"link[href]\", attr: \"href\", source: \"link\" },\n { selector: \"iframe[src]\", attr: \"src\", source: \"iframe\" },\n];\n\n/**\n * Parse `html` and return every link occurrence. Per-page de-duplication\n * is intentionally *not* done here (the orchestrator dedupes globally and\n * needs the per-page occurrences for the report); however identical\n * (url, source) pairs within a single page are collapsed to keep the\n * occurrence list tidy.\n */\nexport function extractLinks(html: string, options: ExtractLinksOptions): LinkRef[] {\n const $ = cheerio.load(html);\n // Relative links resolve against <base href> when present; \"internal vs\n // external\" is judged against the *audited* origin (the page URL).\n const base = resolveBase($, options.baseUrl);\n const origin = safeOrigin(options.baseUrl);\n const specs = options.includeAssets ? [ANCHOR_SPEC, ...ASSET_SPECS] : [ANCHOR_SPEC];\n\n const out: LinkRef[] = [];\n const seen = new Set<string>();\n\n for (const spec of specs) {\n $(spec.selector).each((_, el) => {\n const raw = $(el).attr(spec.attr);\n if (raw === undefined) return;\n const rawHref = raw.trim();\n if (!rawHref) return;\n\n const ref = buildRef($, el, rawHref, spec, base, origin);\n if (!ref) return;\n\n const dedupeKey = `${ref.source}|${ref.url}`;\n if (seen.has(dedupeKey)) return;\n seen.add(dedupeKey);\n out.push(ref);\n });\n }\n\n return out;\n}\n\nfunction buildRef(\n $: cheerio.CheerioAPI,\n el: ReturnType<cheerio.CheerioAPI>[number],\n rawHref: string,\n spec: ElementSpec,\n base: string,\n origin: string | null,\n): LinkRef | null {\n const fragment = extractFragment(rawHref);\n // Probe the URL as authored. Collapsing `/x/` to `/x` is a crawl-frontier\n // optimisation; here it would change the question from \"does this link\n // work?\" to \"does a URL we invented work?\".\n const canonical = canonicaliseUrl(rawHref, base, { preserveTrailingSlash: true });\n\n // Non-http(s) or otherwise un-canonicalisable links (mailto:, tel:,\n // javascript:, pure \"#anchor\"): keep them keyed by their raw form so\n // the checker can mark them `skipped` instead of dropping them.\n const url = canonical ?? rawHref;\n const kind: LinkKind = canonical ? classify(canonical, origin) : \"external\";\n\n const ref: LinkRef = {\n rawHref,\n url,\n kind,\n source: spec.source,\n rel: spec.source === \"a\" || spec.source === \"link\" ? parseRel($(el).attr(\"rel\")) : [],\n };\n\n if (spec.source === \"a\") {\n const text = $(el).text().trim();\n if (text) ref.anchorText = text;\n }\n if (fragment) ref.fragment = fragment;\n\n return ref;\n}\n\n/** Pull the `#fragment` (with hash) off a raw href, ignoring bare anchors’ emptiness. */\nfunction extractFragment(rawHref: string): string | undefined {\n const hash = rawHref.indexOf(\"#\");\n if (hash === -1) return undefined;\n const frag = rawHref.slice(hash);\n return frag.length > 1 ? frag : undefined;\n}\n\nfunction parseRel(rel: string | undefined): string[] {\n if (!rel) return [];\n return rel\n .split(/\\s+/)\n .map((t) => t.trim().toLowerCase())\n .filter(Boolean);\n}\n\nfunction classify(url: string, origin: string | null): LinkKind {\n if (!origin) return \"external\";\n return safeOrigin(url) === origin ? \"internal\" : \"external\";\n}\n\nfunction safeOrigin(url: string): string | null {\n try {\n return new URL(url).origin;\n } catch {\n return null;\n }\n}\n\n/**\n * Resolve the effective base: prefer a `<base href>` in the document when\n * it resolves to an absolute URL, otherwise the supplied page URL.\n * Mirrors how browsers resolve relative links.\n */\nfunction resolveBase($: cheerio.CheerioAPI, pageUrl: string): string {\n const declared = $(\"base[href]\").first().attr(\"href\")?.trim();\n if (declared) {\n try {\n return new URL(declared, pageUrl).toString();\n } catch {\n // Malformed <base href> — fall back to the page URL.\n }\n }\n return pageUrl;\n}\n","/**\n * Verdict classification for the link checker.\n *\n * Turning a raw `(status, reason)` pair into a `LinkVerdict` is where a\n * credible link checker earns trust: the goal is to *avoid false\n * positives*. Redirects are a signal, not a breakage; anti-bot 403/429\n * are triaged separately from genuine 4xx/5xx; soft-404s are flagged,\n * never silently passed.\n */\n\nimport type { FetchFailureReason } from \"../net/fetch-url\";\nimport type { LinkVerdict } from \"./types\";\n\nexport interface ClassifyInput {\n /** Final HTTP status; `0` for a network failure. */\n status: number;\n /** True when at least one redirect hop was followed. */\n redirected: boolean;\n /** Network failure reason, when `status === 0`. */\n reason?: FetchFailureReason;\n /** Soft-404 heuristic tripped (200 body that says \"not found\"). */\n softNotFound?: boolean;\n /** A redirect loop or hop-cap was hit. */\n loop?: boolean;\n}\n\n/**\n * Map a check outcome onto a verdict. Pure and deterministic.\n *\n * - network error / redirect loop → broken\n * - soft-404 → warning\n * - 403 / 429 → blocked (likely anti-bot)\n * - any other 4xx / 5xx → broken\n * - 2xx after a redirect → redirect\n * - 2xx with no redirect → ok\n * - anything else (1xx, 3xx with no 2xx) → broken\n */\nexport function classifyLink(input: ClassifyInput): LinkVerdict {\n if (input.status === 0) return \"broken\";\n if (input.loop) return \"broken\";\n if (input.softNotFound) return \"warning\";\n if (input.status === 403 || input.status === 429) return \"blocked\";\n if (input.status >= 400) return \"broken\";\n if (input.status >= 200 && input.status < 300) {\n return input.redirected ? \"redirect\" : \"ok\";\n }\n // 1xx, or a 3xx that never resolved to a 2xx.\n return \"broken\";\n}\n","/**\n * Per-URL link check.\n *\n * The make-or-break of a credible link checker is *avoiding false\n * positives*. The logic here:\n *\n * 1. Skip non-checkable schemes (`mailto:`/`tel:`/`data:`/`javascript:`).\n * 2. Try `HEAD` first (cheap); fall back to `GET` when the server\n * rejects HEAD (`405`/`501`, often `403`) or returns no useful\n * status.\n * 3. Follow redirects up to a hop cap, recording the chain. A loop or a\n * chain ending in `4xx/5xx` is `broken`; a chain ending in `2xx` is\n * `redirect` (a signal, not breakage).\n * 4. Soft-404 heuristic: a `200` whose body is short and says \"not\n * found\" → `warning`.\n * 5. Anti-bot: `403`/`429` → `blocked`, triaged separately.\n * 6. Retry once on `429`/`5xx`/network errors with a short jittered\n * backoff, honouring `Retry-After` (capped).\n * 7. Network failures (`dns`/`timeout`/`tls`/`network`) → `broken` with\n * the reason attached.\n *\n * Never throws — every path returns a shaped `LinkCheck`.\n */\n\nimport { fetchUrl, type FetchFailureReason } from \"../net/fetch-url\";\nimport { classifyLink } from \"./classify\";\nimport type { LinkCheck } from \"./types\";\n\nexport interface CheckLinkOptions {\n signal?: AbortSignal;\n /** Per-request timeout in ms. Defaults to 8_000. */\n timeoutMs?: number;\n /** Max redirect hops to follow. Defaults to 10. */\n maxRedirects?: number;\n /** Retries on 429/5xx/network. Defaults to 1. */\n retries?: number;\n allowInsecureTls?: boolean;\n userAgent?: string;\n /** Base backoff in ms (jittered). Defaults to 300. */\n backoffBaseMs?: number;\n /** Upper bound on any honoured backoff / Retry-After. Defaults to 3_000. */\n maxBackoffMs?: number;\n /** Injectable sleep (tests). Defaults to a real timer. */\n sleep?: (ms: number) => Promise<void>;\n /** Bytes of body to read for the soft-404 heuristic. Defaults to 16 KB. */\n softBodyBytes?: number;\n}\n\nconst DEFAULTS = {\n timeoutMs: 8_000,\n maxRedirects: 10,\n retries: 1,\n backoffBaseMs: 300,\n maxBackoffMs: 3_000,\n softBodyBytes: 16 * 1024,\n};\n\nconst NOT_FOUND_RE = /\\b(not\\s+found|page\\s+doesn'?t\\s+exist|404|no\\s+such\\s+page)\\b/i;\nconst SOFT_404_BODY_LIMIT = 2_048;\n\ninterface ProbeOutcome {\n status: number;\n finalUrl: string;\n redirectChain: string[];\n reason?: FetchFailureReason;\n loop: boolean;\n body?: string;\n retryAfter?: string;\n method: \"HEAD\" | \"GET\";\n}\n\n/** Probe one URL and classify the result. Never throws. */\nexport async function checkLink(url: string, options: CheckLinkOptions = {}): Promise<LinkCheck> {\n const checkedAt = new Date().toISOString();\n const started = Date.now();\n\n const scheme = schemeOf(url);\n if (scheme !== \"http\" && scheme !== \"https\") {\n return {\n url,\n finalUrl: url,\n status: 0,\n verdict: \"skipped\",\n method: \"GET\",\n redirectChain: [],\n reason: scheme ? `unsupported scheme: ${scheme}` : \"not a URL\",\n checkedAt,\n durationMs: Date.now() - started,\n };\n }\n\n const outcome = await probeWithRetry(url, options);\n const softNotFound = isSoftNotFound(outcome);\n const verdict = classifyLink({\n status: outcome.status,\n redirected: outcome.redirectChain.length > 0,\n reason: outcome.reason,\n softNotFound,\n loop: outcome.loop,\n });\n\n return {\n url,\n finalUrl: outcome.finalUrl,\n status: outcome.status,\n verdict,\n method: outcome.method,\n redirectChain: outcome.redirectChain,\n reason: describeReason(outcome, softNotFound),\n checkedAt,\n durationMs: Date.now() - started,\n };\n}\n\n/** Run a probe, retrying once (configurable) on transient failures. */\nasync function probeWithRetry(url: string, options: CheckLinkOptions): Promise<ProbeOutcome> {\n const retries = options.retries ?? DEFAULTS.retries;\n const sleep = options.sleep ?? defaultSleep;\n let outcome = await probeWithHeadFallback(url, options);\n\n for (let attempt = 0; attempt < retries; attempt++) {\n if (!isTransient(outcome)) break;\n if (options.signal?.aborted) break;\n const delay = backoffDelay(outcome, attempt, options);\n if (delay > 0) await sleep(delay);\n outcome = await probeWithHeadFallback(url, options);\n }\n\n return outcome;\n}\n\n/**\n * HEAD first; fall back to GET when the server rejects HEAD or when we\n * need a body for the soft-404 heuristic on a 200.\n */\nasync function probeWithHeadFallback(\n url: string,\n options: CheckLinkOptions,\n): Promise<ProbeOutcome> {\n const head = await followRedirects(url, \"HEAD\", options);\n if (headRejected(head) || (head.status >= 200 && head.status < 300)) {\n // HEAD was rejected, or HEAD succeeded but we want a body to rule out\n // a soft-404 — re-probe with GET.\n const get = await followRedirects(url, \"GET\", options);\n // If GET itself failed at the network level but HEAD had a real\n // status, trust HEAD (some hosts throttle GET more aggressively).\n if (get.status === 0 && head.status !== 0 && !headRejected(head)) return head;\n return get;\n }\n return head;\n}\n\n/** Whether a HEAD response indicates the server doesn't support HEAD. */\nfunction headRejected(o: ProbeOutcome): boolean {\n return o.status === 405 || o.status === 501 || o.status === 403 || o.status === 0;\n}\n\n/**\n * Follow redirects manually so we can record the chain and detect loops.\n * Returns the terminal response or the looping/over-cap state.\n */\nasync function followRedirects(\n url: string,\n method: \"HEAD\" | \"GET\",\n options: CheckLinkOptions,\n): Promise<ProbeOutcome> {\n const maxRedirects = options.maxRedirects ?? DEFAULTS.maxRedirects;\n const chain: string[] = [];\n const visited = new Set<string>([url]);\n let current = url;\n\n for (let hop = 0; hop <= maxRedirects; hop++) {\n const res = await fetchUrl(current, {\n method,\n redirect: \"manual\",\n timeoutMs: options.timeoutMs ?? DEFAULTS.timeoutMs,\n allowInsecureTls: options.allowInsecureTls,\n userAgent: options.userAgent,\n maxBytes: method === \"GET\" ? (options.softBodyBytes ?? DEFAULTS.softBodyBytes) : undefined,\n });\n\n if (res.status === 0) {\n return {\n status: 0,\n finalUrl: current,\n redirectChain: chain,\n reason: res.reason,\n loop: false,\n method,\n };\n }\n\n const next = res.redirectChain[0];\n if (next) {\n chain.push(next);\n if (visited.has(next)) {\n return { status: res.status, finalUrl: next, redirectChain: chain, loop: true, method };\n }\n visited.add(next);\n current = next;\n continue;\n }\n\n return {\n status: res.status,\n finalUrl: res.finalUrl,\n redirectChain: chain,\n loop: false,\n body: res.body,\n retryAfter: res.retryAfter,\n method,\n };\n }\n\n // Exceeded the hop cap → treat as a loop / unresolved chain.\n return { status: 0, finalUrl: current, redirectChain: chain, loop: true, method };\n}\n\nfunction isSoftNotFound(o: ProbeOutcome): boolean {\n if (o.status < 200 || o.status >= 300) return false;\n if (o.body === undefined) return false;\n // A genuinely short body that announces \"not found\" — the classic\n // CMS soft-404 that returns 200 with an error page.\n if (o.body.length > SOFT_404_BODY_LIMIT) return false;\n return NOT_FOUND_RE.test(o.body);\n}\n\nfunction isTransient(o: ProbeOutcome): boolean {\n if (o.status === 0) return true; // network error\n if (o.status === 429) return true;\n if (o.status >= 500) return true;\n return false;\n}\n\nfunction backoffDelay(o: ProbeOutcome, attempt: number, options: CheckLinkOptions): number {\n const max = options.maxBackoffMs ?? DEFAULTS.maxBackoffMs;\n const retryAfterMs = parseRetryAfter(o.retryAfter);\n if (retryAfterMs !== undefined) return Math.min(retryAfterMs, max);\n const base = options.backoffBaseMs ?? DEFAULTS.backoffBaseMs;\n const exp = base * 2 ** attempt;\n const jitter = Math.floor(Math.random() * base);\n return Math.min(exp + jitter, max);\n}\n\n/** Parse a `Retry-After` header (delta-seconds or HTTP-date) into ms. */\nfunction parseRetryAfter(value: string | undefined): number | undefined {\n if (!value) return undefined;\n const seconds = Number(value);\n if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);\n const date = Date.parse(value);\n if (!Number.isNaN(date)) return Math.max(0, date - Date.now());\n return undefined;\n}\n\nfunction describeReason(o: ProbeOutcome, softNotFound: boolean): string | undefined {\n if (o.reason) return o.reason;\n if (o.loop) return \"redirect loop\";\n if (softNotFound) return \"soft-404\";\n if (o.status === 429) return \"429 rate-limited\";\n if (o.status === 403) return \"403 forbidden\";\n if (o.status >= 400) return `${o.status}`;\n return undefined;\n}\n\nfunction schemeOf(url: string): string | null {\n try {\n return new URL(url).protocol.replace(/:$/, \"\").toLowerCase();\n } catch {\n return null;\n }\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * Link-checker data model.\n *\n * The link engine answers \"do all the links on the site actually\n * resolve?\". It shares the suite's discovery pass (`discoverSitemap`)\n * for the page list, then runs its own light scan + probe pipeline over\n * that set.\n *\n * Like the rest of `src/lib/core/**`, everything here is plain,\n * JSON-serializable data with no Next.js / React / DOM coupling so it\n * can ship as part of `@goflag/core`.\n */\n\n/** Whether a link points back at the audited origin or off-site. */\nexport type LinkKind = \"internal\" | \"external\";\n\n/** Which HTML element a link was extracted from. */\nexport type LinkSource = \"a\" | \"img\" | \"script\" | \"link\" | \"iframe\";\n\n/**\n * A single link occurrence as authored on a page, with the metadata the\n * report needs. Distinct from `discover.ts`'s crawl-scoped\n * `CandidateLink` (same-origin, href-only) — the checker keeps every\n * link, internal and external, with its source element and attributes.\n */\nexport interface LinkRef {\n /** The href/src exactly as authored. */\n rawHref: string;\n /** Canonical absolute URL (fragment stripped) used as the dedupe key. */\n url: string;\n /** Internal vs external relative to the audited origin. */\n kind: LinkKind;\n /** Which element produced the link. */\n source: LinkSource;\n /** Parsed `rel` tokens (lower-cased), e.g. [\"nofollow\",\"sponsored\"]. */\n rel: string[];\n /** Trimmed anchor text for `<a>` elements, when present. */\n anchorText?: string;\n /** The `#fragment` (including the hash) when authored, kept separately. */\n fragment?: string;\n}\n\n/** The outcome of probing a single URL. */\nexport type LinkVerdict =\n | \"ok\" // 2xx\n | \"redirect\" // 3xx resolving to 2xx\n | \"broken\" // 4xx / 5xx / network failure\n | \"blocked\" // 403 / 429 — probably anti-bot, triage manually\n | \"warning\" // soft-404 / suspicious\n | \"skipped\"; // non-http scheme\n\n/** The result of checking one unique URL. */\nexport interface LinkCheck {\n /** The canonical URL that was checked. */\n url: string;\n /** The URL after following redirects (== url when none). */\n finalUrl: string;\n /** Final HTTP status. `0` = network error. */\n status: number;\n /** Classified verdict. */\n verdict: LinkVerdict;\n /** The method that produced the final status. */\n method: \"HEAD\" | \"GET\";\n /** Intermediate redirect hops (resolved absolute), in order. */\n redirectChain: string[];\n /** Human-readable reason (network reason, \"soft-404\", \"429 rate-limited\", …). */\n reason?: string;\n /** ISO timestamp of the check. */\n checkedAt: string;\n /** Wall-clock duration of the check in ms. */\n durationMs: number;\n}\n\n/** A (page → link) pair, used to map checks back to where they appear. */\nexport interface LinkOccurrence {\n pageUrl: string;\n ref: LinkRef;\n}\n\n/** Progress callback payload, emitted as the audit advances. */\nexport interface AuditProgress {\n phase: \"scan\" | \"check\";\n /** Items completed in the current phase. */\n done: number;\n /** Total items in the current phase (best-effort; grows during scan). */\n total: number;\n}\n\n/** The complete result of a link audit, cached per origin. */\nexport interface LinkAuditReport {\n /** Origin the report is keyed by (e.g. `https://example.com`). */\n origin: string;\n /** Base URL the audit started from. */\n baseUrl: string;\n /** Number of pages whose HTML was successfully scanned. */\n pagesScanned: number;\n /** Every (page → link) pair discovered during the scan. */\n occurrences: LinkOccurrence[];\n /** Per unique canonical URL — each URL is checked exactly once. */\n checks: Record<string, LinkCheck>;\n /** Count of checks per verdict. */\n summary: Record<LinkVerdict, number>;\n /** Report view: broken/blocked/warning links grouped by the page they appear on. */\n brokenByPage: Array<{ pageUrl: string; broken: LinkCheck[] }>;\n /** True when a hard cap (`maxPages` / `maxLinks`) stopped collection. */\n truncated: boolean;\n /** Audit-level diagnostics. */\n diagnostics: { pagesFailed: number; warnings: string[] };\n}\n\n/** Empty verdict tally with every key present (so the UI can map over it). */\nexport function emptyVerdictSummary(): Record<LinkVerdict, number> {\n return { ok: 0, redirect: 0, broken: 0, blocked: 0, warning: 0, skipped: 0 };\n}\n","/**\n * Link-audit orchestrator.\n *\n * Takes the suite's shared `SiteDiscovery` (the page list) and runs a\n * two-phase pipeline:\n *\n * 1. **Scan** — fetch each page's static HTML (capped `maxBytes`) and\n * extract its links with `extractLinks`. A page fetch failure is\n * recorded in `diagnostics.pagesFailed`; it never aborts the audit.\n * 2. **Check** — globally de-duplicate the link targets (the footer\n * link on 500 pages is probed *once*) and `checkLink` each unique\n * URL, bounded by a global concurrency cap *and* a per-host cap so a\n * run can't get the IP banned.\n *\n * Then it joins checks back to occurrences to build `brokenByPage` and a\n * per-verdict summary. Every loop is bounded (`maxPages`, `maxLinks`,\n * concurrency, timeouts) and the whole thing never throws.\n */\n\nimport type { SiteDiscovery } from \"../sitemap/types\";\nimport { fetchUrl } from \"../net/fetch-url\";\nimport { extractLinks } from \"./extract\";\nimport { checkLink } from \"./check\";\nimport {\n emptyVerdictSummary,\n type AuditProgress,\n type LinkAuditReport,\n type LinkCheck,\n type LinkOccurrence,\n} from \"./types\";\n\nexport interface LinkAuditOptions {\n signal?: AbortSignal;\n /** Parallel page HTML fetches. Defaults to 4. */\n scanConcurrency?: number;\n /** Parallel link probes (global). Defaults to 8. */\n checkConcurrency?: number;\n /** Parallel link probes per host (politeness). Defaults to 3. */\n maxPerHost?: number;\n /** Per-request timeout in ms. Defaults to 8_000. */\n timeoutMs?: number;\n /** Hard cap on pages scanned. Defaults to 500. */\n maxPages?: number;\n /** Hard cap on unique URLs checked. Defaults to 10_000. */\n maxLinks?: number;\n /** Include `<img>/<script>/<link>/<iframe>` sources. Defaults to false. */\n includeAssets?: boolean;\n /** Probe external (off-origin) links. Defaults to true. */\n checkExternal?: boolean;\n /** Allow self-signed / invalid TLS. */\n allowInsecureTls?: boolean;\n userAgent?: string;\n /** Bytes of page HTML to read during the scan. Defaults to 3 MB. */\n maxScanBytes?: number;\n /** Injectable sleep forwarded to `checkLink` (tests). */\n sleep?: (ms: number) => Promise<void>;\n /** Progress callback fired after each scanned page and each checked link. */\n onProgress?: (p: AuditProgress) => void;\n}\n\nconst DEFAULTS = {\n scanConcurrency: 4,\n checkConcurrency: 8,\n maxPerHost: 3,\n timeoutMs: 8_000,\n maxPages: 500,\n maxLinks: 10_000,\n maxScanBytes: 3 * 1024 * 1024,\n};\n\nexport async function runLinkAudit(\n discovery: SiteDiscovery,\n options: LinkAuditOptions = {},\n): Promise<LinkAuditReport> {\n const maxPages = options.maxPages ?? DEFAULTS.maxPages;\n const maxLinks = options.maxLinks ?? DEFAULTS.maxLinks;\n const checkExternal = options.checkExternal !== false;\n\n const warnings: string[] = [];\n let truncated = false;\n\n // --- Page set -----------------------------------------------------------\n const allPages = discovery.urls.map((u) => u.loc);\n const pageUrls = allPages.slice(0, maxPages);\n if (allPages.length > maxPages) {\n truncated = true;\n warnings.push(`Site has ${allPages.length} pages; only the first ${maxPages} were scanned.`);\n }\n\n // --- Scan phase ---------------------------------------------------------\n const occurrences: LinkOccurrence[] = [];\n let pagesScanned = 0;\n let pagesFailed = 0;\n let scanDone = 0;\n\n await mapWithHostCaps(\n pageUrls,\n {\n concurrency: options.scanConcurrency ?? DEFAULTS.scanConcurrency,\n perHost: Infinity,\n signal: options.signal,\n },\n async (pageUrl) => {\n const res = await fetchUrl(pageUrl, {\n method: \"GET\",\n timeoutMs: options.timeoutMs ?? DEFAULTS.timeoutMs,\n allowInsecureTls: options.allowInsecureTls,\n userAgent: options.userAgent,\n maxBytes: options.maxScanBytes ?? DEFAULTS.maxScanBytes,\n signal: options.signal,\n });\n if (res.status === 0 || res.status >= 400 || res.body === undefined) {\n pagesFailed += 1;\n } else {\n pagesScanned += 1;\n const refs = extractLinks(res.body, {\n baseUrl: res.finalUrl,\n includeAssets: options.includeAssets,\n });\n for (const ref of refs) occurrences.push({ pageUrl, ref });\n }\n scanDone += 1;\n options.onProgress?.({ phase: \"scan\", done: scanDone, total: pageUrls.length });\n },\n () => \"scan\",\n );\n\n // --- Global dedupe ------------------------------------------------------\n const uniqueUrls: string[] = [];\n const seen = new Set<string>();\n for (const occ of occurrences) {\n const url = occ.ref.url;\n if (seen.has(url)) continue;\n if (!checkExternal && occ.ref.kind === \"external\") continue;\n if (uniqueUrls.length >= maxLinks) {\n truncated = true;\n warnings.push(`More than ${maxLinks} unique links found; the remainder were not checked.`);\n break;\n }\n seen.add(url);\n uniqueUrls.push(url);\n }\n\n // --- Check phase --------------------------------------------------------\n const checks: Record<string, LinkCheck> = {};\n let checkDone = 0;\n\n await mapWithHostCaps(\n uniqueUrls,\n {\n concurrency: options.checkConcurrency ?? DEFAULTS.checkConcurrency,\n perHost: options.maxPerHost ?? DEFAULTS.maxPerHost,\n signal: options.signal,\n },\n async (url) => {\n const check = await checkLink(url, {\n timeoutMs: options.timeoutMs ?? DEFAULTS.timeoutMs,\n allowInsecureTls: options.allowInsecureTls,\n userAgent: options.userAgent,\n signal: options.signal,\n sleep: options.sleep,\n });\n checks[url] = check;\n checkDone += 1;\n options.onProgress?.({ phase: \"check\", done: checkDone, total: uniqueUrls.length });\n },\n (url) => hostOf(url),\n );\n\n // --- Map back + summarise ----------------------------------------------\n const summary = emptyVerdictSummary();\n for (const check of Object.values(checks)) summary[check.verdict] += 1;\n\n const brokenByPage = buildBrokenByPage(occurrences, checks);\n\n return {\n origin: discovery.origin,\n baseUrl: discovery.baseUrl,\n pagesScanned,\n occurrences,\n checks,\n summary,\n brokenByPage,\n truncated,\n diagnostics: { pagesFailed, warnings },\n };\n}\n\n/** Group broken/blocked/warning checks by the page they appear on. */\nfunction buildBrokenByPage(\n occurrences: LinkOccurrence[],\n checks: Record<string, LinkCheck>,\n): LinkAuditReport[\"brokenByPage\"] {\n const byPage = new Map<string, Map<string, LinkCheck>>();\n for (const occ of occurrences) {\n const check = checks[occ.ref.url];\n if (!check) continue;\n if (check.verdict === \"ok\" || check.verdict === \"redirect\" || check.verdict === \"skipped\") {\n continue;\n }\n let page = byPage.get(occ.pageUrl);\n if (!page) {\n page = new Map();\n byPage.set(occ.pageUrl, page);\n }\n page.set(check.url, check);\n }\n return Array.from(byPage.entries())\n .map(([pageUrl, map]) => ({ pageUrl, broken: Array.from(map.values()) }))\n .sort((a, b) => b.broken.length - a.broken.length);\n}\n\nfunction hostOf(url: string): string {\n try {\n return new URL(url).host;\n } catch {\n return url;\n }\n}\n\ninterface PoolOptions {\n concurrency: number;\n perHost: number;\n signal?: AbortSignal;\n}\n\n/**\n * Run `worker` over `items` with a global concurrency cap and an optional\n * per-host cap. Resolves when every item completes (or the signal aborts\n * and in-flight work drains). Worker rejections are swallowed — each\n * worker is responsible for collapsing its own failures.\n */\nfunction mapWithHostCaps<T>(\n items: T[],\n opts: PoolOptions,\n worker: (item: T) => Promise<void>,\n hostOf: (item: T) => string,\n): Promise<void> {\n const concurrency = Math.max(1, opts.concurrency);\n const perHost = opts.perHost;\n return new Promise<void>((resolve) => {\n if (items.length === 0) {\n resolve();\n return;\n }\n const pending = items.map((item) => ({ item, host: hostOf(item) }));\n const hostActive = new Map<string, number>();\n let inflight = 0;\n let settled = false;\n\n const finish = () => {\n if (!settled) {\n settled = true;\n resolve();\n }\n };\n\n const pump = () => {\n if (settled) return;\n if (opts.signal?.aborted) {\n if (inflight === 0) finish();\n return;\n }\n while (inflight < concurrency) {\n let pickIndex = -1;\n for (let k = 0; k < pending.length; k++) {\n const entry = pending[k]!;\n if ((hostActive.get(entry.host) ?? 0) < perHost) {\n pickIndex = k;\n break;\n }\n }\n if (pickIndex === -1) break;\n const [picked] = pending.splice(pickIndex, 1);\n const host = picked!.host;\n hostActive.set(host, (hostActive.get(host) ?? 0) + 1);\n inflight += 1;\n void worker(picked!.item)\n .catch(() => undefined)\n .finally(() => {\n inflight -= 1;\n hostActive.set(host, (hostActive.get(host) ?? 1) - 1);\n if (pending.length === 0 && inflight === 0) finish();\n else pump();\n });\n }\n if (pending.length === 0 && inflight === 0) finish();\n };\n\n pump();\n });\n}\n","/**\n * Result of normalizing a user-supplied URL string. Either a usable absolute\n * http(s) URL, or a flag that the input could not be coerced into one.\n */\nexport type NormalizeUrlResult = { ok: true; url: string } | { ok: false };\n\n// Matches a leading `scheme://` (e.g. `http://`, `https://`, `ftp://`). Used to\n// tell \"the user already typed a scheme\" apart from a bare host like\n// `tancrede.eu` or `localhost:3000`.\nconst SCHEME_PATTERN = /^([a-zA-Z][a-zA-Z0-9+.-]*):\\/\\//;\n\n/**\n * Coerce a user-typed location into an absolute http(s) URL.\n *\n * People rarely type the scheme — they paste `tancrede.eu` or\n * `example.com/blog`. We treat a missing scheme as `https://` so the form\n * \"just works\", while still rejecting genuinely unusable input (empty\n * strings, non-http(s) schemes, or anything the URL parser can't make sense\n * of). The returned URL preserves the path/query the user typed; we only\n * prepend the scheme when one is absent.\n */\nexport function normalizeInputUrl(input: string): NormalizeUrlResult {\n const trimmed = input.trim();\n if (!trimmed) return { ok: false };\n\n const schemeMatch = trimmed.match(SCHEME_PATTERN);\n let candidate: string;\n if (schemeMatch) {\n const scheme = (schemeMatch[1] ?? \"\").toLowerCase();\n // A scheme was supplied explicitly — only http/https are inspectable.\n if (scheme !== \"http\" && scheme !== \"https\") return { ok: false };\n candidate = trimmed;\n } else {\n // No scheme: assume https. This is the whole point — `tancrede.eu`\n // becomes `https://tancrede.eu`.\n candidate = `https://${trimmed}`;\n }\n\n let parsed: URL;\n try {\n parsed = new URL(candidate);\n } catch {\n return { ok: false };\n }\n\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") return { ok: false };\n if (!parsed.hostname) return { ok: false };\n\n return { ok: true, url: candidate };\n}\n","/**\n * The conformance view (plan §10) — every rule's status on every page, not\n * just the violations.\n *\n * `seoIssues` answers \"what is wrong here\". That is the right default for a\n * human fixing a site, and the wrong shape for the question the plan calls\n * point #2: *where do we stand against the catalog?* A list of violations\n * cannot distinguish \"this rule passes everywhere\" from \"this rule never\n * applied to a single page\" — and an agent deciding what to work on needs\n * exactly that distinction.\n *\n * So this is the same evaluation, unfiltered: a rule × page matrix of\n * `pass` / `fail` / `warn` / `na`, plus per-rule totals. Opt-in, because on\n * a 200-page crawl it is the largest thing in the report and nobody reading\n * a terminal asked for it.\n *\n * Shape note: rule metadata (rigor, sources, expected) is carried once in\n * `rules`, not repeated per cell. A 200-page × 11-rule matrix with inline\n * metadata is ~2,200 copies of the same eight fields; the legend + status\n * grid says the same thing at a fraction of the size, which matters when\n * the consumer is a context window.\n */\n\nimport type { RuleFinding } from \"../lib/rules\";\nimport type { Rigor, Rule } from \"../lib/rules/types\";\n\n/** A rule's verdict on one page. Mirrors `RuleFinding[\"status\"]`. */\nexport type ConformanceStatus = RuleFinding[\"status\"];\n\n/** Per-rule legend entry and tally across every scanned page. */\nexport interface ConformanceRule {\n ruleId: string;\n kind: Rule[\"kind\"];\n title: string;\n rigor: Rigor;\n sources: string[];\n /** One sentence stating what a passing page looks like. */\n expected: string;\n /**\n * How the rule landed across the run. `crashed` counts pages where the\n * evaluator threw, so the five buckets always sum to the number of pages\n * — a matrix whose arithmetic does not close is worse than no matrix.\n */\n totals: { pass: number; fail: number; warn: number; na: number; crashed: number };\n}\n\n/** One page's row of the matrix. Rules that crashed on it are absent. */\nexport interface ConformancePage {\n url: string;\n /** Rule id → status. Keyed, not positional, so it survives rule reordering. */\n statuses: Record<string, ConformanceStatus>;\n}\n\nexport interface ConformanceView {\n /** Legend + totals, in registry order. */\n rules: ConformanceRule[];\n /**\n * One row per **linted** page, in crawl order — which is a smaller set than\n * `report.pages`: unreachable pages, non-HTML resources, and pages the site\n * itself declared duplicate via canonical never reach the rule layer. Read\n * `pages.length` for the denominator; every rule's totals sum to it.\n */\n pages: ConformancePage[];\n}\n\n/** What `runAudit` collects per page to build the view. */\nexport interface ConformanceRow {\n pageUrl: string;\n findings: ReadonlyArray<RuleFinding>;\n}\n\n/**\n * Fold per-page evaluations into the matrix. `rules` must be the effective\n * (profile-applied) rule set the findings came from — a rule the profile\n * switched off is absent from the view entirely rather than sitting there\n * with five zeroes, because \"not run\" and \"never applied\" are different\n * claims and only one of them is `na`.\n */\nexport function buildConformance(\n rules: ReadonlyArray<Rule>,\n rows: ReadonlyArray<ConformanceRow>,\n): ConformanceView {\n const totals = new Map<string, ConformanceRule[\"totals\"]>(\n rules.map((rule) => [rule.id, { pass: 0, fail: 0, warn: 0, na: 0, crashed: 0 }]),\n );\n\n const pages: ConformancePage[] = rows.map((row) => {\n const statuses: Record<string, ConformanceStatus> = {};\n const seen = new Set<string>();\n for (const finding of row.findings) {\n const tally = totals.get(finding.ruleId);\n if (!tally) continue; // A finding from a rule outside this set: not ours.\n statuses[finding.ruleId] = finding.status;\n tally[finding.status] += 1;\n seen.add(finding.ruleId);\n }\n // No finding for a rule on this page means its evaluator threw; the\n // crash itself is already reported as an `engine.rule-crashed` issue.\n for (const rule of rules) if (!seen.has(rule.id)) totals.get(rule.id)!.crashed += 1;\n return { url: row.pageUrl, statuses };\n });\n\n return {\n rules: rules.map((rule) => ({\n ruleId: rule.id,\n kind: rule.kind,\n title: rule.title,\n rigor: rule.rigor,\n sources: rule.sources,\n expected: rule.expected,\n totals: totals.get(rule.id)!,\n })),\n pages,\n };\n}\n","/**\n * Report orchestrator.\n *\n * One crawl feeds all three checks:\n * - the crawled `Page[]` drives SEO lint + the i18n matrix, and\n * - a synthetic `SiteDiscovery` built from the same page set drives the\n * link audit.\n *\n * Everything funnels into a single `GoflagReport` (see `./types.ts`).\n */\n\nimport { crawl } from \"../lib/core/crawl\";\nimport { matchesAny } from \"../lib/core/glob\";\nimport { sortIssues } from \"../lib/core/lint\";\nimport { lintSite } from \"../lib/core/lint-site\";\nimport {\n buildI18nMatrix,\n looksLikeLocaleSegment,\n reciprocityIssues,\n type I18nMatrix,\n} from \"../lib/core/i18n\";\nimport { deriveLocaleAxis, suggestedLocales } from \"../lib/core/locales\";\nimport { discoverSitemap } from \"../lib/core/sitemap/discover\";\nimport { selectByStructure } from \"../lib/core/coverage\";\nimport { probeRobots } from \"../lib/core/probes/robots\";\nimport { collectAdvisories } from \"../lib/rules/advisory\";\nimport { evaluateRules, findingsToIssues } from \"../lib/rules/evaluate\";\nimport { extractionFromPage } from \"../lib/rules/extraction/from-page\";\nimport { DEFAULT_PROFILE, rulesForProfile } from \"../lib/rules/profiles\";\nimport { PROSE_RULES } from \"../lib/rules/prose\";\nimport { getSiteRule } from \"../lib/rules/site-rules\";\nimport type { SiteContext } from \"../lib/rules/site-types\";\nimport { runLinkAudit } from \"../lib/core/links/audit\";\nimport { normalizeInputUrl } from \"../lib/core/net/normalize-url\";\nimport type { SiteDiscovery } from \"../lib/core/sitemap/types\";\nimport type { Page } from \"../lib/core/types\";\nimport { buildConformance, type ConformanceRow } from \"./conformance\";\nimport { fingerprint, routeKey, targetKey } from \"./fingerprint\";\nimport type {\n BrokenLink,\n GoflagReport,\n ReportAdvisory,\n ReportReciprocityIssue,\n SeoIssue,\n SiteIssue,\n TranslationHole,\n UnreachablePage,\n Verdict,\n} from \"./types\";\n\n/** A 2xx response is the only \"healthy\" page we lint / audit for i18n. */\nfunction isOkStatus(status: number): boolean {\n return status >= 200 && status < 300;\n}\n\n/**\n * Content types whose `<head>` is worth judging.\n *\n * A crawl follows links, and links point at PDFs, images and feeds as readily\n * as at pages. Linting those produces a full set of phantom findings — a PDF\n * has no `<title>`, no canonical, no viewport — and on tancrede.eu a single\n * linked CV was the only `error`-severity finding in the run, which is the\n * difference between a red and a yellow CI gate.\n *\n * An empty content type is treated as HTML: some servers omit it, and being\n * silent about a real page is worse than one stray finding.\n */\nfunction isHtmlPage(page: Page): boolean {\n const type = page.fetch.contentType?.trim().toLowerCase();\n if (!type) return true;\n return type === \"text/html\" || type === \"application/xhtml+xml\";\n}\n\n/**\n * The phase of the audit a progress event belongs to:\n * - \"crawl\": BFS-visiting + inspecting pages (SEO + i18n input).\n * - \"scan\": re-reading each page's HTML to extract its links.\n * - \"links\": probing each unique link target.\n */\nexport type AuditPhase = \"crawl\" | \"scan\" | \"links\";\n\n/** A single progress tick, emitted as the audit advances. */\nexport interface ProgressEvent {\n phase: AuditPhase;\n /** Items completed in the current phase. */\n done: number;\n /** Best-effort total for the phase (the crawl total grows as links are discovered). */\n total: number;\n /** The URL just processed, when the phase tracks one (crawl). */\n url?: string;\n /** HTTP status of `url`, when known (crawl). */\n status?: number;\n}\n\nexport interface AuditOptions {\n /** BFS crawl depth. `0` = entry only. Defaults to 2. */\n depth?: number;\n /** Hard cap on pages crawled. Defaults to 200. */\n maxPages?: number;\n /**\n * How to choose which pages to audit.\n *\n * `structural` keeps every page that stands alone and samples families of\n * pages built from one template — `docs/coverage-plan.md`. `all` is the old\n * behaviour: take what the sitemap lists in order, until `maxPages`.\n *\n * Defaults to `structural` when a sitemap was found, `all` otherwise: with\n * no list of URLs up front there is no structure to select from.\n */\n coverage?: \"structural\" | \"all\";\n /** Glob include filter on the URL pathname. */\n include?: string[];\n /** Glob exclude filter on the URL pathname. */\n exclude?: string[];\n /** Probe external (off-origin) links too. Defaults to true. */\n checkExternal?: boolean;\n /** Skip Chromium: static HTML only (no SPA rendering). Defaults to false. */\n static?: boolean;\n /** Allow self-signed / invalid TLS (localhost, tunnels). */\n allowInsecureTls?: boolean;\n /** Per-request timeout in ms. Defaults to 8_000. */\n timeoutMs?: number;\n /** Caller-driven cancellation. */\n signal?: AbortSignal;\n /** Progress callback for a live CLI. */\n onProgress?: (event: ProgressEvent) => void;\n /**\n * Locales the site serves, when the operator knows better than we can infer\n * (`--locales fr,en,pt-br`). Strongest input to the locale axis.\n */\n locales?: string[];\n /**\n * Routes (locale-free, glob-matched) that are deliberately not translated\n * everywhere, so their gaps are not reported as missing translations.\n * Suppression is counted in `diagnostics.ignoredHoles` — a silenced finding\n * that leaves no trace reads as \"nothing was wrong\".\n */\n ignoreHoles?: string[];\n /**\n * Skip sitemap discovery and seed the crawl from `<url>` alone. Defaults to\n * false — discovery is on by default because crawl-only discovery is exactly\n * what made hreflang checks silently vacuous.\n */\n noSitemap?: boolean;\n /**\n * Named policy overlay applied to the rule registry (`--profile`), e.g.\n * `strict` or `spec-only`. Defaults to `default` (no overlay). Throws on\n * an unknown name rather than auditing under a policy nobody asked for.\n */\n profile?: string;\n /**\n * Emit the rule × page conformance matrix (`--conformance`): every rule's\n * status on every page, passing ones included. Off by default because it\n * is the largest section of the report and only an agent (or a coverage\n * question) wants it.\n */\n conformance?: boolean;\n /**\n * Emit prose rules with their evidence bundles (`--advisories`). These\n * carry no verdict and never affect the gate.\n */\n advisories?: boolean;\n}\n\n/** Infer a locale from the leading path segment, or null when unprefixed. */\nfunction localeOfUrl(url: string): string | null {\n try {\n const { pathname } = new URL(url);\n const first = pathname.split(\"/\").filter(Boolean)[0];\n if (first && looksLikeLocaleSegment(first)) return first;\n } catch {\n // fall through\n }\n return null;\n}\n\n/**\n * Routes present in some locales but absent in others. A \"hole\" is exactly\n * the \"missing translation page\" the tool exists to find. `x-default` is a\n * fallback pointer, not a real translation, so it never counts as a locale\n * that must be filled.\n *\n * `ignoreRoutes` globs out routes the operator has declared intentionally\n * partial. A site has no markup for \"this page does not exist here on\n * purpose\", so a deliberate gap and a forgotten one are indistinguishable from\n * outside — only the operator can tell them apart. Without a way to say so, the\n * report can never reach zero on a site with a jurisdiction-specific legal\n * page, and a report that can never be clean teaches its reader to ignore it.\n *\n * Globs match the locale-free route (`/legal`, `/blog/**`), using the same\n * matcher as `--include`/`--exclude`.\n */\nexport function deriveTranslationHoles(\n matrix: I18nMatrix,\n ignoreRoutes: readonly string[] = [],\n): TranslationHole[] {\n const realLocales = matrix.locales.filter((l) => l !== \"x-default\");\n if (realLocales.length < 2) return [];\n\n const holes: TranslationHole[] = [];\n for (const route of matrix.routes) {\n if (ignoreRoutes.length > 0 && matchesAny(route, [...ignoreRoutes])) continue;\n const present: string[] = [];\n const missing: string[] = [];\n for (const locale of realLocales) {\n const cell = matrix.cells[route]?.[locale];\n if (cell && cell.url) present.push(locale);\n else missing.push(locale);\n }\n if (present.length > 0 && missing.length > 0) {\n holes.push({\n id: fingerprint(\"i18n\", \"hole\", route),\n route,\n presentLocales: present,\n missingLocales: missing,\n });\n }\n }\n return holes;\n}\n\n/**\n * The severity at or above which findings should fail the process.\n *\n * `warning` is the historical behaviour (any finding fails). `error` lets a\n * team adopt goflag on a site that is not clean yet: warnings stay visible in\n * the report while only hard errors block a merge. `never` reports without\n * ever failing, for exploratory runs.\n */\nexport type FailOn = \"warning\" | \"error\" | \"never\";\n\n/**\n * Map a report to a process exit code:\n * 0 = clean (or below the failure threshold), 1 = findings at/above it.\n * Used by the CLI as a CI gate.\n *\n * `red` always fails unless `never`: a red verdict also covers broken links,\n * unreachable pages and a blind link scan — states where \"no findings\" would\n * be a lie rather than a pass.\n */\nexport function exitCode(report: GoflagReport, failOn: FailOn = \"warning\"): number {\n if (failOn === \"never\") return 0;\n if (report.summary.verdict === \"green\") return 0;\n if (failOn === \"error\") return report.summary.verdict === \"red\" ? 1 : 0;\n return 1;\n}\n\n/**\n * Drop pages the site itself says are duplicates.\n *\n * A cross-URL `<link rel=\"canonical\">` is an explicit statement: \"index that\n * one instead of me.\" Linting the variant anyway judges a page the site has\n * already disclaimed, and multiplies every finding by however many variants\n * exist. On stereo.house, 14 of 41 crawled pages were `?tag=` filters of one\n * library page — all correctly declaring the same canonical — and they carried\n * 14 of the 38 SEO findings. Better than a third of the report was one page,\n * counted fourteen times.\n *\n * The guard matters: a variant is only dropped when its canonical target was\n * actually crawled. Otherwise the canonical points somewhere we never looked,\n * and dropping the variant would remove the route from the audit entirely —\n * trading duplicate findings for no findings, which is the worse failure.\n *\n * Variants stay in the crawl either way, so the link audit still probes them.\n */\nfunction dropCanonicalDuplicates(pages: Page[]): { kept: Page[]; dropped: number } {\n const crawled = new Set(pages.map((p) => routeKey(p.fetch.finalUrl)));\n\n const kept = pages.filter((page) => {\n const canonical = page.links.canonical;\n if (!canonical) return true;\n\n const self = routeKey(page.fetch.finalUrl);\n let target: string;\n try {\n target = routeKey(new URL(canonical, page.fetch.finalUrl).toString());\n } catch {\n return true;\n }\n\n if (target === self) return true;\n return !crawled.has(target);\n });\n\n return { kept, dropped: pages.length - kept.length };\n}\n\n/** Run the full audit and return the report. Never throws for site-level failures. */\nexport async function runAudit(\n inputUrl: string,\n options: AuditOptions = {},\n): Promise<GoflagReport> {\n const normalized = normalizeInputUrl(inputUrl);\n if (!normalized.ok) {\n throw new Error(`\"${inputUrl}\" is not a valid http(s) URL.`);\n }\n const entry = normalized.url;\n const origin = new URL(entry).origin;\n\n const depth = options.depth ?? 2;\n const maxPages = options.maxPages ?? 200;\n const checkExternal = options.checkExternal !== false;\n\n // Resolved before the crawl, and once for the whole run: every page is\n // judged under the same policy, and an unknown profile name fails here\n // rather than after a full crawl — or, worse, quietly under `default`.\n const profile = options.profile ?? DEFAULT_PROFILE;\n const rules = rulesForProfile(profile);\n\n // --- Sitemap discovery (independent of the markup we are about to judge) --\n //\n // Runs *before* the crawl so its URLs can seed the frontier. `crawlFallback`\n // is off: the crawl below is the fallback, and running a second one here\n // would double the work for nothing.\n const discovery = options.noSitemap\n ? undefined\n : await discoverSitemap(entry, {\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n allowInsecureTls: options.allowInsecureTls,\n crawlFallback: false,\n }).catch(() => undefined);\n\n const allSitemapUrls = (discovery?.urls ?? []).map((u) => u.loc);\n\n // Which pages to audit, chosen by the shape of the site rather than by the\n // order the crawl happens to reach them. Only possible with a sitemap: with\n // no list of URLs up front there is no structure to select from, so the\n // fallback is the old behaviour and says so.\n const coverageMode = options.coverage ?? (allSitemapUrls.length > 0 ? \"structural\" : \"all\");\n const selection =\n coverageMode === \"structural\" && allSitemapUrls.length > 0\n ? selectByStructure(allSitemapUrls, { locales: options.locales })\n : undefined;\n const sitemapUrls = selection?.urls ?? allSitemapUrls;\n\n // A selection is already the answer to \"how many\", so the 200-page default\n // must not cut it short — that would reintroduce the arbitrary truncation it\n // exists to replace.\n //\n // The headroom is not decoration. The crawl also fetches the entry URL and\n // whatever it redirects to, and those are pages the selection never named: a\n // cap set to the selection size exactly let the entry consume a slot and\n // pushed a real page out. Measured on tancrede, which lost\n // `/es/privacy-policy` to an off-by-one nobody would have noticed in a\n // report that still looked complete.\n //\n // Keeping the default as a floor also preserves what a small site had\n // before: room to follow a link to a page the sitemap forgot, which is a\n // finding rather than noise.\n const effectiveMaxPages = selection\n ? Math.max(options.maxPages ?? maxPages, selection.urls.length + 5)\n : maxPages;\n\n // Fetched independently of sitemap discovery: `--no-sitemap` must not also\n // blind the robots rules, and one small request is cheaper than the class of\n // bug a site-wide `Disallow: /` represents.\n const robots = await probeRobots(origin, {\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n }).catch(() => undefined);\n\n // --- Crawl (drives SEO lint + i18n) ------------------------------------\n const crawlResult = await crawl({\n entryUrl: entry,\n seedUrls: sitemapUrls,\n depth,\n maxPages: effectiveMaxPages,\n include: options.include,\n exclude: options.exclude,\n inspectOptions: {\n mode: options.static ? \"static\" : \"auto\",\n probes: false,\n allowInsecureTls: options.allowInsecureTls,\n timeoutMs: options.timeoutMs,\n signal: options.signal,\n },\n onPage: (page, ctx) =>\n options.onProgress?.({\n phase: \"crawl\",\n done: ctx.visited,\n total: ctx.visited + ctx.queued,\n url: page.fetch.finalUrl,\n status: page.fetch.status,\n }),\n });\n\n const pages: Page[] = crawlResult.pages;\n const warnings = crawlResult.errors.map((e) => `crawl: ${e.url} — ${e.message}`);\n\n // A page that returned a non-2xx status has no meaningful <head>, links,\n // or hreflang — linting it produces phantom \"missing title/description/…\"\n // findings. Split the healthy pages out and report the rest as errored.\n const okPages = pages.filter((p) => isOkStatus(p.fetch.status));\n // Linked resources stay in the crawl (the link audit must still probe them)\n // but never reach the rule layer, which only speaks about HTML documents.\n const documents = okPages.filter(isHtmlPage);\n const { kept: htmlPages, dropped: duplicatePages } = dropCanonicalDuplicates(documents);\n // Two ways a page fails to be audited, and both belong here.\n //\n // A non-2xx answer is the obvious one. The other is a page that never\n // answered at all — a timeout, a reset, a DNS failure — which `crawl` puts in\n // `errors` rather than in `pages`. That one used to leave nothing behind but\n // a warning line, and warnings do not gate: the run stayed green, the report\n // looked complete, and the page silently left the audited set. Its slot in\n // the crawl budget then went to whatever the frontier offered next, so the\n // page *set* changed too.\n //\n // That is what poisons a baseline. Captured with four timeouts, a baseline\n // holds four pages the next run will not have and misses four it will,\n // and `--regressions-only` reports the difference as regressions on a branch\n // that touched nothing. Measured on openfinanceguide 2026-08-09: four STET\n // endpoint pages timed out at capture, and the merge request after it failed\n // on one \"new\" finding that had been on that template all along.\n //\n // `status: 0` is what the schema already reserved for a network error. This\n // is the first thing to produce it.\n const unreachablePages: UnreachablePage[] = [\n ...pages\n .filter((p) => !isOkStatus(p.fetch.status))\n .map((p) => ({\n id: fingerprint(\"page\", routeKey(p.fetch.finalUrl)),\n url: p.fetch.finalUrl,\n status: p.fetch.status,\n })),\n ...crawlResult.errors.map((e) => ({\n id: fingerprint(\"page\", routeKey(e.url)),\n url: e.url,\n status: 0,\n })),\n ];\n\n // --- SEO lint (healthy pages only) -------------------------------------\n //\n // Each page is projected onto the extraction and evaluated exactly once,\n // whatever the caller asked for: the violations list, the conformance\n // matrix and the advisory bundles are three narrowings of that single\n // evaluation, not three passes over the page.\n const seoIssues: SeoIssue[] = [];\n const conformanceRows: ConformanceRow[] = [];\n const advisories: ReportAdvisory[] = [];\n // Resolved from the *effective* rules, not the global registry: one issue\n // must not read its severity from one rule set and its `why` from another.\n const ruleById = new Map(rules.map((rule) => [rule.id, rule]));\n for (const page of htmlPages) {\n const pageUrl = page.fetch.finalUrl;\n const extraction = extractionFromPage(page);\n const evaluation = evaluateRules(extraction, rules);\n if (options.conformance) {\n conformanceRows.push({ pageUrl, findings: evaluation.findings });\n }\n if (options.advisories) {\n for (const advisory of collectAdvisories(extraction, PROSE_RULES)) {\n advisories.push({\n ...advisory,\n id: fingerprint(\"advisory\", advisory.ruleId, routeKey(pageUrl)),\n pageUrl,\n });\n }\n }\n // The occurrence index keeps fingerprints distinct if a rule ever fires\n // more than once on a page, without depending on the (mutable) message.\n const occurrence = new Map<string, number>();\n for (const issue of sortIssues(findingsToIssues(evaluation, rules))) {\n const n = occurrence.get(issue.ruleId) ?? 0;\n occurrence.set(issue.ruleId, n + 1);\n const rule = ruleById.get(issue.ruleId);\n seoIssues.push({\n id: fingerprint(\"seo\", issue.ruleId, routeKey(pageUrl), String(n)),\n pageUrl,\n ruleId: issue.ruleId,\n severity: issue.severity,\n message: issue.message,\n why: rule?.title,\n fix: issue.fix?.snippet,\n });\n }\n }\n\n // --- i18n (healthy pages only) -----------------------------------------\n //\n // The locale axis is unioned from three independent sources so that a site\n // declaring no `hreflang` at all can still be recognised as multilingual —\n // the failure the crawl-only axis could not see. Sitemap URLs are also fed\n // to the matrix as declared-but-uncrawled cells, so a route missing from a\n // locale reads as a hole rather than as absence of evidence.\n const localeAxis = deriveLocaleAxis({\n explicit: options.locales,\n sitemapUrls,\n pages: htmlPages,\n });\n\n // With no declared axis we do not invent one — but staying silent about it\n // would look identical to \"this site is fine\", which is the failure mode\n // phase 1 existed to remove. Say what we saw and what to pass instead.\n if (localeAxis.source === \"none\" && localeAxis.candidates.length > 0) {\n const suggestion = suggestedLocales(localeAxis);\n warnings.push(\n suggestion\n ? `No sitemap and no --locales: i18n checks are off. Locale-looking prefixes found — re-run with \\`--locales ${suggestion}\\` to enable them.`\n : `No sitemap and no --locales: i18n checks are off. Prefixes seen (${localeAxis.candidates\n .map((c) => c.tag)\n .join(\", \")}) do not look like locales; pass --locales if any of them is one.`,\n );\n }\n\n const matrix = buildI18nMatrix(htmlPages, {\n declaredUrls: sitemapUrls,\n locales: localeAxis.locales,\n });\n // Holes are a claim about locale coverage. With no declared axis we have\n // just refused to claim the site is multilingual, so claiming it is missing\n // translations would contradict that — and it is exactly how `/cv` (a CV\n // page served in French) produced 31 phantom holes on tancrede.eu.\n const holes = localeAxis.multilingual ? deriveTranslationHoles(matrix, options.ignoreHoles) : [];\n // Count what the exclusion hid, so the number is auditable rather than a\n // silent shrink between two runs.\n const ignoredHoles =\n localeAxis.multilingual && (options.ignoreHoles?.length ?? 0) > 0\n ? deriveTranslationHoles(matrix).length - holes.length\n : 0;\n const reciprocity: ReportReciprocityIssue[] = reciprocityIssues(htmlPages).map((issue) => ({\n id: fingerprint(\n \"i18n\",\n issue.code,\n routeKey(issue.url),\n issue.peerUrl ? routeKey(issue.peerUrl) : \"\",\n issue.locale ?? \"\",\n ),\n ...issue,\n }));\n\n // --- Cross-page rules ---------------------------------------------------\n const siteContext: SiteContext = {\n origin,\n pages: htmlPages,\n matrix,\n localeAxis,\n discovery,\n robots,\n };\n // Fingerprints key on (rule, page, occurrence-within-that-pair) rather than\n // a global index, so adding or reordering a rule cannot renumber unrelated\n // findings and invalidate a stored baseline.\n const siteOccurrence = new Map<string, number>();\n const siteIssues: SiteIssue[] = lintSite(siteContext).map((draft) => {\n const key = `${draft.ruleId}\\u0000${draft.pageUrl}`;\n const n = siteOccurrence.get(key) ?? 0;\n siteOccurrence.set(key, n + 1);\n const rule = getSiteRule(draft.ruleId);\n return {\n id: fingerprint(\"site\", draft.ruleId, routeKey(draft.pageUrl), String(n)),\n pageUrl: draft.pageUrl,\n ruleId: draft.ruleId,\n severity: draft.severity,\n message: draft.message,\n why: rule?.summary,\n fix: draft.fix?.snippet,\n };\n });\n\n // --- Link audit over the healthy crawled page set ----------------------\n //\n // `effectiveMaxPages`, not `maxPages`. The crawl was told the selection is\n // the answer to \"how many\"; this pass was still being told 200, so a\n // structural run crawled and linted 748 pages and scanned links on the first\n // 200 of them. Nothing said so usefully either — the report carried\n // `truncated: true` and \"Site has 753 pages; only the first 200 were\n // scanned\" while listing 748, which reads as a display bug rather than as\n // the coverage claim it was. `0 broken links` then meant 200 pages, on a\n // report that said 748.\n const linkDiscovery = syntheticDiscovery(origin, entry, okPages, crawlResult.truncated);\n const linkReport = await runLinkAudit(linkDiscovery, {\n allowInsecureTls: options.allowInsecureTls,\n checkExternal,\n timeoutMs: options.timeoutMs,\n maxPages: effectiveMaxPages,\n signal: options.signal,\n onProgress: (p) =>\n options.onProgress?.({\n phase: p.phase === \"scan\" ? \"scan\" : \"links\",\n done: p.done,\n total: p.total,\n }),\n });\n warnings.push(...linkReport.diagnostics.warnings);\n\n const brokenLinks: BrokenLink[] = [];\n for (const { pageUrl, broken } of linkReport.brokenByPage) {\n for (const check of broken) {\n brokenLinks.push({\n id: fingerprint(\"link\", routeKey(pageUrl), targetKey(check.url)),\n pageUrl,\n href: check.url,\n status: check.status,\n verdict: check.verdict,\n reason: check.reason,\n });\n }\n }\n\n // A total (or near-total) scan failure means \"0 broken links\" is not a\n // clean bill of health — it's \"we couldn't check\". Say so loudly instead\n // of letting the run look green.\n const scanTargets = okPages.length;\n const scanFailed = linkReport.diagnostics.pagesFailed;\n const scanBlind = scanTargets > 0 && linkReport.pagesScanned === 0;\n if (scanBlind) {\n warnings.push(\n `Link scan failed on all ${scanFailed} page(s) — \"0 broken links\" is unverified, not clean.`,\n );\n } else if (scanFailed > 0 && scanFailed >= scanTargets / 2) {\n warnings.push(\n `Link scan failed on ${scanFailed} of ${scanTargets} page(s); link results are partial.`,\n );\n }\n\n // --- Summary + verdict -------------------------------------------------\n const brokenCount = linkReport.summary.broken;\n const errorCount =\n seoIssues.filter((i) => i.severity === \"error\").length +\n siteIssues.filter((i) => i.severity === \"error\").length;\n const missingTranslations = holes.length + reciprocity.length;\n const anyFinding =\n missingTranslations > 0 ||\n seoIssues.length > 0 ||\n siteIssues.length > 0 ||\n brokenLinks.length > 0;\n\n // Reaching nothing at all is not a clean bill of health — it is a failed\n // audit wearing one. Without this, an unreachable host, a DNS blip or a\n // mid-run network drop reports GREEN with zero findings, which is the most\n // dangerous output the tool can produce.\n const crawlBlind = pages.length === 0;\n if (crawlBlind) {\n warnings.push(\n `Crawled 0 pages from ${entry} — the audit reached nothing, so \"no findings\" is unverified, not clean.`,\n );\n }\n\n // A page in your own crawl that errors, or a scan that saw nothing, is a\n // hard failure — never green.\n let verdict: Verdict = \"green\";\n if (crawlBlind || brokenCount > 0 || errorCount > 0 || unreachablePages.length > 0 || scanBlind) {\n verdict = \"red\";\n } else if (anyFinding) {\n verdict = \"yellow\";\n }\n\n return {\n url: entry,\n finishedAt: new Date().toISOString(),\n profile,\n summary: {\n brokenLinks: brokenCount,\n missingTranslations,\n seoIssues: seoIssues.length,\n siteIssues: siteIssues.length,\n unreachablePages: unreachablePages.length,\n verdict,\n },\n localeAxis: {\n locales: localeAxis.locales,\n source: localeAxis.source,\n multilingual: localeAxis.multilingual,\n ...(localeAxis.candidates.length > 0 ? { candidates: localeAxis.candidates } : {}),\n },\n pages: pages.map((p) => ({\n url: p.fetch.finalUrl,\n status: p.fetch.status,\n locale: localeOfUrl(p.fetch.finalUrl),\n })),\n unreachablePages,\n brokenLinks,\n missingTranslations: { holes, reciprocity },\n seoIssues,\n siteIssues,\n // Both sections are omitted entirely when not asked for, rather than\n // emitted empty: an empty `advisories: []` would read as \"no prose rule\n // has anything to ask about this site\", which is a claim goflag has not\n // made.\n ...(options.conformance ? { conformance: buildConformance(rules, conformanceRows) } : {}),\n ...(options.advisories ? { advisories } : {}),\n diagnostics: {\n pagesCrawled: pages.length,\n pagesScanned: linkReport.pagesScanned,\n pagesFailed: linkReport.diagnostics.pagesFailed,\n truncated: crawlResult.truncated || linkReport.truncated,\n warnings,\n // What the run actually looked at. A sampled audit that does not say so\n // is a partial audit wearing the face of a complete one, which is worse\n // than the cap it replaces.\n coverage: {\n mode: coverageMode,\n ...(selection\n ? {\n considered: selection.total,\n selected: selection.urls.length,\n families: selection.families,\n }\n : {}),\n },\n ...(ignoredHoles > 0 ? { ignoredHoles } : {}),\n ...(duplicatePages > 0 ? { duplicatePages } : {}),\n ...(discovery\n ? {\n sitemap: {\n found: discovery.diagnostics.found,\n sitemapUrl: discovery.diagnostics.sitemapUrl,\n urlCount: allSitemapUrls.length,\n uncrawled: countUncrawled(sitemapUrls, pages),\n ...(discovery.diagnostics.unreachable\n ? { unreachable: discovery.diagnostics.unreachable }\n : {}),\n },\n }\n : {}),\n },\n };\n}\n\n/**\n * How many sitemap URLs the crawl never reached — usually because `maxPages`\n * capped it. Reported so \"no findings on those routes\" is never mistaken for\n * \"those routes are fine\".\n */\nfunction countUncrawled(sitemapUrls: string[], pages: Page[]): number {\n const crawled = new Set(pages.map((p) => p.fetch.finalUrl));\n let n = 0;\n for (const url of sitemapUrls) if (!crawled.has(url)) n += 1;\n return n;\n}\n\n/** Build a `SiteDiscovery` from the crawl result so the link audit can reuse the page set. */\nfunction syntheticDiscovery(\n origin: string,\n baseUrl: string,\n pages: Page[],\n truncated: boolean,\n): SiteDiscovery {\n return {\n origin,\n baseUrl,\n source: \"crawl\",\n urls: pages.map((p) => ({ loc: p.fetch.finalUrl })),\n diagnostics: {\n found: false,\n status: 0,\n declaredInRobots: false,\n robotsFound: false,\n atWellKnownPath: false,\n wellFormed: false,\n isIndex: false,\n childSitemapCount: 0,\n childSitemapErrors: 0,\n urlCount: pages.length,\n warnings: [],\n },\n truncated,\n };\n}\n","/**\n * Render a `GoflagReport` as a human-readable terminal report.\n *\n * Pure function of the report — no I/O. Colour is disabled automatically\n * when stdout is not a TTY or `NO_COLOR` is set.\n */\n\nimport type { Severity } from \"../lib/core/types\";\nimport { DEFAULT_PROFILE } from \"../lib/rules/profiles\";\nimport type { GoflagReport, Verdict } from \"./types\";\n\ninterface RenderOptions {\n color?: boolean;\n}\n\nconst ANSI = {\n reset: \"\\x1b[0m\",\n bold: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n cyan: \"\\x1b[36m\",\n};\n\nfunction paint(enabled: boolean) {\n return (code: string, text: string) => (enabled ? `${code}${text}${ANSI.reset}` : text);\n}\n\nconst VERDICT_FLAG: Record<Verdict, string> = {\n green: \"green flag\",\n yellow: \"yellow flag\",\n red: \"red flag\",\n};\n\nfunction verdictColor(verdict: Verdict): string {\n return verdict === \"green\" ? ANSI.green : verdict === \"yellow\" ? ANSI.yellow : ANSI.red;\n}\n\nexport function renderTerminal(report: GoflagReport, options: RenderOptions = {}): string {\n const color = options.color ?? false;\n const c = paint(color);\n const lines: string[] = [];\n\n lines.push(\"\");\n lines.push(`${c(ANSI.bold, \"goflag\")} ${c(ANSI.dim, report.url)}`);\n lines.push(\n `${c(verdictColor(report.summary.verdict), VERDICT_FLAG[report.summary.verdict].toUpperCase())} ` +\n c(\n ANSI.dim,\n `${report.diagnostics.pagesCrawled} pages crawled, ${report.diagnostics.pagesScanned} scanned` +\n // Named only when it is not the default. A run under `spec-only`\n // that prints \"0 SEO issues\" without saying so reads as a clean\n // site rather than a narrowed one.\n (report.profile === DEFAULT_PROFILE ? \"\" : `, profile ${report.profile}`),\n ),\n );\n // --- Coverage ----------------------------------------------------------\n //\n // Printed whenever the run sampled, and only then. A sampled audit that says\n // nothing is a partial audit wearing the face of a complete one: the summary\n // below counts findings on 760 pages of 4451 and reads exactly like a\n // summary of the whole site.\n const coverage = report.diagnostics.coverage;\n if (coverage?.families?.length && coverage.considered && coverage.selected) {\n const biggest = coverage.families[0]!;\n\n // Counted from what the run actually crawled, not from what it selected.\n // `--max-pages` can still cut the selection short, and a line that reported\n // the intent would be the exact untruth it exists to prevent.\n const audited = report.diagnostics.pagesCrawled;\n const capped = audited < coverage.selected ? `, capped by --max-pages` : \"\";\n\n lines.push(\n c(\n ANSI.dim,\n `COVERAGE ${audited} of ${coverage.considered} pages audited${capped} · ` +\n `${coverage.families.length} families sampled, largest ` +\n `${biggest.sampled}/${biggest.size} ${biggest.pattern}`,\n ),\n );\n lines.push(\n c(\n ANSI.dim,\n ` Template rules are conclusive. Copy rules — title.length, ` +\n `description.length — are sampled.`,\n ),\n );\n }\n\n lines.push(\"\");\n\n // --- Summary line ------------------------------------------------------\n lines.push(\n [\n count(c, report.summary.brokenLinks, \"broken link\", report.summary.brokenLinks > 0),\n count(\n c,\n report.summary.missingTranslations,\n \"missing translation\",\n report.summary.missingTranslations > 0,\n ),\n count(c, report.summary.seoIssues, \"SEO issue\", report.summary.seoIssues > 0),\n count(c, report.summary.siteIssues, \"site issue\", report.summary.siteIssues > 0),\n count(\n c,\n report.summary.unreachablePages,\n \"unreachable page\",\n report.summary.unreachablePages > 0,\n ),\n ].join(\" \"),\n );\n lines.push(\"\");\n\n // --- Unreachable pages -------------------------------------------------\n if (report.unreachablePages.length > 0) {\n lines.push(c(ANSI.bold, \"Unreachable pages\"));\n for (const page of report.unreachablePages) {\n lines.push(` ${c(ANSI.red, `[${page.status || \"network error\"}]`)} ${page.url}`);\n }\n lines.push(\"\");\n }\n\n // --- Broken links ------------------------------------------------------\n if (report.brokenLinks.length > 0) {\n lines.push(c(ANSI.bold, \"Broken links\"));\n const byPage = groupBy(report.brokenLinks, (b) => b.pageUrl);\n for (const [pageUrl, links] of byPage) {\n lines.push(` ${c(ANSI.cyan, pageUrl)}`);\n for (const link of links) {\n const tag =\n link.verdict === \"broken\"\n ? c(ANSI.red, `[${link.status || link.reason || \"broken\"}]`)\n : c(ANSI.yellow, `[${link.verdict}${link.reason ? ` ${link.reason}` : \"\"}]`);\n lines.push(` ${tag} ${link.href}`);\n }\n }\n lines.push(\"\");\n }\n\n // --- Missing translations ---------------------------------------------\n const { holes, reciprocity } = report.missingTranslations;\n if (holes.length > 0 || reciprocity.length > 0) {\n lines.push(c(ANSI.bold, \"Missing translations\"));\n for (const hole of holes) {\n lines.push(\n ` ${c(ANSI.cyan, hole.route)} — missing ${c(ANSI.yellow, hole.missingLocales.join(\", \"))}` +\n c(ANSI.dim, ` (have ${hole.presentLocales.join(\", \")})`),\n );\n }\n for (const issue of reciprocity) {\n lines.push(` ${c(ANSI.yellow, issue.code)} ${c(ANSI.dim, issue.url)}`);\n lines.push(` ${issue.message}`);\n }\n lines.push(\"\");\n }\n\n // --- SEO issues --------------------------------------------------------\n if (report.seoIssues.length > 0) {\n lines.push(c(ANSI.bold, \"SEO issues\"));\n lines.push(...renderIssuesByPage(report.seoIssues, c));\n lines.push(\"\");\n }\n\n // --- Cross-page issues -------------------------------------------------\n // Rendered separately from `seoIssues` because the distinction is\n // actionable, not cosmetic: these findings are statements about the site as\n // a whole, so fixing one usually fixes the whole column at once.\n if (report.siteIssues.length > 0) {\n lines.push(c(ANSI.bold, \"Site-wide issues\"));\n lines.push(\n ` ${c(ANSI.dim, `locales: ${report.localeAxis.locales.join(\", \") || \"none\"} (via ${report.localeAxis.source})`)}`,\n );\n lines.push(...renderIssuesByPage(report.siteIssues, c));\n lines.push(\"\");\n }\n\n // --- Conformance -------------------------------------------------------\n // Opt-in, and rendered as per-rule totals rather than the full matrix: a\n // 200-row grid is unreadable in a terminal, and the column that actually\n // answers \"where do we stand\" is the tally. The matrix itself is in --json.\n if (report.conformance) {\n lines.push(c(ANSI.bold, \"Conformance\"));\n const judged = report.conformance.pages.length;\n lines.push(c(ANSI.dim, ` ${judged} page${judged === 1 ? \"\" : \"s\"} judged × every rule`));\n for (const rule of report.conformance.rules) {\n const { pass, fail, warn, na, crashed } = rule.totals;\n const tally = [\n fail > 0 ? c(ANSI.red, `${fail} fail`) : null,\n warn > 0 ? c(ANSI.yellow, `${warn} warn`) : null,\n pass > 0 ? c(ANSI.green, `${pass} pass`) : null,\n na > 0 ? c(ANSI.dim, `${na} n/a`) : null,\n crashed > 0 ? c(ANSI.red, `${crashed} crashed`) : null,\n ].filter(Boolean);\n lines.push(\n ` ${c(ANSI.cyan, rule.ruleId.padEnd(24))} ${tally.join(c(ANSI.dim, \" · \"))}` +\n c(ANSI.dim, ` [${rule.rigor}]`),\n );\n }\n lines.push(\"\");\n }\n\n // --- Advisories --------------------------------------------------------\n // Questions, not findings: listed once per rule with the pages they apply\n // to, and never counted in the summary line above. The evidence bundle an\n // agent needs is in --json; printing it here would bury the findings that\n // a human can actually act on.\n if (report.advisories && report.advisories.length > 0) {\n lines.push(c(ANSI.bold, \"Needs judgment\"));\n lines.push(c(ANSI.dim, \" goflag states these; it will not guess the answer.\"));\n for (const [ruleId, group] of groupBy([...report.advisories], (a) => a.ruleId)) {\n lines.push(\n ` ${c(ANSI.cyan, ruleId)} ${c(ANSI.dim, `(${group.length} page${group.length === 1 ? \"\" : \"s\"})`)}`,\n );\n lines.push(` ${group[0]!.prose}`);\n }\n lines.push(\"\");\n }\n\n if (report.summary.verdict === \"green\") {\n lines.push(c(ANSI.green, \"No problems found.\"));\n lines.push(\"\");\n }\n\n for (const warning of report.diagnostics.warnings) {\n lines.push(c(ANSI.dim, `note: ${warning}`));\n }\n if (report.diagnostics.truncated) {\n lines.push(c(ANSI.dim, \"note: results truncated by a page/link cap.\"));\n }\n\n return lines.join(\"\\n\");\n}\n\nfunction count(\n c: (code: string, t: string) => string,\n n: number,\n noun: string,\n bad: boolean,\n): string {\n const label = `${n} ${noun}${n === 1 ? \"\" : \"s\"}`;\n return bad ? c(ANSI.bold, c(ANSI.yellow, label)) : c(ANSI.dim, label);\n}\n\n/**\n * Group findings under the page they belong to. Shared by the per-page and\n * cross-page sections: both emit the same `{ pageUrl, ruleId, severity,\n * message }` shape, and a reader scanning the terminal wants them to look\n * alike so the eye can skim one column of severities.\n */\nfunction renderIssuesByPage(\n issues: ReadonlyArray<{\n pageUrl: string;\n ruleId: string;\n severity: Severity;\n message: string;\n }>,\n c: (code: string, t: string) => string,\n): string[] {\n const lines: string[] = [];\n for (const [pageUrl, group] of groupBy([...issues], (i) => i.pageUrl)) {\n lines.push(` ${c(ANSI.cyan, pageUrl)}`);\n for (const issue of group) {\n const sev =\n issue.severity === \"error\"\n ? c(ANSI.red, \"error\")\n : issue.severity === \"warning\"\n ? c(ANSI.yellow, \"warn \")\n : c(ANSI.dim, \"info \");\n lines.push(` ${sev} ${c(ANSI.dim, issue.ruleId)} ${stripBackticks(issue.message)}`);\n }\n }\n return lines;\n}\n\nfunction groupBy<T>(items: T[], key: (item: T) => string): Map<string, T[]> {\n const map = new Map<string, T[]>();\n for (const item of items) {\n const k = key(item);\n const arr = map.get(k);\n if (arr) arr.push(item);\n else map.set(k, [item]);\n }\n return map;\n}\n\nfunction stripBackticks(s: string): string {\n return s.replace(/`/g, \"\");\n}\n","/**\n * Render a `GoflagSummary` as a compact, deduplicated terminal report.\n *\n * Where `renderTerminal` lists every finding grouped by page, this lists each\n * distinct problem once with a count and a small sample of affected pages —\n * the view you want for a large crawl or a quick \"is it green?\" check. Pure\n * function of the summary; no I/O.\n */\n\nimport { DEFAULT_PROFILE } from \"../lib/rules/profiles\";\nimport type { GoflagSummary, RollupSeo } from \"./summarize\";\nimport type { Verdict } from \"./types\";\n\ninterface RenderOptions {\n color?: boolean;\n}\n\nconst ANSI = {\n reset: \"\\x1b[0m\",\n bold: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n cyan: \"\\x1b[36m\",\n};\n\nfunction paint(enabled: boolean) {\n return (code: string, text: string) => (enabled ? `${code}${text}${ANSI.reset}` : text);\n}\n\nconst VERDICT_FLAG: Record<Verdict, string> = {\n green: \"GREEN FLAG\",\n yellow: \"YELLOW FLAG\",\n red: \"RED FLAG\",\n};\n\nfunction verdictColor(verdict: Verdict): string {\n return verdict === \"green\" ? ANSI.green : verdict === \"yellow\" ? ANSI.yellow : ANSI.red;\n}\n\nexport function renderSummaryTerminal(summary: GoflagSummary, options: RenderOptions = {}): string {\n const c = paint(options.color ?? false);\n const lines: string[] = [];\n\n lines.push(\"\");\n lines.push(`${c(ANSI.bold, \"goflag\")} ${c(ANSI.dim, summary.url)} ${c(ANSI.dim, \"(summary)\")}`);\n lines.push(\n `${c(verdictColor(summary.verdict), VERDICT_FLAG[summary.verdict])} ` +\n c(\n ANSI.dim,\n `${summary.totals.pagesCrawled} pages crawled, ${summary.totals.pagesScanned} scanned` +\n // Same reasoning as `renderTerminal`: a narrowed run must say so,\n // or its low finding count reads as a clean site.\n (summary.profile === DEFAULT_PROFILE ? \"\" : `, profile ${summary.profile}`),\n ),\n );\n lines.push(\"\");\n lines.push(\n [\n metric(c, summary.totals.brokenLinks, \"broken link\"),\n metric(c, summary.totals.missingTranslations, \"missing translation\"),\n metric(c, summary.totals.seoIssues, \"SEO issue\"),\n metric(c, summary.totals.siteIssues, \"site issue\"),\n metric(c, summary.totals.unreachablePages, \"unreachable page\"),\n ].join(\" \"),\n );\n lines.push(\"\");\n\n // --- Unreachable pages -------------------------------------------------\n if (summary.unreachablePages.length > 0) {\n lines.push(c(ANSI.bold, \"Unreachable pages\"));\n for (const page of summary.unreachablePages) {\n lines.push(` ${c(ANSI.red, `[${page.status || \"network error\"}]`)} ${page.url}`);\n }\n lines.push(\"\");\n }\n\n // --- Broken links (one row per target) --------------------------------\n if (summary.brokenLinks.length > 0) {\n lines.push(c(ANSI.bold, \"Broken links\"));\n for (const link of summary.brokenLinks) {\n const tag =\n link.verdict === \"broken\"\n ? c(ANSI.red, `[${link.status || link.reason || \"broken\"}]`)\n : c(ANSI.yellow, `[${link.verdict}${link.reason ? ` ${link.reason}` : \"\"}]`);\n lines.push(` ${tag} ${link.href} ${c(ANSI.dim, `×${link.count}`)}`);\n lines.push(` ${c(ANSI.dim, `on ${samplePages(link.pages, link.morePages)}`)}`);\n }\n lines.push(\"\");\n }\n\n // --- Missing translations ---------------------------------------------\n const { holes, reciprocity } = summary.translations;\n if (holes.length > 0 || reciprocity.length > 0) {\n lines.push(c(ANSI.bold, \"Missing translations\"));\n for (const hole of holes) {\n lines.push(\n ` ${c(ANSI.cyan, hole.route)} — missing ${c(ANSI.yellow, hole.missingLocales.join(\", \"))}` +\n c(ANSI.dim, ` (have ${hole.presentLocales.join(\", \")})`),\n );\n }\n for (const issue of reciprocity) {\n lines.push(` ${c(ANSI.yellow, issue.code)} ${c(ANSI.dim, `×${issue.count}`)}`);\n lines.push(` ${stripBackticks(issue.sample)}`);\n lines.push(` ${c(ANSI.dim, `on ${samplePages(issue.pages, issue.morePages)}`)}`);\n }\n lines.push(\"\");\n }\n\n // --- SEO issues (one row per rule, with why/fix) ----------------------\n if (summary.seoIssues.length > 0) {\n lines.push(c(ANSI.bold, \"SEO issues\"));\n lines.push(...rollupRows(summary.seoIssues, c));\n lines.push(\"\");\n }\n\n // --- Cross-page issues (same rollup shape) -----------------------------\n if (summary.siteIssues.length > 0) {\n lines.push(c(ANSI.bold, \"Site-wide issues\"));\n lines.push(...rollupRows(summary.siteIssues, c));\n lines.push(\"\");\n }\n\n // --- Conformance (totals only; the matrix lives in the full report) ----\n if (summary.conformance) {\n lines.push(c(ANSI.bold, \"Conformance\"));\n for (const rule of summary.conformance.rules) {\n const { pass, fail, warn, na, crashed } = rule.totals;\n const tally = [\n fail > 0 ? c(ANSI.red, `${fail} fail`) : null,\n warn > 0 ? c(ANSI.yellow, `${warn} warn`) : null,\n pass > 0 ? c(ANSI.green, `${pass} pass`) : null,\n na > 0 ? c(ANSI.dim, `${na} n/a`) : null,\n crashed > 0 ? c(ANSI.red, `${crashed} crashed`) : null,\n ].filter(Boolean);\n lines.push(` ${c(ANSI.cyan, rule.ruleId.padEnd(24))} ${tally.join(c(ANSI.dim, \" · \"))}`);\n }\n lines.push(\"\");\n }\n\n // --- Advisories (one row per rule; evidence stays in the JSON) ---------\n if (summary.advisories && summary.advisories.length > 0) {\n lines.push(c(ANSI.bold, \"Needs judgment\"));\n const byRule = new Map<string, { prose: string; pages: number }>();\n for (const advisory of summary.advisories) {\n const entry = byRule.get(advisory.ruleId);\n if (entry) entry.pages += 1;\n else byRule.set(advisory.ruleId, { prose: advisory.prose, pages: 1 });\n }\n for (const [ruleId, { prose, pages }] of byRule) {\n lines.push(\n ` ${c(ANSI.cyan, ruleId)} ${c(ANSI.dim, `(${pages} page${pages === 1 ? \"\" : \"s\"})`)}`,\n );\n lines.push(` ${prose}`);\n }\n lines.push(\"\");\n }\n\n if (summary.verdict === \"green\") {\n lines.push(c(ANSI.green, \"No problems found.\"));\n lines.push(\"\");\n }\n\n for (const warning of summary.warnings) {\n lines.push(c(ANSI.dim, `note: ${warning}`));\n }\n if (summary.truncated) {\n lines.push(c(ANSI.dim, \"note: results truncated by a page/link cap.\"));\n }\n\n return lines.join(\"\\n\");\n}\n\nfunction metric(c: (code: string, t: string) => string, n: number, noun: string): string {\n const label = `${n} ${noun}${n === 1 ? \"\" : \"s\"}`;\n return n > 0 ? c(ANSI.bold, c(ANSI.yellow, label)) : c(ANSI.dim, label);\n}\n\nfunction samplePages(pages: string[], more: number): string {\n const shown = pages.join(\", \");\n return more > 0 ? `${shown} (+${more} more)` : shown;\n}\n\nfunction stripBackticks(s: string): string {\n return s.replace(/`/g, \"\");\n}\n\n/**\n * One block per rolled-up rule. Shared by the SEO and site-wide sections:\n * both are `RollupSeo`, and rendering them identically keeps the fix snippet\n * in the same place regardless of which registry produced the finding.\n */\nfunction rollupRows(\n issues: ReadonlyArray<RollupSeo>,\n c: (code: string, t: string) => string,\n): string[] {\n const lines: string[] = [];\n for (const issue of issues) {\n const sev =\n issue.severity === \"error\"\n ? c(ANSI.red, \"error\")\n : issue.severity === \"warning\"\n ? c(ANSI.yellow, \"warn \")\n : c(ANSI.dim, \"info \");\n lines.push(` ${sev} ${c(ANSI.cyan, issue.ruleId)} ${c(ANSI.dim, `×${issue.count}`)}`);\n if (issue.why) lines.push(` ${c(ANSI.dim, stripBackticks(issue.why))}`);\n if (issue.fix) {\n // Multi-line snippets (the Next.js fixes) must stay readable, so indent\n // every line rather than dumping a `\\n`-laden string on one row.\n const [head, ...rest] = issue.fix.split(\"\\n\");\n lines.push(` ${c(ANSI.dim, \"fix:\")} ${head}`);\n for (const line of rest) lines.push(` ${c(ANSI.dim, line)}`);\n }\n lines.push(` ${c(ANSI.dim, `on ${samplePages(issue.pages, issue.morePages)}`)}`);\n }\n return lines;\n}\n","/**\n * Roll a full `GoflagReport` up into a compact `GoflagSummary`.\n *\n * The full report lists every finding on every page — great for a baseline\n * file or a diff, but noisy for a human skim and expensive for an LLM agent\n * to read. The summary deduplicates:\n *\n * - broken links by target (one entry per broken URL, with the pages that\n * link to it),\n * - SEO issues by rule (one entry per rule, carrying its `why`/`fix` once,\n * with a sample of affected pages),\n * - reciprocity findings by code.\n *\n * It is a pure function of the report — `goflag <url> --summary` and\n * `--summary --json` both derive their output from this, so the rollup logic\n * lives in exactly one place.\n */\n\nimport type { ReciprocityCode } from \"../lib/core/i18n\";\nimport type { LinkVerdict } from \"../lib/core/links/types\";\nimport type { Severity } from \"../lib/core/types\";\nimport type { ConformanceRule } from \"./conformance\";\nimport type {\n GoflagReport,\n ReportAdvisory,\n SeoIssue,\n SiteIssue,\n TranslationHole,\n UnreachablePage,\n Verdict,\n} from \"./types\";\n\n/** How many example pages/URLs a rolled-up entry keeps before it says \"+N more\". */\nexport const SAMPLE_LIMIT = 5;\n\n/** One broken target, with every page that links to it collapsed together. */\nexport interface RollupLink {\n href: string;\n verdict: LinkVerdict;\n status: number;\n reason?: string;\n /** How many (page → link) references share this target + verdict. */\n count: number;\n /** Up to {@link SAMPLE_LIMIT} pages that reference it. */\n pages: string[];\n /** References beyond the sample. */\n morePages: number;\n}\n\n/** One SEO rule, collapsed across every page it fired on. */\nexport interface RollupSeo {\n ruleId: string;\n severity: Severity;\n why?: string;\n fix?: string;\n /** A representative message (from the first occurrence). */\n sample: string;\n count: number;\n pages: string[];\n morePages: number;\n}\n\n/** One reciprocity code, collapsed across every page it fired on. */\nexport interface RollupReciprocity {\n code: ReciprocityCode;\n count: number;\n sample: string;\n pages: string[];\n morePages: number;\n}\n\nexport interface GoflagSummary {\n url: string;\n finishedAt: string;\n /** The rule profile the run was judged under. See `GoflagReport.profile`. */\n profile: string;\n verdict: Verdict;\n totals: {\n brokenLinks: number;\n missingTranslations: number;\n seoIssues: number;\n siteIssues: number;\n unreachablePages: number;\n pagesCrawled: number;\n pagesScanned: number;\n pagesFailed: number;\n };\n unreachablePages: UnreachablePage[];\n brokenLinks: RollupLink[];\n translations: {\n holes: TranslationHole[];\n reciprocity: RollupReciprocity[];\n };\n seoIssues: RollupSeo[];\n /** Cross-page findings, rolled up by rule exactly like `seoIssues`. */\n siteIssues: RollupSeo[];\n /**\n * Per-rule conformance totals, when `--conformance` was asked for. The\n * per-page matrix is dropped here on purpose — it is precisely the\n * page-by-page repetition summary mode exists to collapse, and the totals\n * answer the same question. Use the full report for the grid.\n */\n conformance?: { rules: ConformanceRule[] };\n /**\n * Prose rules needing judgment, when `--advisories` was asked for, carried\n * verbatim. Not rolled up: each page's evidence bundle is different, and\n * collapsing them would throw away the only thing that makes an advisory\n * answerable.\n */\n advisories?: ReportAdvisory[];\n truncated: boolean;\n warnings: string[];\n}\n\nexport function summarize(report: GoflagReport): GoflagSummary {\n return {\n url: report.url,\n finishedAt: report.finishedAt,\n profile: report.profile,\n verdict: report.summary.verdict,\n totals: {\n brokenLinks: report.summary.brokenLinks,\n missingTranslations: report.summary.missingTranslations,\n seoIssues: report.summary.seoIssues,\n siteIssues: report.summary.siteIssues,\n unreachablePages: report.summary.unreachablePages,\n pagesCrawled: report.diagnostics.pagesCrawled,\n pagesScanned: report.diagnostics.pagesScanned,\n pagesFailed: report.diagnostics.pagesFailed,\n },\n unreachablePages: report.unreachablePages,\n brokenLinks: rollupLinks(report),\n translations: {\n holes: report.missingTranslations.holes,\n reciprocity: rollupReciprocity(report),\n },\n seoIssues: rollupByRule(report.seoIssues),\n siteIssues: rollupByRule(report.siteIssues),\n ...(report.conformance ? { conformance: { rules: report.conformance.rules } } : {}),\n ...(report.advisories ? { advisories: report.advisories } : {}),\n truncated: report.diagnostics.truncated,\n warnings: report.diagnostics.warnings,\n };\n}\n\nfunction rollupLinks(report: GoflagReport): RollupLink[] {\n const groups = new Map<string, RollupLink>();\n for (const link of report.brokenLinks) {\n const key = `${link.href}\\u0000${link.verdict}`;\n const existing = groups.get(key);\n if (existing) {\n existing.count += 1;\n if (existing.pages.length < SAMPLE_LIMIT) existing.pages.push(link.pageUrl);\n else existing.morePages += 1;\n } else {\n groups.set(key, {\n href: link.href,\n verdict: link.verdict,\n status: link.status,\n reason: link.reason,\n count: 1,\n pages: [link.pageUrl],\n morePages: 0,\n });\n }\n }\n return [...groups.values()].sort((a, b) => b.count - a.count || a.href.localeCompare(b.href));\n}\n\n/**\n * Collapse per-page findings by rule id. Shared by `seoIssues` and\n * `siteIssues`: both registries emit the same finding shape, and a reader\n * skimming a summary cares about \"which policy fired, how often\" regardless\n * of which registry evaluated it.\n */\nfunction rollupByRule(issues: ReadonlyArray<SeoIssue | SiteIssue>): RollupSeo[] {\n const groups = new Map<string, RollupSeo>();\n for (const issue of issues) {\n const existing = groups.get(issue.ruleId);\n if (existing) {\n existing.count += 1;\n if (existing.pages.length < SAMPLE_LIMIT) existing.pages.push(issue.pageUrl);\n else existing.morePages += 1;\n } else {\n groups.set(issue.ruleId, {\n ruleId: issue.ruleId,\n severity: issue.severity,\n why: issue.why,\n fix: issue.fix,\n sample: issue.message,\n count: 1,\n pages: [issue.pageUrl],\n morePages: 0,\n });\n }\n }\n const sevRank: Record<Severity, number> = { error: 0, warning: 1, info: 2 };\n return [...groups.values()].sort(\n (a, b) => sevRank[a.severity] - sevRank[b.severity] || a.ruleId.localeCompare(b.ruleId),\n );\n}\n\nfunction rollupReciprocity(report: GoflagReport): RollupReciprocity[] {\n const groups = new Map<ReciprocityCode, RollupReciprocity>();\n for (const issue of report.missingTranslations.reciprocity) {\n const existing = groups.get(issue.code);\n if (existing) {\n existing.count += 1;\n if (existing.pages.length < SAMPLE_LIMIT) existing.pages.push(issue.url);\n else existing.morePages += 1;\n } else {\n groups.set(issue.code, {\n code: issue.code,\n count: 1,\n sample: issue.message,\n pages: [issue.url],\n morePages: 0,\n });\n }\n }\n return [...groups.values()].sort((a, b) => b.count - a.count || a.code.localeCompare(b.code));\n}\n","/**\n * Live progress logger for the CLI.\n *\n * Everything here writes to a *side* stream (stderr by default) so the JSON\n * report on stdout is never polluted. Three modes:\n *\n * - \"compact\" (default): a single, in-place status line — spinner +\n * progress bar + \"done/total\" + the current page. On a non-TTY (CI,\n * pipes) it degrades to throttled plain lines so logs stay readable.\n * - \"verbose\": one line per page as it is analyzed (status + URL), plus\n * phase headers. No spinner; lines scroll.\n * - \"quiet\": nothing at all.\n *\n * The logger is fed `ProgressEvent`s (see `./build.ts`) via `onProgress`,\n * and the CLI calls `note()` for banners/warnings and `stop()` at the end.\n */\n\nimport type { ProgressEvent } from \"./build\";\n\nexport type LogMode = \"compact\" | \"verbose\" | \"quiet\";\n\ninterface WriteStreamLike {\n write(chunk: string): boolean;\n isTTY?: boolean;\n columns?: number;\n}\n\nexport interface LoggerOptions {\n /** Destination stream. Defaults to `process.stderr`. */\n stream?: WriteStreamLike;\n /** Emit ANSI colour. Defaults to `stream.isTTY`. */\n color?: boolean;\n /** Display mode. Defaults to \"compact\". */\n mode?: LogMode;\n /** Spinner tick interval in ms. `0` disables the animating timer (tests). */\n spinnerIntervalMs?: number;\n /** Minimum gap between plain progress lines on a non-TTY. Defaults to 250. */\n throttleMs?: number;\n /** Injectable clock (tests). */\n now?: () => number;\n}\n\nconst FRAMES = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\nconst BAR_WIDTH = 18;\n\nconst ANSI = {\n reset: \"\\x1b[0m\",\n dim: \"\\x1b[2m\",\n bold: \"\\x1b[1m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n cyan: \"\\x1b[36m\",\n};\n\nconst PHASE_LABEL: Record<ProgressEvent[\"phase\"], string> = {\n crawl: \"crawl\",\n scan: \"scan\",\n links: \"links\",\n};\n\nconst PHASE_HEADER: Record<ProgressEvent[\"phase\"], string> = {\n crawl: \"Crawling pages\",\n scan: \"Scanning pages for links\",\n links: \"Checking links\",\n};\n\nconst PHASE_DONE_NOUN: Record<ProgressEvent[\"phase\"], string> = {\n crawl: \"pages\",\n scan: \"pages scanned\",\n links: \"links\",\n};\n\nexport class Logger {\n private readonly stream: WriteStreamLike;\n private readonly color: boolean;\n private readonly mode: LogMode;\n private readonly isTTY: boolean;\n private readonly spinnerIntervalMs: number;\n private readonly throttleMs: number;\n private readonly now: () => number;\n\n private timer: ReturnType<typeof setInterval> | undefined;\n private frame = 0;\n private last: ProgressEvent | undefined;\n private lineOpen = false;\n private lastPlainAt = 0;\n private headerShown: Partial<Record<ProgressEvent[\"phase\"], boolean>> = {};\n\n constructor(options: LoggerOptions = {}) {\n this.stream = options.stream ?? process.stderr;\n this.isTTY = this.stream.isTTY === true;\n this.color = options.color ?? this.isTTY;\n this.mode = options.mode ?? \"compact\";\n this.spinnerIntervalMs = options.spinnerIntervalMs ?? 80;\n this.throttleMs = options.throttleMs ?? 250;\n this.now = options.now ?? Date.now;\n }\n\n private paint(code: string, text: string): string {\n return this.color ? `${code}${text}${ANSI.reset}` : text;\n }\n\n /** Print a standalone line (banner, warning). Survives around the spinner. */\n note(message: string): void {\n if (this.mode === \"quiet\") return;\n this.clearLine();\n this.stream.write(`${this.paint(ANSI.dim, message)}\\n`);\n if (this.mode === \"compact\" && this.last) this.render();\n }\n\n /** Feed one progress tick. */\n onProgress = (event: ProgressEvent): void => {\n if (this.mode === \"quiet\") return;\n this.last = event;\n\n if (this.mode === \"verbose\") {\n this.verboseLine(event);\n return;\n }\n\n // compact\n if (this.isTTY) {\n this.ensureTimer();\n this.render();\n } else {\n this.plainThrottled(event);\n }\n };\n\n /** Stop the spinner and clear the status line so the report starts clean. */\n stop(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = undefined;\n }\n this.clearLine();\n }\n\n // --- rendering ---------------------------------------------------------\n\n private ensureTimer(): void {\n if (this.timer || this.spinnerIntervalMs <= 0) return;\n this.timer = setInterval(() => {\n this.frame = (this.frame + 1) % FRAMES.length;\n this.render();\n }, this.spinnerIntervalMs);\n // Don't keep the process alive just for the spinner.\n (this.timer as { unref?: () => void }).unref?.();\n }\n\n private render(): void {\n if (!this.last) return;\n const line = this.composeLine(this.last);\n if (this.isTTY) {\n this.stream.write(`\\r\\x1b[2K${line}`);\n this.lineOpen = true;\n } else {\n this.stream.write(`${line}\\n`);\n }\n }\n\n private composeLine(e: ProgressEvent): string {\n const spinner = this.paint(ANSI.cyan, FRAMES[this.frame]!);\n const phase = this.paint(ANSI.bold, PHASE_LABEL[e.phase]);\n const counts = `${e.done}/${e.total}`;\n const bar = this.bar(e.done, e.total);\n const subject = e.url ? this.paint(ANSI.dim, this.truncate(e.url)) : \"\";\n return [spinner, phase, bar, counts, subject].filter(Boolean).join(\" \");\n }\n\n private bar(done: number, total: number): string {\n if (total <= 0) return \"\";\n const ratio = Math.max(0, Math.min(1, done / total));\n const filled = Math.round(ratio * BAR_WIDTH);\n const bar = \"█\".repeat(filled) + \"░\".repeat(BAR_WIDTH - filled);\n return this.paint(ANSI.dim, `[${bar}]`);\n }\n\n private verboseLine(e: ProgressEvent): void {\n if (!this.headerShown[e.phase]) {\n this.headerShown[e.phase] = true;\n this.stream.write(`${this.paint(ANSI.bold, PHASE_HEADER[e.phase])}\\n`);\n }\n if (e.phase === \"crawl\" && e.url) {\n const counts = this.paint(ANSI.dim, `(${e.done}/${e.total})`);\n this.stream.write(` ${this.statusTag(e.status)} ${e.url} ${counts}\\n`);\n } else if (e.phase === \"scan\" || e.phase === \"links\") {\n // Avoid a line per item; just mark completion of the phase.\n if (e.done === e.total && e.total > 0) {\n this.stream.write(` ${this.paint(ANSI.dim, `${e.total} ${PHASE_DONE_NOUN[e.phase]}`)}\\n`);\n }\n }\n }\n\n private statusTag(status: number | undefined): string {\n if (status === undefined) return this.paint(ANSI.dim, \"···\");\n if (status >= 200 && status < 300) return this.paint(ANSI.green, String(status));\n if (status >= 300 && status < 400) return this.paint(ANSI.yellow, String(status));\n return this.paint(ANSI.red, String(status));\n }\n\n private plainThrottled(e: ProgressEvent): void {\n const t = this.now();\n const done = e.done === e.total;\n if (!done && t - this.lastPlainAt < this.throttleMs) return;\n this.lastPlainAt = t;\n this.stream.write(`goflag: ${PHASE_LABEL[e.phase]} ${e.done}/${e.total}\\n`);\n }\n\n private clearLine(): void {\n if (this.isTTY && this.lineOpen) {\n this.stream.write(\"\\r\\x1b[2K\");\n this.lineOpen = false;\n }\n }\n\n private truncate(url: string): string {\n const width = this.stream.columns ?? 80;\n // Leave room for spinner + phase + bar + counts (~40 cols).\n const budget = Math.max(16, width - 44);\n if (url.length <= budget) return url;\n return `…${url.slice(url.length - budget + 1)}`;\n }\n}\n"],"mappings":"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","names":["DEFAULT_UA","titleMissing","canonicalMissing","hreflangMissing","probes","segments","cheerio","relaxTlsIfRequested","sorted","relaxTlsIfRequested","cheerio","resolveBase","DEFAULTS","DEFAULTS","hostOf","ANSI","paint","VERDICT_FLAG","verdictColor","stripBackticks","ANSI"]}
|