@goflag/cli 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/robots/parse.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/bcp47.ts","../src/lib/core/i18n.ts","../src/lib/core/robots/match.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/net/fetch-url.ts","../src/lib/core/links/classify.ts","../src/lib/core/links/check.ts","../src/lib/core/sitemap/entries.ts","../src/lib/core/clusters.ts","../src/lib/core/coverage.ts","../src/lib/core/probes/assets.ts","../src/lib/core/probes/favicon.ts","../src/lib/rules/advisory.ts","../src/lib/rules/profiles/index.ts","../src/lib/rules/prose.ts","../src/lib/rules/site-prose.ts","../src/lib/core/links/extract.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/render-preview.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\n/**\n * Parse the `sizes` syntax — a space-separated list of `WxH`, or `any`.\n *\n * Exported because a Web App Manifest's `icons[].sizes` member is defined by\n * reference to this same attribute, and the extraction adapter reads both. One\n * syntax, one parser.\n */\nexport function 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\n/** One `Allow:` / `Disallow:` line, with where it was written. */\nexport interface RobotsRule {\n kind: \"allow\" | \"disallow\";\n pattern: string;\n line: number;\n}\n\n/** One group: the agents it names and the rules that follow them. */\nexport interface RobotsGroup {\n /** Product tokens, lowercased — matching is case-insensitive. */\n userAgents: Array<{ value: string; line: number }>;\n rules: RobotsRule[];\n}\n\n/** A line that parsed as nothing, kept so a rule can point at it. */\nexport interface RobotsInvalidLine {\n line: number;\n raw: string;\n reason: string;\n}\n\n/** What `parseRobots` reads out of a body, independent of how it was fetched. */\nexport interface RobotsParse {\n groups: RobotsGroup[];\n /** `Sitemap:` records. Independent of groups per RFC 9309 §2.2.4. */\n sitemaps: Array<{ value: string; line: number }>;\n invalidLines: RobotsInvalidLine[];\n /** Recognisable non-standard directives (`Crawl-delay`, `Host`, …). */\n unknownDirectives: Array<{ name: string; line: number }>;\n}\n\n/**\n * The site's robots.txt, fetched and fully parsed\n * (`docs/sitemap-robots-plan.md` §3.1).\n *\n * Replaces the two booleans the engine used to keep. Rules read this the way\n * page rules read an `Extraction`: everything the file said, including the\n * lines it got wrong, each carrying its number so a finding can point at one.\n */\nexport interface RobotsProbe extends RobotsParse {\n url: string;\n /** `0` on a network error. */\n status: number;\n /** Whether the file was fetched and read (a 404 is `false`, and fine). */\n found: boolean;\n raw?: string;\n /** Body length in bytes — RFC 9309 §2.4's 500 KiB limit counts octets. */\n byteLength: number;\n /** Redirects followed; RFC 9309 §2.3.1.2 permits following them. */\n redirects: { count: number; finalUrl: string; crossOrigin: boolean };\n}\n\n/** One intrinsic size a file declares about itself. */\nexport interface AssetSize {\n width: number;\n height: number;\n}\n\n/**\n * What is actually served at an asset URL a page declared — an `og:image`, an\n * icon. Produced once per distinct URL by the probe pass in `report/build.ts`\n * (`docs/og-plan.md` D8), never by a rule.\n */\nexport interface AssetProbe {\n url: string;\n /** HTTP status. `0` on a network failure. */\n status: number;\n /** 2xx *and* an image content type. A 200 of HTML is not an image. */\n ok: boolean;\n contentType?: string;\n /**\n * Sizes decoded from the file's header. Several for an ICO container, one\n * for a PNG, absent for every format this does not decode — which a rule\n * must read as \"unknown\", never as \"none\".\n */\n sizes?: AssetSize[];\n}\n\n/**\n * What is served at one URL a sitemap lists.\n *\n * `via` says where the answer came from — the crawl, the link audit, or a\n * fetch made for this — because \"goflag checked it\" and \"goflag already knew\"\n * are different claims, and only one of them costs the site a request.\n */\nexport interface SitemapEntryProbe {\n url: string;\n /** `0` on a network failure. */\n status: number;\n via: \"crawl\" | \"link-audit\" | \"probe\";\n /** Where it landed; equals `url` when nothing redirected. */\n finalUrl: string;\n redirected: boolean;\n}\n\n/**\n * What the origin answers at `/favicon.ico`.\n *\n * Origin-level by nature: one file governs the whole site, exactly like\n * robots.txt, so it is probed once per run rather than per page.\n */\nexport interface FaviconProbe {\n url: string;\n status: number;\n /** 2xx *and* an image content type. A 200 of HTML is a soft 404. */\n found: boolean;\n /** From `content-type`, lowercased, parameters stripped. */\n contentType?: string;\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 * Why the escalation this page needed could not run — Playwright absent, or\n * its Chromium missing.\n *\n * Separate from `escalationReason` because the two are opposite facts and\n * one used to carry both as prose. A page that wanted the browser and did\n * not get it is judged on an unhydrated shell, which invents a full column\n * of `title.missing` / `description.missing` findings; the report has to say\n * so rather than let the reader believe the site is empty. `report/build.ts`\n * turns any page carrying this into a diagnostics warning.\n */\n escalationBlocked?: 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 * What was served at the asset URLs this page declared — `og:image`, icons —\n * keyed by the URL as it was resolved. Filled by the probe pass in\n * `report/build.ts`, deduplicated across the whole run.\n *\n * Absent when no pass ran, which is a different claim from an empty map: the\n * rules that read it answer `na` on absence and would be inventing a finding\n * if they did anything else.\n */\n assets?: Record<string, AssetProbe>;\n}\n\n// ---------------------------------------------------------------------------\n// Issues (forward declaration — Phase 5 fills in the rule engine)\n// ---------------------------------------------------------------------------\n\nexport type Severity = \"error\" | \"warning\" | \"info\";\n\n/**\n * How authoritative the requirement behind a finding is.\n *\n * Lives here rather than in the rule layer for the same reason `Issue` does:\n * it crosses the engine → report → CLI `--json` boundary. An agent reading a\n * report has to be able to tell a `spec-required` from a `heuristic` before it\n * decides what to change, and a type that stopped at the rule registry could\n * not tell it — `../rules/types` re-exports this one.\n */\nexport type Rigor =\n \"spec-required\" | \"spec-recommended\" | \"vendor-spec\" | \"guideline\" | \"heuristic\";\n\nexport interface Issue {\n ruleId: string;\n severity: Severity;\n message: string;\n /**\n * How authoritative the requirement is, and the documents that back it.\n *\n * Carried on the finding rather than looked up by the consumer, because a\n * report is read where the registry is not: a JSON file in CI, a PR comment,\n * an agent's context. Absent on a cross-page rule that has not declared one\n * (see `SiteRule`) and on the synthetic `engine.rule-crashed`.\n */\n rigor?: Rigor;\n sources?: string[];\n /** What the page actually said, as the rule saw it. JSON-serializable. */\n observed?: unknown;\n /** One sentence stating what a passing page looks like. */\n expected?: 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 (`&amp;` → `&`, `&lt;` → `<`, 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/**\n * How far the walker descends before it stops.\n *\n * `JSON.parse` is no protection here: V8's parser is iterative and accepts\n * nesting far past what a recursive walker survives — 30 KB of\n * `{\"a\":{\"a\":…}}` parses cleanly and then overflows the stack below. This\n * walker runs on every audit, not just where a rule reports structured data,\n * and it is the extraction pass, so throwing here loses the whole page: the\n * crawl's per-page catch could only file it as `[network error] … Maximum\n * call stack size exceeded`, which is a page auditing itself out of the run\n * by declaring one script. Callers reached outside that catch got the throw.\n *\n * A real schema.org graph nests a handful of levels — `@graph` → article →\n * author → organization → image. Whatever a page declares 100 levels down is\n * not an entity anyone will search for, so refusing to descend costs no type\n * we would have reported.\n */\nconst MAX_DEPTH = 100;\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, 0);\n return [...out];\n}\n\nfunction visit(node: unknown, out: Set<string>, depth: number): void {\n if (node === null || node === undefined) return;\n if (depth > MAX_DEPTH) return;\n if (Array.isArray(node)) {\n for (const item of node) visit(item, out, depth + 1);\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, depth + 1);\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, depth + 1);\n }\n}\n\nfunction decodeHtmlEntities(s: string): string {\n return s\n .replaceAll(\"&quot;\", '\"')\n .replaceAll(\"&apos;\", \"'\")\n .replaceAll(\"&#39;\", \"'\")\n .replaceAll(\"&#x27;\", \"'\")\n .replaceAll(\"&lt;\", \"<\")\n .replaceAll(\"&gt;\", \">\")\n .replaceAll(\"&nbsp;\", \" \")\n .replaceAll(\"&amp;\", \"&\");\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","/**\n * A robots.txt parser that keeps what it could not understand.\n *\n * The engine has had two ad-hoc readers of this file — a regex for `Sitemap:`\n * lines and a line-walk looking for `Disallow: /` — each answering exactly one\n * question and throwing the rest away. That is why goflag can say \"this site\n * blocks everything\" and nothing else about a file that is frequently where a\n * site's whole indexing story goes wrong.\n *\n * `docs/sitemap-robots-plan.md` §3.1 asks for the opposite: a parse that keeps\n * everything, **including the lines it rejected**, because \"there is a typo on\n * line 14\" is a finding and a boolean cannot carry it. Every record therefore\n * carries its line number.\n *\n * RFC 9309 is the reference throughout. The parts that matter here:\n *\n * - §2.2.1 a group is one or more `User-agent` lines followed by rules; a\n * `User-agent` line after a rule line starts a **new** group.\n * - §2.2.4 `Sitemap:` is independent of groups — it belongs to the file, not\n * to whichever group it happens to sit in.\n * - §2.2.2 an empty `Disallow:` is valid and allows everything.\n * - Comments run from `#` to end of line, anywhere.\n */\n\nimport type { RobotsGroup, RobotsInvalidLine, RobotsParse } from \"../types\";\n\n/** Directives that are part of the standard and belong to a group. */\nconst RULE_DIRECTIVES = new Set([\"allow\", \"disallow\"]);\n\n/**\n * Directives that are not in RFC 9309 but appear often enough that calling\n * them typos would be wrong. Recognised so a rule can say \"this is ignored\"\n * rather than \"this is broken\" — two different things to tell a reader.\n */\nconst KNOWN_EXTENSIONS = new Set([\"crawl-delay\", \"host\", \"clean-param\", \"request-rate\", \"noindex\"]);\n\n/**\n * Parse a robots.txt body.\n *\n * Never throws and never rejects a file wholesale: a robots.txt that is half\n * garbage still has a working half, and a crawler would use it. Lines that\n * parse as nothing are collected rather than dropped.\n */\nexport function parseRobots(body: string): RobotsParse {\n const groups: RobotsGroup[] = [];\n const sitemaps: Array<{ value: string; line: number }> = [];\n const invalidLines: RobotsInvalidLine[] = [];\n const unknownDirectives: Array<{ name: string; line: number }> = [];\n\n /** The group being filled, and whether it has taken a rule yet. */\n let current: RobotsGroup | undefined;\n let currentTookRule = false;\n\n const lines = body.split(/\\r\\n|\\r|\\n/);\n\n for (const [index, rawLine] of lines.entries()) {\n const line = index + 1;\n const withoutComment = rawLine.replace(/#.*$/, \"\");\n const trimmed = withoutComment.trim();\n if (trimmed.length === 0) continue;\n\n const separator = trimmed.indexOf(\":\");\n if (separator === -1) {\n invalidLines.push({ line, raw: rawLine, reason: \"no `:` separator\" });\n continue;\n }\n\n const name = trimmed.slice(0, separator).trim().toLowerCase();\n const value = trimmed.slice(separator + 1).trim();\n\n if (name === \"user-agent\") {\n if (value.length === 0) {\n invalidLines.push({ line, raw: rawLine, reason: \"empty `User-agent`\" });\n continue;\n }\n // §2.2.1: consecutive `User-agent` lines share one group; one that\n // follows a rule opens a new group instead.\n if (!current || currentTookRule) {\n current = { userAgents: [], rules: [] };\n groups.push(current);\n currentTookRule = false;\n }\n current.userAgents.push({ value: value.toLowerCase(), line });\n continue;\n }\n\n if (name === \"sitemap\") {\n // §2.2.4: independent of groups, so it does not close or open one.\n if (value.length === 0) {\n invalidLines.push({ line, raw: rawLine, reason: \"empty `Sitemap`\" });\n continue;\n }\n sitemaps.push({ value, line });\n continue;\n }\n\n if (RULE_DIRECTIVES.has(name)) {\n if (!current) {\n // A rule with no group above it governs nobody. Crawlers drop it, and\n // the author almost certainly did not mean that.\n invalidLines.push({\n line,\n raw: rawLine,\n reason: `\\`${name}\\` before any \\`User-agent\\` group`,\n });\n continue;\n }\n current.rules.push({ kind: name as \"allow\" | \"disallow\", pattern: value, line });\n currentTookRule = true;\n continue;\n }\n\n if (KNOWN_EXTENSIONS.has(name)) {\n unknownDirectives.push({ name, line });\n continue;\n }\n\n invalidLines.push({ line, raw: rawLine, reason: `unknown directive \\`${name}\\`` });\n }\n\n return { groups, sitemaps, invalidLines, unknownDirectives };\n}\n","import { parseRobots } from \"../robots/parse\";\nimport type { RobotsProbe } from \"../types\";\nimport { combineSignals } from \"./abort\";\n\nexport interface RobotsProbeOptions {\n signal?: AbortSignal;\n timeoutMs?: number;\n}\n\n/**\n * Fetch and parse the origin's robots.txt.\n *\n * The two questions the engine used to ask — where are the sitemaps, does this\n * block everything — are now two readings of one parse\n * (`docs/sitemap-robots-plan.md` §3.1), so a rule can also ask about a typo on\n * line 14 or a directive nobody implements.\n *\n * A failure is not the same as an absence, and the shape says so: a 404 is\n * `found: false` with an empty parse, which RFC 9309 §2.3.1.3 reads as \"allow\n * everything\". A 5xx or a network error is also `found: false` but keeps its\n * status, because §2.3.1.4 says a crawler must then assume the opposite —\n * complete disallow — and `robotstxt.unreachable` is the rule that says so.\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 const empty = { groups: [], sitemaps: [], invalidLines: [], unknownDirectives: [] };\n const noRedirect = { count: 0, finalUrl: url, crossOrigin: false };\n\n try {\n const res = await fetch(url, { signal, redirect: \"follow\" });\n\n // `fetch` does not report the hop count, but it does report where it\n // landed — which is the part a rule can act on. A different origin is the\n // proxy accident `robotstxt.cross-origin` exists for.\n const redirected = res.url !== url;\n const redirects = {\n count: redirected ? 1 : 0,\n finalUrl: res.url || url,\n crossOrigin: redirected && !sameOrigin(res.url, url),\n };\n\n if (!res.ok) {\n return { url, status: res.status, found: false, byteLength: 0, redirects, ...empty };\n }\n\n const raw = await res.text();\n return {\n url,\n status: res.status,\n found: true,\n raw,\n byteLength: Buffer.byteLength(raw, \"utf8\"),\n redirects,\n ...parseRobots(raw),\n };\n } catch {\n return { url, status: 0, found: false, byteLength: 0, redirects: noRedirect, ...empty };\n } finally {\n cleanup();\n }\n}\n\nfunction sameOrigin(a: string, b: string): boolean {\n try {\n return new URL(a).origin === new URL(b).origin;\n } catch {\n return true;\n }\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 and record why on\n // `escalationBlocked`, which `report/build.ts` turns into a diagnostics\n // warning: judging an unhydrated shell produces a page of findings that are\n // all false, and the reader has no way to know unless the report says it.\n // We still never crash a default `inspect` run over a missing Playwright.\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: heuristic.reason,\n escalationBlocked: 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, after the retry. */\n errors: CrawlError[];\n /**\n * URLs that failed once and answered on the second attempt. Reported rather\n * than swallowed: a page that needs two tries is a fact about the site, and\n * a run that quietly recovered from six of them is not the same run as one\n * that recovered from none.\n */\n recovered: string[];\n /** True when the crawl stopped because `maxPages` was hit. */\n truncated: boolean;\n}\n\ninterface QueueItem {\n url: string;\n depth: number;\n /** How many times this URL has already been tried and thrown. */\n attempt?: number;\n}\n\n/**\n * One retry per URL, and the retry goes to the back of the frontier rather\n * than firing straight away.\n *\n * Immediately is the wrong moment: a page that just timed out is a page whose\n * server is cold or busy, and asking again in the same wave asks the same\n * question of the same overloaded process. The back of the queue is minutes\n * away on a crawl of any size, which is the spacing this needs and costs\n * nothing to arrange.\n *\n * One, not three: the report now treats a page that never answered as a\n * finding, so the cost of a false timeout is a red pipeline rather than a\n * silent hole. That is worth one more request. It is not worth turning a site\n * that is genuinely down into three times the load and three times the wait —\n * `errors` is the honest answer there, and it arrives sooner.\n */\nconst MAX_ATTEMPTS = 2;\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 recovered: [],\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 const recovered: string[] = [];\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 const attempt = (result.item.attempt ?? 0) + 1;\n if (attempt < MAX_ATTEMPTS) {\n queue.push({ ...result.item, attempt });\n continue;\n }\n errors.push({ url: result.item.url, message: result.error });\n continue;\n }\n\n if (result.item.attempt) recovered.push(result.item.url);\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 recovered,\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 // The finding has known all four since it was evaluated; this is where\n // they used to stop. A report that drops the rigor asks every consumer\n // to re-derive it from a registry it does not have.\n rigor: finding.rigor,\n sources: finding.sources,\n observed: finding.observed,\n expected: finding.expected,\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 { ExtractionAsset, ExtractionIcon } from \"./extraction/types\";\nimport type { BooleanRule, Extraction, 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\n/** The aspect ratio a link preview is laid out for, and how far off is tolerable. */\nconst RATIO_IDEAL: [number, number] = [1.7, 2.1];\nconst RATIO_ACCEPTABLE: [number, number] = [1, 3];\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\n/** Whether a string is a URL a crawler can fetch without a base to resolve it. */\nfunction isAbsoluteHttpUrl(value: string): boolean {\n try {\n const url = new URL(value);\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n } catch {\n return false;\n }\n}\n\n/** A language tag reduced to the two subtags `og:locale` is able to express. */\nfunction localeParts(tag: string): { language: string; region?: string } | undefined {\n const subtags = tag.trim().toLowerCase().split(/[-_]/).filter(Boolean);\n const language = subtags[0];\n if (!language || !/^[a-z]{2,3}$/.test(language)) return undefined;\n const region = subtags.slice(1).find((s) => /^([a-z]{2}|\\d{3})$/.test(s));\n return region ? { language, region } : { language };\n}\n\n/**\n * Whether two tags name the same locale for Open Graph's purposes.\n *\n * `og:locale` is `language_TERRITORY` and hreflang is BCP 47, so `pt_BR` and\n * `pt-BR` are one declaration written twice. A tag with no territory matches\n * every territory of its language: a page whose hreflang says `fr` cannot be\n * faulted for an `og:locale:alternate` of `fr_FR`, because only one of the two\n * formats is able to state the difference.\n */\nfunction sameLocale(a: string, b: string): boolean {\n const left = localeParts(a);\n const right = localeParts(b);\n if (!left || !right || left.language !== right.language) return false;\n return !left.region || !right.region || left.region === right.region;\n}\n\n/**\n * The locales this page's hreflang annotations put it in a cluster with,\n * normalized and de-duplicated. `x-default` is excluded: it names a fallback,\n * not a locale, and counting it would make a monolingual page look translated.\n */\nfunction hreflangCluster(ex: Extraction): string[] {\n const seen = new Set<string>();\n for (const annotation of ex.links.hreflang) {\n if (annotation.isXDefault) continue;\n const parts = localeParts(annotation.hreflang);\n if (!parts) continue;\n seen.add(parts.region ? `${parts.language}-${parts.region}` : parts.language);\n }\n return [...seen];\n}\n\n/** One asset's outcome, said the way a finding has to say it. */\nfunction describeAsset(status: number, contentType: string | undefined): string {\n if (status === 0) return \"the request failed\";\n if (status >= 400 || status < 200) return `HTTP ${status}`;\n return `HTTP ${status} with \\`${contentType ?? \"no content type\"}\\`, which is not an image`;\n}\n\n/** `rel` tokens that declare an icon, lowercased. */\nconst ICON_RELS = new Set([\n \"icon\",\n \"shortcut icon\",\n \"apple-touch-icon\",\n \"apple-touch-icon-precomposed\",\n \"mask-icon\",\n]);\n\n/** `rel` tokens Apple reads for the home-screen icon. */\nconst APPLE_ICON_RELS = new Set([\"apple-touch-icon\", \"apple-touch-icon-precomposed\"]);\n\n/** The icons this page's `<head>` declares, whatever flavour of `rel` it used. */\nfunction declaredIcons(ex: Extraction): ExtractionIcon[] {\n return ex.links.icons.filter((icon) => ICON_RELS.has(icon.rel.trim().toLowerCase()));\n}\n\nfunction normalise(value: string): string {\n return value.trim().toLowerCase().split(/\\s+/).sort().join(\" \");\n}\n\n/**\n * Whether two icon references name the same file.\n *\n * A `<link href>` is resolved against the page; a manifest `src` is resolved\n * against the manifest and arrives verbatim. Comparing the paths — after\n * dropping any query — is what lets `/icon.png` and\n * `https://example.com/icon.png` be recognised as one declaration made twice,\n * which is the only case this comparison exists for.\n */\nfunction sameAsset(href: string, src: string): boolean {\n const pathOf = (value: string): string => {\n try {\n return new URL(value, \"https://goflag.invalid\").pathname;\n } catch {\n return value.split(\"?\")[0] ?? value;\n }\n };\n return pathOf(href) === pathOf(src);\n}\n\n/**\n * Whether the page opted into Open Graph at all. The locale rules stay quiet\n * on a page with no `og:*` tags: `og.title.missing` and `og.image.missing`\n * already say the thing worth saying, and a third finding asking for\n * `og:locale` on a page that has no Open Graph is noise stacked on a verdict.\n */\nfunction hasOpenGraph(ex: Extraction): boolean {\n return Boolean(\n ex.openGraph.title?.value || ex.openGraph.images.length > 0 || ex.openGraph.url?.value,\n );\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 if (!hasOpenGraph(ex)) 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 ogImageAbsolute: BooleanRule = {\n id: \"og.image.absolute\",\n kind: \"boolean\",\n category: \"opengraph\",\n // The same severity as `og.image.missing`, deliberately: the unfurl is\n // identical either way, and the registry should not rank the broken\n // declaration above the absent one when their effect is the same.\n severity: \"warning\",\n title: \"`og:image` must be an absolute URL\",\n why:\n \"Unlike a browser, the crawler that builds the preview has no document to \" +\n \"resolve a relative path against — it reads the tag, not the page. A \" +\n \"relative `og:image` is therefore not a smaller mistake than a missing \" +\n \"one: the unfurl is identical, and the tag makes it look handled.\",\n rigor: \"vendor-spec\",\n sources: [\"ogp\", \"meta-og-sharing\"],\n reads: [\"openGraph.images\"],\n expected: \"every `og:image` URL absolute, scheme included\",\n relates: [\"og.image.missing\", \"canonical.absolute\"],\n fix: {\n title: \"Give Next a metadataBase, or write the origin in\",\n snippet: [\n \"// app/layout.tsx — every relative metadata URL resolves against this.\",\n \"export const metadata = {\",\n \" metadataBase: new URL(process.env.SITE_ORIGIN!),\",\n ' openGraph: { images: [\"/og.png\"] }, // emitted absolute',\n \"};\",\n ].join(\"\\n\"),\n language: \"tsx\",\n },\n evaluate: (ex) => {\n if (ex.openGraph.images.length === 0) return { status: \"na\", observed: null };\n\n const declarations: Array<{ property: string; value: string }> = [];\n for (const image of ex.openGraph.images) {\n declarations.push({ property: \"og:image\", value: image.url.value.trim() });\n if (image.secureUrl) {\n declarations.push({\n property: \"og:image:secure_url\",\n value: image.secureUrl.value.trim(),\n });\n }\n }\n\n const observed = declarations.map((d) => d.value);\n const relative = declarations.filter((d) => !isAbsoluteHttpUrl(d.value));\n if (relative.length === 0) return { status: \"pass\", observed };\n\n return {\n status: \"fail\",\n observed,\n message: `Open Graph image URL is not absolute: ${relative\n .map((d) => `\\`${d.property}\\` = \\`${d.value}\\``)\n .join(\", \")}. Crawlers cannot resolve it.`,\n origin: { kind: \"meta\", property: relative[0]!.property },\n };\n },\n};\n\nconst ogImageAlt: BooleanRule = {\n id: \"og.image.alt\",\n kind: \"boolean\",\n category: \"opengraph\",\n severity: \"warning\",\n title: \"Describe the shared image with `og:image:alt`\",\n why:\n \"The protocol defines `og:image:alt` as a description of what is in the \" +\n \"image, and a generated card usually carries the page's title as pixels. \" +\n \"Omitting it leaves that text sighted-only, in the one place a link is \" +\n \"seen before anybody has opened the page. Optional in the vocabulary, \" +\n \"which is why this is a guideline and not a spec violation.\",\n rigor: \"guideline\",\n sources: [\"ogp\"],\n reads: [\"openGraph.images\"],\n expected: \"an `og:image:alt` beside every `og:image`\",\n relates: [\"og.image.missing\"],\n fix: {\n title: \"Carry the alt per image, not as a constant\",\n snippet: [\n \"// app/…/opengraph-image.tsx — `export const alt` is one fixed string, so a\",\n \"// translated or data-derived description has to go through the metadata fn.\",\n \"export async function generateImageMetadata({ params }) {\",\n \" const t = await getTranslations({ locale: params.locale });\",\n ' return [{ id: \"og\", size, contentType, alt: t(\"og.alt\") }];',\n \"}\",\n ].join(\"\\n\"),\n language: \"tsx\",\n },\n evaluate: (ex) => {\n if (ex.openGraph.images.length === 0) return { status: \"na\", observed: null };\n\n const observed = ex.openGraph.images.map((image) => ({\n url: image.url.value,\n alt: image.alt?.value ?? null,\n }));\n const undescribed = observed.filter((image) => !image.alt?.trim());\n if (undescribed.length === 0) return { status: \"pass\", observed };\n\n return {\n status: \"fail\",\n observed,\n message:\n undescribed.length === observed.length\n ? \"Page declares an `og:image` with no `og:image:alt`.\"\n : `${undescribed.length} of ${observed.length} \\`og:image\\` declarations have no \\`og:image:alt\\`.`,\n origin: { kind: \"meta\", property: \"og:image:alt\" },\n };\n },\n};\n\n/**\n * The sibling of `og.image.alt`, and a separate id for a separate defect.\n *\n * The presence rule cannot see this one: the tag is there, so it passes, and\n * it will keep passing. Splitting it costs a catalogue entry and makes both\n * verdicts readable — the same reasoning that split `og.image.dimensions` from\n * `og.image.ratio`.\n *\n * Found by looking, not by auditing. `goflag preview` put the alt beside the\n * card on two independent sites and both repeated the title: twelve pages of\n * this project's own documentation, three articles on the second site. The\n * catalogue waits for two consumers, and that was the second.\n */\nconst ogImageAltCaption: BooleanRule = {\n id: \"og.image.alt.caption\",\n kind: \"boolean\",\n category: \"opengraph\",\n severity: \"info\",\n title: \"`og:image:alt` describes the image, it does not repeat the title\",\n why:\n \"The protocol asks for a description of what is in the image, and says in \" +\n \"the same breath that it is not a caption. A copy of the title is the one \" +\n \"value that is always available and never describes anything: a reader \" +\n \"who cannot see the card is handed the same words twice and learns \" +\n \"nothing about the picture. It also satisfies `og.image.alt` forever, \" +\n \"which is why it needs an id of its own.\",\n rigor: \"guideline\",\n sources: [\"ogp\"],\n reads: [\"openGraph.images\", \"openGraph.title\", \"document.title\"],\n expected: \"an `og:image:alt` that says something the title does not\",\n relates: [\"og.image.alt\", \"og.image.representative\"],\n fix: {\n title: \"Say what the card shows, not what the page is called\",\n snippet: [\n \"// The card is a title on a background. The alt says so — once, from a\",\n \"// template, so every page gets one and none of them is the title alone.\",\n \"const ogAlt = (title: string) => `The title \\u201C${title}\\u201D on a dark preview card.`;\",\n ].join(\"\\n\"),\n language: \"ts\",\n },\n evaluate: (ex) => {\n const described = ex.openGraph.images.filter((image) => image.alt?.value.trim());\n if (described.length === 0) return { status: \"na\", observed: null };\n\n // Compared against whichever title the card was built from: a page that\n // sets `og:title` names its card there, and one that does not falls back\n // to `<title>`, exactly as every consumer does.\n const title = (ex.openGraph.title?.value ?? ex.document.title?.value ?? \"\").trim();\n if (title === \"\") return { status: \"na\", observed: null };\n\n const observed = described.map((image) => ({\n url: image.url.value,\n alt: image.alt?.value ?? \"\",\n }));\n const captions = observed.filter((image) => image.alt.trim() === title);\n if (captions.length === 0) return { status: \"pass\", observed };\n\n return {\n status: \"fail\",\n observed,\n message:\n captions.length === observed.length\n ? \"`og:image:alt` repeats the title instead of describing the image.\"\n : `${captions.length} of ${observed.length} \\`og:image:alt\\` values repeat the title.`,\n origin: { kind: \"meta\", property: \"og:image:alt\" },\n };\n },\n};\n\nconst ogImageDimensions: BooleanRule = {\n id: \"og.image.dimensions\",\n kind: \"boolean\",\n category: \"opengraph\",\n severity: \"warning\",\n title: \"Declare `og:image:width` and `og:image:height`\",\n why:\n \"A crawler that does not know the size cannot lay the card out until it \" +\n \"has fetched the image, so the first share of a URL renders without it — \" +\n \"the one share that matters most. The dimensions are the fix, and they \" +\n \"cost two tags.\",\n rigor: \"vendor-spec\",\n sources: [\"ogp\", \"meta-og-sharing\"],\n reads: [\"openGraph.images\"],\n expected: \"both `og:image:width` and `og:image:height` on every image\",\n relates: [\"og.image.missing\", \"og.image.ratio\"],\n fix: {\n title: \"Declare the size alongside the URL\",\n snippet: [\n \"// app/…/page.tsx\",\n \"export const metadata = {\",\n ' openGraph: { images: [{ url: \"/og.png\", width: 1200, height: 630 }] },',\n \"};\",\n \"// The file convention emits both from `export const size` for free.\",\n ].join(\"\\n\"),\n language: \"tsx\",\n },\n evaluate: (ex) => {\n if (ex.openGraph.images.length === 0) return { status: \"na\", observed: null };\n\n const observed = ex.openGraph.images.map((image) => ({\n url: image.url.value,\n width: image.width?.value ?? null,\n height: image.height?.value ?? null,\n }));\n const incomplete = observed.filter((image) => image.width === null || image.height === null);\n if (incomplete.length === 0) return { status: \"pass\", observed };\n\n return {\n status: \"fail\",\n observed,\n message: `${incomplete.length === observed.length ? \"The\" : `${incomplete.length} of ${observed.length}`} \\`og:image\\` declaration${\n incomplete.length === 1 ? \"\" : \"s\"\n } omit${incomplete.length === 1 ? \"s\" : \"\"} \\`og:image:width\\` or \\`og:image:height\\`.`,\n origin: { kind: \"meta\", property: \"og:image:width\" },\n };\n },\n};\n\nconst ogImageRatio: ScoredRule = {\n id: \"og.image.ratio\",\n kind: \"scored\",\n category: \"opengraph\",\n title: \"Keep the shared image near the 1.91:1 the card is laid out for\",\n why:\n \"Meta's recommended 1200×630 is a shape before it is a size. An image far \" +\n \"from it is not rejected — it is cropped to fit, and the crop is chosen \" +\n \"by the consumer rather than by you.\",\n rigor: \"vendor-spec\",\n sources: [\"meta-og-sharing\", \"x-cards\"],\n reads: [\"openGraph.images\"],\n bands: { ideal: RATIO_IDEAL, acceptable: RATIO_ACCEPTABLE },\n severityByBand: { acceptable: \"info\", poor: \"warning\" },\n expected: `a width ÷ height between ${RATIO_IDEAL[0]} and ${RATIO_IDEAL[1]} (1.91:1 is 1.9)`,\n relates: [\"og.image.dimensions\"],\n evaluate: (ex) => {\n // The first image that states its size: consumers take the first one they\n // can use, and a rule that measured the others would band a picture no\n // unfurl is going to show.\n const measured = ex.openGraph.images.find((image) => image.width?.value && image.height?.value);\n // No image, or none that declared its size — `og.image.dimensions` owns\n // that gap, and guessing a ratio from an unfetched image would be a\n // measurement goflag did not make.\n if (!measured) return { status: \"na\", observed: 0 };\n\n const ratio = Math.round((measured.width!.value / measured.height!.value) * 100) / 100;\n const { status, band } = bandFor(ratio, { ideal: RATIO_IDEAL, acceptable: RATIO_ACCEPTABLE });\n if (status === \"pass\") return { status, band, observed: ratio };\n\n return {\n status,\n band,\n observed: ratio,\n message: `\\`og:image\\` is ${measured.width!.value}×${measured.height!.value} — a ratio of ${ratio}:1, against the 1.91:1 the preview card is laid out for. Consumers will crop it.`,\n origin: { kind: \"meta\", property: \"og:image:width\" },\n };\n },\n};\n\nconst ogLocaleMissing: BooleanRule = {\n id: \"og.locale.missing\",\n kind: \"boolean\",\n category: \"opengraph\",\n severity: \"warning\",\n title: \"A translated page has to say which locale it is in\",\n why:\n \"`og:locale` defaults to `en_US` in the protocol itself — so a page that \" +\n \"omits it does not decline to answer, it answers wrongly. On a site whose \" +\n \"whole point is that the same page exists in several languages, every \" +\n \"translation claims to be American English.\",\n rigor: \"vendor-spec\",\n sources: [\"ogp\"],\n reads: [\"openGraph.locale\", \"links.hreflang\"],\n expected: \"an `og:locale` on any page that declares hreflang alternates\",\n relates: [\"og.locale.alternates\", \"hreflang.missing\"],\n fix: {\n title: \"Emit the locale in the page's own metadata\",\n snippet: [\n \"// app/[locale]/…/page.tsx\",\n \"export const metadata = {\",\n ' openGraph: { locale: \"fr_FR\", alternateLocale: [\"en_US\", \"es_ES\"] },',\n \"};\",\n ].join(\"\\n\"),\n language: \"tsx\",\n },\n evaluate: (ex) => {\n const cluster = hreflangCluster(ex);\n const observed = ex.openGraph.locale?.value?.trim() ?? null;\n if (cluster.length < 2 || !hasOpenGraph(ex)) return { status: \"na\", observed };\n if (observed) return { status: \"pass\", observed };\n\n return {\n status: \"fail\",\n observed: null,\n message: `Page declares hreflang alternates (${cluster.join(\", \")}) but no \\`og:locale\\`; the protocol default \\`en_US\\` applies instead.`,\n origin: { kind: \"meta\", property: \"og:locale\" },\n };\n },\n};\n\nconst ogLocaleAlternates: BooleanRule = {\n id: \"og.locale.alternates\",\n kind: \"boolean\",\n category: \"opengraph\",\n severity: \"warning\",\n title: \"`og:locale:alternate` and the hreflang cluster must name the same locales\",\n why:\n \"Two vocabularies describe the same fact — this page exists in these \" +\n \"languages — and nothing in a build keeps them in step, so they drift \" +\n \"silently. Whichever one is short is the one telling a consumer that a \" +\n \"translation it could have linked to does not exist.\",\n rigor: \"vendor-spec\",\n sources: [\"ogp\", \"google-hreflang\"],\n reads: [\"openGraph.locale\", \"openGraph.localeAlternates\", \"links.hreflang\"],\n expected: \"one `og:locale:alternate` per hreflang locale other than this page's own\",\n relates: [\"og.locale.missing\", \"hreflang.missing\"],\n fix: {\n title: \"Derive both lists from the same cluster\",\n snippet: [\n \"// The hreflang map and the OG locales are one fact stated twice.\",\n \"openGraph: {\",\n \" locale: ogLocale(locale),\",\n \" alternateLocale: cluster.filter((l) => l !== locale).map(ogLocale),\",\n \"},\",\n \"alternates: { languages: cluster },\",\n ].join(\"\\n\"),\n language: \"ts\",\n },\n evaluate: (ex) => {\n const cluster = hreflangCluster(ex);\n const own = ex.openGraph.locale?.value?.trim();\n const declared = ex.openGraph.localeAlternates.map((a) => a.value.trim()).filter(Boolean);\n const observed = { locale: own ?? null, alternates: declared, hreflang: cluster };\n\n // With no `og:locale` there is no \"own locale\" to subtract from the\n // cluster, and `og.locale.missing` is already reporting the same page.\n if (cluster.length < 2 || !own) return { status: \"na\", observed };\n\n const missing = cluster.filter(\n (tag) => !sameLocale(tag, own) && !declared.some((d) => sameLocale(d, tag)),\n );\n const unbacked = declared.filter((d) => !cluster.some((tag) => sameLocale(tag, d)));\n if (missing.length === 0 && unbacked.length === 0) return { status: \"pass\", observed };\n\n const disagreements: string[] = [];\n if (missing.length > 0) {\n disagreements.push(`no \\`og:locale:alternate\\` for ${missing.join(\", \")}`);\n }\n if (unbacked.length > 0) {\n disagreements.push(`\\`og:locale:alternate\\` ${unbacked.join(\", \")} has no hreflang`);\n }\n\n return {\n status: \"fail\",\n observed,\n message: `Open Graph and hreflang disagree about this page's translations: ${disagreements.join(\"; \")}.`,\n origin: { kind: \"meta\", property: \"og:locale:alternate\" },\n };\n },\n};\n\nconst iconsMissing: BooleanRule = {\n id: \"icons.missing\",\n kind: \"boolean\",\n category: \"icons\",\n severity: \"warning\",\n title: \"Declare an icon — the tab, the bookmark and the share sheet all read one\",\n why:\n \"No specification requires an icon, which is exactly why nothing complains \" +\n \"and every consumer improvises. A browser falls back to `/favicon.ico` at \" +\n \"the root, a feed reader or a link unfurler often falls back to nothing, \" +\n \"and a bookmark to a site with no icon is a grey square among fifty.\",\n rigor: \"guideline\",\n sources: [\"whatwg-html-link-types\", \"mdn-link-rel\"],\n reads: [\"links.icons\", \"links.manifest\"],\n expected: 'at least one `<link rel=\"icon\">`, or icons declared by a manifest',\n relates: [\"icons.apple-touch.missing\", \"icons.manifest-mismatch\"],\n fix: {\n title: \"Let the file convention declare it\",\n snippet: [\n '// app/icon.svg (or icon.png) — Next emits the <link rel=\"icon\"> itself.',\n '// app/apple-icon.png likewise emits rel=\"apple-touch-icon\".',\n \"// A /favicon.ico at the root is a separate matter: see icons.ico.missing.\",\n ].join(\"\\n\"),\n language: \"tsx\",\n },\n evaluate: (ex) => {\n const head = declaredIcons(ex);\n const fromManifest = ex.links.manifest?.icons ?? [];\n const observed = {\n head: head.map((icon) => ({ rel: icon.rel, href: icon.href })),\n manifest: fromManifest.map((icon) => icon.src),\n };\n if (head.length > 0 || fromManifest.length > 0) return { status: \"pass\", observed };\n\n return {\n status: \"fail\",\n observed,\n message:\n 'Page declares no icon: no `<link rel=\"icon\">`, and no icons from a manifest. ' +\n \"Consumers fall back to `/favicon.ico` if the site happens to serve one.\",\n origin: { kind: \"link\", rel: \"icon\" },\n };\n },\n};\n\nconst iconsAppleTouchMissing: BooleanRule = {\n id: \"icons.apple-touch.missing\",\n kind: \"boolean\",\n category: \"icons\",\n severity: \"info\",\n title: \"Declare an `apple-touch-icon` for the iOS home screen\",\n why:\n 'iOS does not read `rel=\"icon\"` for a home-screen shortcut. With no ' +\n \"`apple-touch-icon` it screenshots the page instead, so what someone saved \" +\n \"is a thumbnail of whatever was on screen when they saved it.\",\n rigor: \"vendor-spec\",\n sources: [\"apple-web-apps\", \"whatwg-html-link-types\"],\n reads: [\"links.icons\", \"links.manifest\"],\n expected: 'a `<link rel=\"apple-touch-icon\">`',\n relates: [\"icons.missing\"],\n evaluate: (ex) => {\n const head = declaredIcons(ex);\n // A page that declares no icon at all is `icons.missing`'s to report;\n // asking it for the Apple flavour on top would be a second finding about\n // the same absence.\n if (head.length === 0) return { status: \"na\", observed: null };\n\n const apple = head.filter((icon) => APPLE_ICON_RELS.has(icon.rel.trim().toLowerCase()));\n const observed = head.map((icon) => icon.rel);\n if (apple.length > 0) return { status: \"pass\", observed };\n\n return {\n status: \"fail\",\n observed,\n message: `Page declares icons (${observed.join(\", \")}) but no \\`apple-touch-icon\\`; iOS will screenshot the page instead.`,\n origin: { kind: \"link\", rel: \"apple-touch-icon\" },\n };\n },\n};\n\nconst iconsManifestMismatch: BooleanRule = {\n id: \"icons.manifest-mismatch\",\n kind: \"boolean\",\n category: \"icons\",\n severity: \"info\",\n title: \"The manifest and the `<head>` must not disagree about the same icon\",\n why:\n \"Two files declare the icons, maintained by different hands, and nothing \" +\n \"compares them. The failure worth naming is not that the two lists differ \" +\n \"— they legitimately do, since an apple-touch-icon is not a manifest icon \" +\n \"— but that they describe the *same file* differently, or that the icons \" +\n \"exist only in the manifest, where a browser tab never looks.\",\n rigor: \"guideline\",\n sources: [\"w3c-appmanifest\", \"whatwg-html-link-types\"],\n reads: [\"links.icons\", \"links.manifest\"],\n expected: \"the same size and type wherever an icon is declared twice\",\n relates: [\"icons.missing\"],\n evaluate: (ex) => {\n const manifest = ex.links.manifest;\n // No manifest, none fetched, none parsed, or one that declares no icons:\n // nothing to compare. `parsed === undefined` is \"never looked at\", which\n // is not evidence about the site.\n if (!manifest?.parsed || !manifest.icons || manifest.icons.length === 0) {\n return { status: \"na\", observed: null };\n }\n\n const head = declaredIcons(ex);\n const observed = {\n head: head.map((icon) => ({ href: icon.href, sizes: icon.sizes ?? null })),\n manifest: manifest.icons.map((icon) => ({ src: icon.src, sizes: icon.sizes ?? null })),\n };\n\n if (head.length === 0) {\n return {\n status: \"fail\",\n observed,\n message: `Manifest declares ${manifest.icons.length} icon(s) but the \\`<head>\\` declares none; a browser tab reads the \\`<head>\\`, not the manifest.`,\n origin: { kind: \"link\", rel: \"manifest\" },\n };\n }\n\n // Same file, two declarations. Compare on the trailing path so a manifest\n // writing `/icon.png` and a `<link>` resolved to an absolute URL are still\n // recognised as one file.\n const disagreements: string[] = [];\n for (const icon of manifest.icons) {\n const twin = head.find((candidate) => sameAsset(candidate.href, icon.src));\n if (!twin) continue;\n if (icon.sizes && twin.sizes && normalise(icon.sizes) !== normalise(twin.sizes)) {\n disagreements.push(\n `\\`${icon.src}\\` is \\`${twin.sizes}\\` in the \\`<head>\\` and \\`${icon.sizes}\\` in the manifest`,\n );\n }\n if (icon.type && twin.type && icon.type.toLowerCase() !== twin.type.toLowerCase()) {\n disagreements.push(\n `\\`${icon.src}\\` is \\`${twin.type}\\` in the \\`<head>\\` and \\`${icon.type}\\` in the manifest`,\n );\n }\n }\n\n if (disagreements.length === 0) return { status: \"pass\", observed };\n return {\n status: \"fail\",\n observed,\n message: `The manifest and the \\`<head>\\` describe the same icon differently: ${disagreements.join(\"; \")}.`,\n origin: { kind: \"link\", rel: \"manifest\" },\n };\n },\n};\n\nconst ogImageReachable: BooleanRule = {\n id: \"og.image.reachable\",\n kind: \"boolean\",\n category: \"opengraph\",\n severity: \"error\",\n title: \"The URL in `og:image` has to answer with an image\",\n why:\n \"Every other check on a preview image judges the tag. This one judges the \" +\n \"file, and it is the only one that can catch the failure with no symptom: \" +\n \"a URL that is present, well-formed, absolute — and dead. The card renders \" +\n \"empty and nothing in the page says why. Found on this project's own \" +\n \"documentation, where eleven pages pointed at a route a redirect had been \" +\n \"swallowing since the day it was written.\",\n rigor: \"vendor-spec\",\n sources: [\"ogp\", \"meta-og-sharing\"],\n reads: [\"openGraph.images\", \"assets\"],\n expected: \"a 2xx image at every `og:image` URL\",\n relates: [\"og.image.missing\", \"og.image.absolute\"],\n evaluate: (ex) => {\n // No probe pass ran. Reporting an absence goflag never checked for would\n // be inventing a finding.\n if (!ex.assets) return { status: \"na\", observed: null };\n\n const probed = ex.openGraph.images\n .map((image) => ex.assets?.[image.url.value.trim()])\n .filter((probe): probe is NonNullable<typeof probe> => probe !== undefined);\n if (probed.length === 0) return { status: \"na\", observed: null };\n\n const observed = probed.map((probe) => ({\n status: probe.status,\n contentType: probe.contentType ?? null,\n }));\n const dead = probed.filter((probe) => !probe.ok);\n if (dead.length === 0) return { status: \"pass\", observed };\n\n return {\n status: \"fail\",\n observed,\n message: `\\`og:image\\` does not serve an image: ${dead\n .map((probe) => describeAsset(probe.status, probe.contentType))\n .join(\"; \")}. The preview card will render without it.`,\n origin: { kind: \"meta\", property: \"og:image\" },\n };\n },\n};\n\nconst ogImageSizesMismatch: BooleanRule = {\n id: \"og.image.sizes-mismatch\",\n kind: \"boolean\",\n category: \"opengraph\",\n severity: \"warning\",\n title: \"`og:image:width` and `og:image:height` must be the image's real size\",\n why:\n \"The declared size is what a crawler lays the card out with before it has \" +\n \"the file, which is the whole reason `og.image.dimensions` asks for it. A \" +\n \"wrong one is therefore worse than none: the crawler reserves a shape the \" +\n \"image does not have, and the card is letterboxed or cropped on the first \" +\n \"share — the one that matters most.\\n\\n\" +\n \"It is also the declaration that blinds every other check. `og.image.ratio` \" +\n \"reads these two numbers and refuses to fetch, so an invented 1200×630 \" +\n \"scores 1.9 and passes over an image that is square. Found on a site whose \" +\n \"cover art is 1024×1024 and one of whose covers is a 337-byte 1×1 \" +\n \"placeholder, all three declared 1200×630 by a library that had never \" +\n \"looked at them.\",\n rigor: \"guideline\",\n sources: [\"ogp\", \"meta-og-sharing\"],\n reads: [\"openGraph.images\", \"assets\"],\n expected: \"declared dimensions equal to the file's own\",\n relates: [\"og.image.dimensions\", \"og.image.ratio\", \"og.image.reachable\"],\n fix: {\n title: \"Measure the image, or let the file convention do it\",\n snippet: [\n \"// app/…/opengraph-image.tsx — `size` is the truth and the declaration at once\",\n \"export const size = { width: 1200, height: 630 };\",\n \"\",\n \"// Naming a file instead? Then the numbers are that file's, not a default.\",\n \"openGraph: { images: [{ url: cover.url, width: cover.width, height: cover.height }] }\",\n ].join(\"\\n\"),\n language: \"tsx\",\n },\n evaluate: (ex) => {\n // No probe pass ran. Comparing a declaration against nothing would be\n // inventing the half of the finding goflag did not measure.\n if (!ex.assets) return { status: \"na\", observed: null };\n\n const comparable = ex.openGraph.images\n .map((image) => ({ image, probe: ex.assets?.[image.url.value.trim()] }))\n .filter(\n (pair) =>\n // Both sides have to have said something. A format goflag does not\n // decode leaves `sizes` absent, and absent means unknown — which is\n // not a mismatch.\n Boolean(pair.probe?.ok && pair.probe.sizes?.length) &&\n pair.image.width?.value !== undefined &&\n pair.image.height?.value !== undefined,\n );\n if (comparable.length === 0) return { status: \"na\", observed: null };\n\n const observed = comparable.map(({ image, probe }) => ({\n url: image.url.value,\n declared: `${image.width!.value}x${image.height!.value}`,\n // The first frame. An `og:image` is one picture; a container carrying\n // several is an icon, and `icons.sizes-mismatch` is the rule for those.\n actual: `${probe!.sizes![0]!.width}x${probe!.sizes![0]!.height}`,\n }));\n\n const wrong = observed.filter((entry) => entry.declared !== entry.actual);\n if (wrong.length === 0) return { status: \"pass\", observed };\n\n return {\n status: \"fail\",\n observed,\n message: `The declared size is not the image's: ${wrong\n .map((entry) => `\\`${entry.url}\\` declares ${entry.declared} and is ${entry.actual}`)\n .join(\"; \")}.`,\n origin: { kind: \"meta\", property: \"og:image:width\" },\n };\n },\n};\n\nconst iconsUnreachable: BooleanRule = {\n id: \"icons.unreachable\",\n kind: \"boolean\",\n category: \"icons\",\n severity: \"warning\",\n title: \"A declared icon has to answer with an image\",\n why:\n \"An icon that 404s is worse than one never declared: the client asks, \" +\n \"gets nothing, and has already skipped the `/favicon.ico` it would have \" +\n \"fallen back to. The declaration is what took the fallback away.\",\n rigor: \"vendor-spec\",\n sources: [\"whatwg-html-link-types\", \"mdn-link-rel\"],\n reads: [\"links.icons\", \"assets\"],\n expected: \"a 2xx image at every declared icon URL\",\n relates: [\"icons.missing\", \"icons.ico.missing\"],\n evaluate: (ex) => {\n if (!ex.assets) return { status: \"na\", observed: null };\n\n const probed = declaredIcons(ex)\n .map((icon) => ({ icon, probe: ex.assets?.[icon.href] }))\n .filter((pair): pair is { icon: ExtractionIcon; probe: ExtractionAsset } =>\n Boolean(pair.probe),\n );\n if (probed.length === 0) return { status: \"na\", observed: null };\n\n const observed = probed.map(({ icon, probe }) => ({\n rel: icon.rel,\n href: icon.href,\n status: probe.status,\n }));\n const dead = probed.filter(({ probe }) => !probe.ok);\n if (dead.length === 0) return { status: \"pass\", observed };\n\n return {\n status: \"fail\",\n observed,\n message: `Declared icon does not serve an image: ${dead\n .map(\n ({ icon, probe }) =>\n `\\`${icon.rel}\\` → \\`${icon.href}\\` (${describeAsset(probe.status, probe.contentType)})`,\n )\n .join(\"; \")}.`,\n origin: { kind: \"link\", rel: \"icon\" },\n };\n },\n};\n\nconst iconsSizesMismatch: BooleanRule = {\n id: \"icons.sizes-mismatch\",\n kind: \"boolean\",\n category: \"icons\",\n severity: \"info\",\n title: \"A `sizes` attribute must describe the file it points at\",\n why:\n \"`sizes` is how a client picks one icon out of several without fetching \" +\n \"them all, so a wrong value costs exactly what the attribute was there to \" +\n \"save: the client downloads the file it was told to prefer and gets the \" +\n \"wrong resolution. A half-true declaration is the common shape — a `.ico` \" +\n \"carrying 16, 32 and 48 declared as `48x48` advertises one third of itself.\",\n rigor: \"guideline\",\n sources: [\"whatwg-html-link-types\", \"mdn-link-rel\"],\n reads: [\"links.icons\", \"assets\"],\n expected: \"`sizes` listing what the file actually contains\",\n relates: [\"icons.unreachable\"],\n evaluate: (ex) => {\n if (!ex.assets) return { status: \"na\", observed: null };\n\n const comparable = declaredIcons(ex)\n .map((icon) => ({ icon, probe: ex.assets?.[icon.href] }))\n .filter(\n (pair): pair is { icon: ExtractionIcon; probe: ExtractionAsset } =>\n // Only where both sides said something. `any` declares no dimension\n // to check, and a format goflag does not decode leaves `sizes`\n // absent — which means unknown, and unknown is not a mismatch.\n Boolean(pair.probe?.ok && pair.probe.sizes?.length) &&\n pair.icon.parsedSizes.some((size) => size !== \"any\"),\n );\n if (comparable.length === 0) return { status: \"na\", observed: null };\n\n const observed = comparable.map(({ icon, probe }) => ({\n href: icon.href,\n declared: icon.sizes ?? null,\n actual: (probe.sizes ?? []).map((size) => `${size.width}x${size.height}`),\n }));\n\n const wrong = observed.filter((entry) => {\n const declared = new Set(\n comparable\n .find((pair) => pair.icon.href === entry.href)!\n .icon.parsedSizes.filter((size) => size !== \"any\")\n .map((size) => `${size.width}x${size.height}`),\n );\n const actual = new Set(entry.actual);\n // Set equality: a declaration that omits sizes the file carries is as\n // wrong as one that claims sizes it does not.\n return declared.size !== actual.size || [...declared].some((size) => !actual.has(size));\n });\n\n if (wrong.length === 0) return { status: \"pass\", observed };\n return {\n status: \"fail\",\n observed,\n message: `\\`sizes\\` does not describe the file: ${wrong\n .map(\n (entry) =>\n `\\`${entry.href}\\` declares \\`${entry.declared}\\` and contains ${entry.actual.join(\", \")}`,\n )\n .join(\"; \")}.`,\n origin: { kind: \"link\", rel: \"icon\" },\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 iconsAppleTouchMissing,\n iconsManifestMismatch,\n iconsMissing,\n iconsSizesMismatch,\n iconsUnreachable,\n ogDescriptionMissing,\n ogImageAbsolute,\n ogImageAlt,\n ogImageAltCaption,\n ogImageDimensions,\n ogImageMissing,\n ogImageRatio,\n ogImageReachable,\n ogImageSizesMismatch,\n ogLocaleAlternates,\n ogLocaleMissing,\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 entry of a Web App Manifest's `icons` member. */\nexport interface ExtractionManifestIcon {\n src: string;\n sizes?: string;\n type?: string;\n purpose?: string;\n /** Parsed `sizes` entries; empty for absent or unparseable values. */\n parsedSizes: Array<{ width: number; height: number } | \"any\">;\n}\n\n/**\n * The `<link rel=\"manifest\">`, and what the manifest said when goflag fetched\n * it.\n *\n * `parsed` is deliberately three-valued. `undefined` means no probe ran — the\n * manifest was never looked at — while `false` means it was fetched and could\n * not be read. A rule must be able to tell those apart from a manifest that\n * parsed and declares no icons, because only the last of the three is evidence\n * about the site.\n */\nexport interface ExtractionManifest {\n href: string;\n parsed?: boolean;\n /** Present only when `parsed` is true. */\n icons?: ExtractionManifestIcon[];\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?: ExtractionManifest;\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/** One intrinsic size a file declares about itself. */\nexport interface ExtractionAssetSize {\n width: number;\n height: number;\n}\n\n/**\n * What was served at one asset URL the page declared.\n *\n * The rules stay pure functions of this model: the network happened in a\n * dedicated pass (`docs/og-plan.md` D8) and its answers arrive here as data,\n * exactly like the manifest's.\n */\nexport interface ExtractionAsset {\n status: number;\n /** 2xx *and* an image content type. */\n ok: boolean;\n contentType?: string;\n /**\n * Sizes decoded from the file's header — several for an ICO. Absent for a\n * format goflag does not decode, which means *unknown*, never *none*.\n */\n sizes?: ExtractionAssetSize[];\n}\n\n/** One tag, named the way the page named it. */\nexport interface ExtractionHydratedMeta {\n name?: string;\n property?: string;\n httpEquiv?: string;\n content?: string;\n}\n\n/** One link, named the way the page named it. */\nexport interface ExtractionHydratedLink {\n rel: string;\n href?: string;\n hreflang?: string;\n}\n\n/**\n * What client JavaScript added to, or took from, the `<head>`.\n *\n * The gap between the two passes, at tag granularity — not per `Fact`, which\n * carries which *tag* produced a value and never which *pass*. A value present\n * here is a value the browser shows and a non-JS crawler never sees: the\n * failure mode every `og.*` rule is blind to, because each of them judges the\n * declaration it is given and this is about which declaration that was.\n *\n * **Absent on most runs, and that is not \"nothing changed\".** The delta only\n * exists where both bodies do: the auto-escalation path, where a static fetch\n * looked empty and a headless pass followed. A `--static` run never renders,\n * and an explicit `--headless` run keeps no static body to compare against.\n */\nexport interface ExtractionHydration {\n titleChanged: boolean;\n htmlLangChanged: boolean;\n injectedMetas: ExtractionHydratedMeta[];\n removedMetas: ExtractionHydratedMeta[];\n injectedLinks: ExtractionHydratedLink[];\n removedLinks: ExtractionHydratedLink[];\n /** JSON-LD blocks that exist only after hydration. */\n jsonLdBlocksAdded: number;\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 * What was served at each asset URL this page declared, keyed by resolved\n * URL. **Absent when no probe pass ran** — a rule must read that as \"not\n * looked at\" rather than \"nothing there\", the same three-valued care the\n * manifest gets.\n */\n assets?: Record<string, ExtractionAsset>;\n /**\n * What hydration changed in the `<head>`, when both passes ran and can be\n * compared. Absent means \"not established\" — never \"nothing moved\".\n */\n hydration?: ExtractionHydration;\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). 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 * One probe crosses over: the Web App Manifest. It is fetched per page,\n * because the page is what declares it, and its icons are a declaration about\n * this page's `<head>` rather than about the site. Its payload arrives as\n * `unknown` from the network, so it is read defensively here — that is the\n * price of letting a rule stay a pure function of the extraction.\n *\n * The hydration delta crosses over too, renamed and trimmed. It was left\n * behind here until `goflag preview` had a use for it: it is the one fact in\n * `Page` that no rule can reach and no other section of the report carries —\n * which tag the browser shows and the crawler does not.\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 { parseSizes } from \"../../core/extract/static\";\nimport type { Page } from \"../../core/types\";\nimport type {\n Extraction,\n ExtractionDocument,\n ExtractionLinks,\n ExtractionManifest,\n ExtractionManifestIcon,\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 optionalString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value : undefined;\n}\n\n/**\n * The `icons` member of a fetched manifest, read out of `unknown`.\n *\n * An entry without a usable `src` is dropped rather than repaired: the\n * manifest spec makes `src` the only required member of an icon, so an entry\n * missing it names nothing a rule could judge or a browser could fetch.\n */\nfunction manifestIcons(data: unknown): ExtractionManifestIcon[] {\n if (typeof data !== \"object\" || data === null) return [];\n const raw = (data as { icons?: unknown }).icons;\n if (!Array.isArray(raw)) return [];\n\n const icons: ExtractionManifestIcon[] = [];\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const { src, sizes, type, purpose } = entry as Record<string, unknown>;\n const source = optionalString(src);\n if (!source) continue;\n\n icons.push({\n src: source,\n sizes: optionalString(sizes),\n type: optionalString(type),\n purpose: optionalString(purpose),\n parsedSizes: parseSizes(optionalString(sizes)),\n });\n }\n return icons;\n}\n\nfunction manifestFromPage(page: Page): ExtractionManifest | undefined {\n const link = page.links.manifest;\n if (!link) return undefined;\n\n const probe = page.probes.manifest;\n // No probe ran, so nothing is known beyond the declaration itself. Saying\n // `parsed: false` here would claim goflag looked and failed.\n if (!probe) return { href: link.href };\n if (!probe.found || probe.parseError !== undefined) {\n return { href: link.href, parsed: false };\n }\n\n return { href: link.href, parsed: true, icons: manifestIcons(probe.data) };\n}\n\nfunction linksFromPage(page: Page): ExtractionLinks {\n return {\n hreflang: page.links.alternates,\n icons: page.links.icons,\n manifest: manifestFromPage(page),\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 ...(page.assets ? { assets: page.assets } : {}),\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 ...(page.hydration\n ? {\n hydration: {\n titleChanged: page.hydration.titleChanged,\n htmlLangChanged: page.hydration.htmlLangChanged,\n injectedMetas: page.hydration.clientInjectedMetas,\n removedMetas: page.hydration.clientRemovedMetas,\n injectedLinks: page.hydration.clientInjectedLinks,\n removedLinks: page.hydration.clientRemovedLinks,\n jsonLdBlocksAdded: page.hydration.jsonLdBlocksAdded,\n },\n }\n : {}),\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 * Language tags, judged as an auditor judges them.\n *\n * `locale.invalid` used to be a shape check — two or three letters, optionally\n * a region — and it was wrong in both directions at once (plan §B.5):\n *\n * isValidLocale(\"qq\") → true invented language, reported as fine\n * isValidLocale(\"pt-ZZ\") → true \"unknown region\", reported as fine\n * isValidLocale(\"zh-Hant\") → false real tag, reported as a defect\n *\n * The second column is the one that matters. A rule named `locale.invalid`\n * that accepts an invented tag does not keep its promise, and one that rejects\n * every Chinese or Serbian site is the failure an auditor can least afford.\n *\n * The data comes from ICU, which is in Node — no table, no dependency. It is\n * the same source `@goflag/next` reads, deliberately through a separate\n * implementation: the plan (§B.1) keeps the producer and the auditor as two\n * witnesses, because a shared validator would make their agreement prove\n * nothing. Their severities differ and are meant to. The library refuses a\n * script subtag it has no use for; the auditor accepts `zh-Hant`, because a\n * site is entitled to serve it.\n */\n\nconst languageNames = new Intl.DisplayNames([\"en\"], { type: \"language\", fallback: \"none\" });\nconst regionNames = new Intl.DisplayNames([\"en\"], { type: \"region\", fallback: \"none\" });\nconst scriptNames = new Intl.DisplayNames([\"en\"], { type: \"script\", fallback: \"none\" });\n\n/**\n * Codes ICU knows and an hreflang value must still not carry.\n *\n * `ZZ` is a real CLDR code *meaning* unknown, so an existence check that\n * trusted ICU alone would accept `pt-ZZ` — the exact tag this rule exists to\n * catch. `XA` and `XB` are ICU's pseudo-locales for testing. The macro-regions\n * stay: `es-419` is documented by Google and serves real audiences.\n */\nconst NOT_AN_AUDIENCE_REGION = new Set([\"ZZ\", \"XA\", \"XB\"]);\n\n/** Language codes that name the absence of a language rather than one. */\nconst NOT_AN_AUDIENCE_LANGUAGE = new Set([\"mul\", \"zxx\", \"und\", \"mis\"]);\n\n/** `Zzzz` is CLDR's \"unknown script\", the script-level equivalent of `ZZ`. */\nconst NOT_A_SCRIPT = new Set([\"Zzzz\"]);\n\n/**\n * Whether this runtime can tell a real tag from an invented one.\n *\n * Node has shipped full ICU by default since v13, but a `small-icu` build\n * carries English alone and answers `undefined` for languages that exist. The\n * library throws on such a runtime; an auditor must not. Judging somebody\n * else's site, the worst outcome is a page full of `locale.invalid` findings\n * that are all false, so the probe degrades to the old shape check instead —\n * and says so, once, in `diagnostics.warnings`, because a check that quietly\n * stopped checking is what the coverage and unreachable-page work already\n * taught this codebase not to ship.\n */\nexport const ICU_KNOWS_LANGUAGES: boolean =\n languageNames.of(\"fr\") !== undefined &&\n languageNames.of(\"qq\") === undefined &&\n regionNames.of(\"BR\") !== undefined;\n\n/** The message `diagnostics.warnings` carries when the probe is unavailable. */\nexport const ICU_UNAVAILABLE_WARNING =\n \"This Node build cannot tell a real language tag from an invented one \" +\n \"(small-icu). hreflang tags were checked for shape only, so `locale.invalid` \" +\n \"may have missed tags that do not exist.\";\n\n/** Shape only: what the rule checked before ICU, and its fallback since. */\nconst BCP47_SHAPE = /^[a-z]{2,3}(-[a-z]{2}|-\\d{3})?$/i;\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_SHAPE.test(segment);\n}\n\n/**\n * True when `tag` is a language tag a site may legitimately declare.\n *\n * Structure is answered by `Intl.getCanonicalLocales`, which rejects `pt_BR`\n * and `en--US` — the malformations that actually appear in the wild — and\n * accepts variants and extensions, which are valid and none of this rule's\n * business. Existence is then checked subtag by subtag: an unknown language,\n * region or script is what makes a well-formed tag a dead one.\n */\nexport function isValidLocale(tag: string): boolean {\n if (tag === \"x-default\") return true;\n if (!ICU_KNOWS_LANGUAGES) return BCP47_SHAPE.test(tag);\n\n let canonical: string;\n try {\n canonical = Intl.getCanonicalLocales(tag)[0] ?? \"\";\n } catch {\n return false;\n }\n if (!canonical) return false;\n\n let parts: Intl.Locale;\n try {\n parts = new Intl.Locale(canonical);\n } catch {\n return false;\n }\n\n // `language` is typed as a string but is absent on a tag that carries no\n // primary subtag — `und`, and the grandfathered forms. A tag naming no\n // language names no audience.\n const language = (parts.language ?? \"\").toLowerCase();\n if (!language) return false;\n if (NOT_AN_AUDIENCE_LANGUAGE.has(language)) return false;\n if (languageNames.of(language) === undefined) return false;\n\n if (parts.script) {\n if (NOT_A_SCRIPT.has(parts.script)) return false;\n if (scriptNames.of(parts.script) === undefined) return false;\n }\n\n if (parts.region) {\n if (NOT_AN_AUDIENCE_REGION.has(parts.region)) return false;\n if (regionNames.of(parts.region) === undefined) return false;\n }\n\n return true;\n}\n\n/**\n * True when the primary subtag names a language ICU knows.\n *\n * Used for the candidate evidence behind a locale axis goflag refuses to\n * guess, where the question is \"could this path segment plausibly be a\n * language?\" rather than \"is this tag valid?\". It replaces a hand-written\n * ISO 639-1 list, which the plan (§B.5) noted becomes deletable the moment the\n * probe exists — and which was stale by construction, since it could only ever\n * be as current as the day somebody typed it.\n */\nexport function isKnownLanguageTag(tag: string): boolean {\n const primary = (tag.toLowerCase().split(\"-\")[0] ?? \"\").trim();\n if (!primary) return false;\n if (NOT_AN_AUDIENCE_LANGUAGE.has(primary)) return false;\n if (!ICU_KNOWS_LANGUAGES) return BCP47_SHAPE.test(primary);\n return languageNames.of(primary) !== undefined;\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 is decided in `./bcp47`, by ICU rather than by a\n * shape: a tag whose language, script or region does not exist flags as\n * `locale.invalid`, and `x-default` is always accepted.\n */\n\nimport { isValidLocale, looksLikeLocaleSegment } from \"./bcp47\";\nimport type { Page } from \"./types\";\n\n/**\n * The codes `reciprocityIssues` emits — all three of them.\n *\n * `self-mismatch` used to sit here too, declared and unreachable: no branch\n * ever produced it, so the site's rule catalogue advertised a check goflag\n * could not perform, and a reader took silence for a pass. The declaration is\n * gone rather than the check implemented, because a page whose self-reference\n * points elsewhere is already caught as a `missing-back-link` on the peer it\n * names. If it is ever worth its own code, it comes back with the branch that\n * emits it.\n */\nexport type ReciprocityCode = \"missing-back-link\" | \"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\n/**\n * What the audit actually knows about the URL in a cell.\n *\n * - `crawled` — fetched and inspected. The only one that is a fact.\n * - `sitemap` — the site's own `<loc>` list names it, but the crawl did not\n * reach it. Under structural coverage this is most of the grid.\n * - `alternate` — **no list names it**: the only evidence is a\n * `<link rel=\"alternate\">` on some page. It still fills the cell, so it\n * still hides a hole, and nothing has checked that the page is there.\n *\n * Derived from the winning URL rather than from which writer got there first,\n * so `record`'s ordering — and every fingerprint that depends on it — is\n * untouched (`docs/i18n-cluster-plan.md` §10).\n */\nexport type I18nCellSource = \"crawled\" | \"sitemap\" | \"alternate\";\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 /** How the URL got here. Absent when `url` is null — nothing to source. */\n source?: I18nCellSource;\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/**\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 — validation has always been case-insensitive — 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/**\n * Tag validity lives in `./bcp47`, which asks ICU rather than a regex. Both\n * are re-exported here because this module is where the matrix and the\n * reciprocity walk consume them, and the call sites read better for it.\n *\n * They answer two different questions, and the split is the point:\n * `looksLikeLocaleSegment` judges a URL segment on shape alone — `/de/` and\n * `/api/` are indistinguishable to a crawler, and the axis is what decides\n * between them — while `isValidLocale` judges a tag a site actually declared,\n * where an invented language is the defect being looked for.\n */\nexport { isValidLocale, looksLikeLocaleSegment };\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 * Which row a URL belongs to, when the site declared it.\n *\n * Built from the sitemap's own `xhtml:link` cluster declarations\n * (`./clusters`). It overrides the pathname-derived route and nothing else:\n * it never adds a locale, never fills a cell, and never says a page exists.\n * A site that declares no cluster — or one without `x-default` — keeps the\n * pathname route and today's behaviour exactly.\n */\n clusterRouteOf?: (url: string) => string | undefined;\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; source: I18nCellSource };\n const grid = new Map<string, Map<string, Slot>>();\n const locales = new Set<string>();\n const routes = new Set<string>();\n // The site's own URL list, for telling \"the sitemap says this page exists\"\n // from \"only an `hreflang` on some page says so\".\n const declaredSet = new Set(options.declaredUrls ?? []);\n\n for (const locale of options.locales ?? []) {\n const tag = locale.trim().toLowerCase();\n if (tag) locales.add(tag);\n }\n\n /**\n * The row a URL sits in: the cluster the site declared, when it declared\n * one, and the pathname-derived route otherwise. This is the only place the\n * cluster index is consulted — it moves a cell between rows and never\n * creates one.\n */\n function rowOf(url: string, pathnameRoute: string): string {\n return options.clusterRouteOf?.(url) ?? pathnameRoute;\n }\n\n /**\n * What we know about a URL, read off the URL itself.\n *\n * Deliberately not \"who wrote the cell first\": `record` keeps the first\n * writer and the writers run in a fixed order, so an alternate can win a\n * cell whose URL the sitemap also lists. Sourcing by writer would then call\n * that cell unverified when the site's own list vouches for it.\n */\n function sourceOf(url: string): I18nCellSource {\n if (inspectedByUrl.has(url)) return \"crawled\";\n return declaredSet.has(url) ? \"sitemap\" : \"alternate\";\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), source: sourceOf(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(rowOf(page.fetch.finalUrl, 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 // The declared locale is trusted; the row is the declared cluster when\n // there is one, and the alternate's own path otherwise — which is what\n // splits a slug-translating pair into two rows when nothing declared it.\n record(rowOf(altUrl.toString(), 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(rowOf(url.toString(), 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, source: slot.source }\n : { url: null, inspected: false };\n }\n }\n return { locales: sortedLocales, routes: sortedRoutes, cells };\n}\n\n/**\n * Cells whose only evidence is an `hreflang` on some page: not crawled, and\n * not in the site's own URL list either.\n *\n * These are the ones that can hide a real gap. A cell is filled by the\n * declaration, `deriveTranslationHoles` reads `cell.url` and sees a translation\n * — and nothing ever checked that the page is served. Measured on the\n * `tancrede` snapshot: six alternate targets absent from thirty `<loc>`, so six\n * true holes invisible (`docs/i18n-cluster-plan.md` §10.1).\n *\n * This **counts**; it does not judge. Refusing to believe an unlisted alternate\n * would invent holes on every site using `sitemap: false`, a deliberate and\n * supported `@goflag/next` feature (`docs/sitemap-scope-plan.md`) — trading a\n * false negative for a false positive, in the direction this tool forbids. The\n * number exists so the next decision can be made on a measurement rather than\n * on an argument.\n */\nexport function countUnverifiedAlternates(matrix: I18nMatrix): number {\n let count = 0;\n for (const route of matrix.routes) {\n for (const locale of matrix.locales) {\n if (matrix.cells[route]?.[locale]?.source === \"alternate\") count++;\n }\n }\n return count;\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 page advertises 2+ locales and no `x-default`, emit\n * `x-default-missing` — once per page, not once per cluster.\n * - When a locale tag names a language, script or region that does not\n * exist, 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 * The RFC 9309 matcher: does this robots.txt allow this path?\n *\n * A pure function over a parsed file, shared by every rule that asks the\n * question — `robots.blocks-page` today, `sitemap.entry.blocked-by-robots`\n * when the sitemap half of `docs/sitemap-robots-plan.md` lands, and the\n * crawler itself if it ever learns to obey what it now fully reads.\n *\n * The semantics are small in number and easy to get subtly wrong, so each has\n * a dedicated test in `./match.test.ts`:\n *\n * | Behaviour | RFC 9309 |\n * | ----------------- | ------------------------------------------------------- |\n * | Group selection | most specific matching `User-agent`, else `*` |\n * | Rule precedence | longest match by octets wins; a tie goes to `allow` |\n * | Wildcards | `*` any sequence, `$` end anchor (§2.2.3) |\n * | Percent-encoding | compare octets — encoded and bare forms are equivalent |\n * | Case | paths are case-sensitive, user-agents are not |\n * | Empty `Disallow:` | allows everything, and is not a defect |\n *\n * The one judgement call: **an empty pattern never matches.** RFC 9309 gives\n * `Disallow:` with no value the meaning \"allow everything\", which is the same\n * outcome as the rule not existing — so it is dropped rather than treated as a\n * zero-length prefix that would match every path and, being length 0, always\n * lose anyway.\n */\n\nimport type { RobotsGroup, RobotsRule } from \"../types\";\n\nexport interface RobotsDecision {\n allowed: boolean;\n /** The rule that decided, absent when nothing matched (allow by default). */\n rule?: RobotsRule;\n /** Which group's rules were consulted; `*` or a product token. */\n group?: string;\n}\n\n/**\n * Decide whether `path` may be fetched by `userAgent`.\n *\n * `path` is the URL's path plus query — what the protocol matches against —\n * and callers pass it already in the form the server would see.\n */\nexport function robotsAllows(\n groups: readonly RobotsGroup[],\n path: string,\n userAgent = \"*\",\n): RobotsDecision {\n const group = selectGroup(groups, userAgent);\n // No group applies: nothing forbids anything. §2.2.1 — a file that names\n // only `Googlebot` says nothing at all about anyone else.\n if (!group) return { allowed: true };\n\n const token = group.userAgents[0]?.value ?? \"*\";\n\n let best: RobotsRule | undefined;\n let bestLength = -1;\n for (const rule of group.rules) {\n if (rule.pattern.length === 0) continue;\n if (!patternMatches(rule.pattern, path)) continue;\n\n const length = octetLength(rule.pattern);\n if (length > bestLength) {\n best = rule;\n bestLength = length;\n continue;\n }\n // §2.2.2: equal length is a tie, and `allow` wins it.\n if (length === bestLength && rule.kind === \"allow\") best = rule;\n }\n\n if (!best) return { allowed: true, group: token };\n return { allowed: best.kind === \"allow\", rule: best, group: token };\n}\n\n/**\n * The group whose rules apply: the most specific matching `User-agent`,\n * falling back to `*`.\n *\n * \"Most specific\" is the longest matching product token — a file with both\n * `Googlebot` and `Googlebot-Image` says different things to each. Duplicate\n * groups for one token are merged, because a file that names the same agent\n * twice means the union and crawlers read it that way.\n */\nfunction selectGroup(groups: readonly RobotsGroup[], userAgent: string): RobotsGroup | undefined {\n const wanted = userAgent.trim().toLowerCase();\n\n let bestToken: string | undefined;\n for (const group of groups) {\n for (const agent of group.userAgents) {\n if (agent.value === \"*\") continue;\n // A product token matches when it is a prefix of the crawler's name:\n // `Googlebot` answers for `Googlebot/2.1`.\n if (!wanted.startsWith(agent.value)) continue;\n if (bestToken === undefined || agent.value.length > bestToken.length) {\n bestToken = agent.value;\n }\n }\n }\n\n const token = bestToken ?? \"*\";\n const matching = groups.filter((group) =>\n group.userAgents.some((agent) => agent.value === token),\n );\n if (matching.length === 0) return undefined;\n if (matching.length === 1) return matching[0];\n\n return {\n userAgents: matching.flatMap((group) => group.userAgents),\n rules: matching.flatMap((group) => group.rules),\n };\n}\n\n/**\n * Whether a pattern matches a path, with `*` and `$`.\n *\n * Built as a regular expression over the **normalized** forms of both, so\n * `/caf%C3%A9` and `/café` are one path written two ways. Everything that is\n * not a wildcard is escaped, so a pattern containing `.` or `+` means those\n * characters and not their regex meanings.\n */\nfunction patternMatches(pattern: string, path: string): boolean {\n const anchored = pattern.endsWith(\"$\");\n const body = anchored ? pattern.slice(0, -1) : pattern;\n\n const source = body\n .split(\"*\")\n .map((literal) => escapeRegExp(normalizeOctets(literal)))\n .join(\".*\");\n\n return new RegExp(`^${source}${anchored ? \"$\" : \"\"}`).test(normalizeOctets(path));\n}\n\n/**\n * Decode percent-escapes so two spellings of one path compare equal.\n *\n * RFC 9309 says to compare octets, and a server that serves `/café` serves it\n * for `/caf%C3%A9` too. Undecodable input is returned as it came: a malformed\n * escape is not a reason to refuse an answer.\n */\nfunction normalizeOctets(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\n/** Pattern length in octets, which is what §2.2.2 compares. */\nfunction octetLength(pattern: string): number {\n return Buffer.byteLength(normalizeOctets(pattern), \"utf8\");\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\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 { robotsAllows } from \"../core/robots/match\";\nimport type { SitemapDocument } from \"../core/sitemap/types\";\nimport type { Page, SitemapEntryProbe } 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/**\n * The row a URL sits in: the cluster the site declared, when it declared one,\n * and the pathname-derived route otherwise.\n *\n * Both sides of `hreflang.sitemap-mismatch` have to agree on what \"the same\n * route\" means, and `splitRoute` alone answers with the path. On a site that\n * translates its slugs that is the wrong answer twice over: `/fr/tarifs`\n * yields route `/tarifs`, where the sitemap lists only `fr`, while its English\n * twin sits under `/pricing` — so a correct pair reads as two half-covered\n * routes and earns two warnings. Consulting the declared cluster first is the\n * same move `buildI18nMatrix` already makes (`../core/i18n.ts`); it only ever\n * moves a URL into a row, never invents one, so a site that declares no\n * cluster is byte-for-byte unaffected.\n */\nfunction rowOf(site: SiteContext, url: string, pathname: string): string {\n return site.clusterRouteOf?.(url) ?? splitRoute(pathname).route;\n}\n\n/**\n * Route → locales the sitemap lists a URL for, counting only locales the site\n * actually serves.\n */\nfunction sitemapLocalesByRoute(site: SiteContext): Map<string, Set<string>> {\n const byRoute = new Map<string, Set<string>>();\n const axis = new Set(site.localeAxis.locales.map((l) => l.toLowerCase()));\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 { locale } = splitRoute(pathname);\n if (locale === \"x-default\") continue;\n // The axis is what decides. `splitRoute` reads a segment by **shape\n // alone** — `bcp47.ts` says so in as many words, and `/de/` and `/api/`\n // are indistinguishable to it. `api`, `doc` and `www` all pass the\n // two-or-three-letter test, so a multilingual site with a `/doc/` section\n // was handing these rules a locale named `doc` and being told to publish\n // an `hreflang=\"doc\"` alternate for it.\n if (!axis.has(locale.toLowerCase())) continue;\n const route = rowOf(site, entry.loc, pathname);\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 *\n * ## Why `vendor-spec`, and why two sources\n *\n * The obligation and the mechanism come from different documents, and the rule\n * cites both because an agent reading only one would draw the wrong conclusion.\n *\n * WHATWG defines the **mechanism** normatively: `rel=\"alternate\"` with an\n * `hreflang` attribute designates a translation. What it does not do is require\n * anyone to emit one. No web standard says a multilingual site must declare its\n * alternates; that requirement belongs to Google alone, which is why the rule\n * claims `vendor-spec` and not `spec-required`, and why it stays silent on a\n * single-locale site where the obligation has no subject.\n *\n * Google's document is unambiguous about it, re-read 2026-08-15: *\"Each language\n * version must list itself as well as all other language versions\"*, and *\"If two\n * pages don't both point to each other, the tags will be ignored.\"*\n *\n * ## Why `error` under a merely vendor-spec requirement\n *\n * Rigor and severity are different axes, and this rule is where the difference\n * shows. Rigor says how authoritative the requirement is — here, one vendor's.\n * Severity says how bad the consequence is, and the consequence is total: the\n * quoted sentence means a broken cluster is not degraded but **discarded**, so a\n * site that half-declares its alternates gets exactly what a site declaring none\n * gets. There is no partial credit to warn about.\n */\nconst hreflangMissing: SiteRule = {\n id: \"hreflang.missing\",\n severity: \"error\",\n summary: \"Pages on a multilingual site must advertise their locale alternates\",\n rigor: \"vendor-spec\",\n sources: [\"google-hreflang\", \"whatwg-html-link-types\"],\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/** One route where the sitemap publishes a locale the `<head>` leaves out. */\ninterface CoverageGap {\n page: Page;\n route: string;\n /** Locales the sitemap lists that the `<head>` does not advertise. */\n onlyInSitemap: string[];\n}\n\n/**\n * Locales the sitemap publishes and the `<head>` does not name, per route.\n *\n * This computed the opposite direction too until the unsourceable half became a\n * question (`./site-prose.ts`), and kept computing it for a day after nothing\n * read it. Dropped rather than left in: a field nobody reads is the shape every\n * dead-signal entry in this repository's plans started as.\n *\n * Pages with no alternates at all are skipped: that is `hreflang.missing`'s\n * finding, and reporting it twice would double-count one defect.\n */\nfunction coverageGaps(site: SiteContext): CoverageGap[] {\n const bySitemap = sitemapLocalesByRoute(site);\n const gaps: CoverageGap[] = [];\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 = rowOf(site, page.fetch.finalUrl, pathname);\n const inSitemap = bySitemap.get(route);\n if (!inSitemap || inSitemap.size === 0) continue;\n\n gaps.push({\n page,\n route,\n onlyInSitemap: sorted(new Set([...inSitemap].filter((l) => !head.has(l)))),\n });\n }\n\n return gaps;\n}\n\n/** One registry away from its twin question, and the same remedy answers both. */\nconst ONE_SOURCE_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} as const;\n\nconst bothApply = (site: SiteContext) =>\n isMultilingual(site) && (site.discovery?.urls.length ?? 0) > 0;\n\n/**\n * The site publishes a translation its own `<head>` does not advertise.\n *\n * The sitemap listing `/fr/x` is the site asserting that the French version\n * exists and should be indexed. If `/en/x` then names no `fr` alternate, that\n * version sits outside the cluster: the two pages compete instead of\n * consolidating, which is the whole failure `hreflang` exists to prevent.\n *\n * ## Why this half is sourceable and the other is not\n *\n * Google requires the set to be complete — *\"Each language version must list\n * itself as well as all other language versions\"* — and the sitemap is the\n * site's own evidence that the omitted version is real. That chain is what makes\n * this `vendor-spec`: a cited requirement plus a fact the site supplied, not an\n * inference about what a site probably meant.\n *\n * ## The hidden link in that chain, and where it actually broke\n *\n * \"The sitemap lists `/fr/x`\" only means \"a French version exists\" if something\n * established that `/fr/x` is a locale variant at all. `splitRoute` answers that\n * by **shape alone** — `bcp47.ts` is explicit that `/de/` and `/api/` are\n * indistinguishable to it and that the locale axis is what decides — and\n * `sitemapLocalesByRoute` was not consulting the axis. `api`, `doc` and `www`\n * all pass its two-or-three-letter test, so a multilingual site with a `/doc/`\n * section produced a locale named `doc`, and this rule told its owner to\n * publish an `hreflang=\"doc\"` alternate.\n *\n * That was the real defect behind the doubt this rule was raised under — not\n * the slug-translating case, where two paths simply form two rows and nothing\n * fires. The fix is in `sitemapLocalesByRoute`, one line, and it belongs there\n * rather than here: both halves read those rows, and only one of them was ever\n * going to be audited for it.\n *\n * `warning` and not `error`, unlike `hreflang.missing`: a partial cluster still\n * consolidates the versions it does list, so the damage is bounded to the ones\n * left out. There is no cluster at all in the `error` case.\n */\nconst hreflangClusterIncomplete: SiteRule = {\n id: \"hreflang.cluster-incomplete\",\n severity: \"warning\",\n summary: \"Every locale the sitemap publishes must appear in the route's `hreflang` cluster\",\n rigor: \"vendor-spec\",\n sources: [\"google-hreflang\"],\n appliesTo: bothApply,\n check: ({ site, issue }) =>\n coverageGaps(site)\n .filter((gap) => gap.onlyInSitemap.length > 0)\n .map(({ page, route, onlyInSitemap }) =>\n issue({\n pageUrl: page.fetch.finalUrl,\n message:\n `Route \\`${route}\\`: the sitemap lists ${onlyInSitemap.join(\", \")} but the ` +\n `\\`<head>\\` does not advertise ${onlyInSitemap.length > 1 ? \"them\" : \"it\"}. ` +\n `The site publishes ${onlyInSitemap.length > 1 ? \"those versions\" : \"that version\"} ` +\n `and leaves ${onlyInSitemap.length > 1 ? \"them\" : \"it\"} outside the cluster, so ` +\n `${onlyInSitemap.length > 1 ? \"they compete\" : \"it competes\"} with this page instead ` +\n `of consolidating with it.`,\n origin: { kind: \"link\", rel: \"alternate\" },\n fix: ONE_SOURCE_FIX,\n }),\n ),\n};\n\n/**\n * `hreflang.sitemap-mismatch` used to sit here, and it is now a question rather\n * than a verdict — see `./site-prose.ts`.\n *\n * It carried `rigor: null` and `severity: warning` at the same time: a refusal\n * to say how authoritative a claim is, followed by the claim. Splitting it\n * sourced the half Google backs (`hreflang.cluster-incomplete`, above) and left\n * this one with nothing behind it, because nothing requires an\n * hreflang-declared page to appear in a sitemap. The disagreement is still real\n * and still worth surfacing — goflag simply cannot say which of the two\n * generators is wrong, which is the definition of a question.\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 *\n * The gate now asks the RFC 9309 matcher whether `/` is allowed, rather than\n * looking for a literal `Disallow: /` line. Same answer on the file that\n * prompted the rule, and a correct one on the files that spell it otherwise —\n * `Disallow: *`, or a `Disallow: /` that an `Allow:` further down takes back.\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 rigor: \"vendor-spec\",\n sources: [\"ietf-rfc9309\", \"google-robots-intro\"],\n appliesTo: (site) =>\n site.robots?.found === true && !robotsAllows(site.robots.groups, \"/\").allowed,\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/**\n * The fallback nobody declares and half the internet still requests.\n *\n * No specification requires `/favicon.ico`, which is why this is a guideline\n * and not a vendor-spec rule: modern browsers follow the `<link>` a page\n * declares and never touch the root. The clients that do are the naive ones —\n * feed readers, link unfurlers, crawlers written against 2005 — and they ask\n * the root blind, take what they get, and show nothing when it 404s.\n *\n * Site-level because the subject is one file at one path. Reported per page it\n * would be the same sentence five hundred times.\n */\nconst iconsIcoMissing: SiteRule = {\n id: \"icons.ico.missing\",\n severity: \"info\",\n summary: \"Serve a `/favicon.ico` at the root for the clients that ask blind\",\n rigor: \"guideline\",\n sources: [\"whatwg-html-link-types\", \"mdn-link-rel\"],\n // Only judged when the probe actually ran. No probe means goflag did not\n // look, and a rule that reported an absence it never checked for would be\n // inventing a finding.\n appliesTo: (site) => site.favicon !== undefined,\n check: ({ site, issue }) => {\n const probe = site.favicon;\n if (!probe || probe.found) return [];\n\n // Three distinguishable failures, and the remedy differs for each, so the\n // message says which one happened rather than \"no favicon\".\n const detail =\n probe.status === 0\n ? \"the request failed outright\"\n : probe.status >= 400\n ? `the origin answered ${probe.status}`\n : `the origin answered ${probe.status} with \\`${probe.contentType ?? \"no content type\"}\\`, which is not an image — a catch-all route serving the app shell looks exactly like this`;\n\n return issue({\n pageUrl: probe.url,\n message: `No \\`/favicon.ico\\` at the root: ${detail}. Clients that ask for it blind — feed readers, link unfurlers, older crawlers — get nothing.`,\n origin: { kind: \"computed\" },\n fix: {\n title: \"Generate it from the icon you already have\",\n snippet: [\n \"// No framework convention emits an .ico: Next's icon.tsx goes through\",\n \"// ImageResponse, which is PNG. The container is a header plus one\",\n \"// entry per size plus the PNGs concatenated — thirty lines and no\",\n \"// dependency, over an image library you almost certainly already have.\",\n \"//\",\n \"// Fingerprint the *inputs* (the source image, the sizes), not the\",\n \"// bytes you produce: encoders are not stable across versions, so a\",\n \"// script keyed on its own output dirties the file on every commit.\",\n \"// Then a --check mode can verify it in CI without writing anything.\",\n ].join(\"\\n\"),\n language: \"ts\",\n },\n });\n },\n};\n\n/**\n * The file-level robots.txt rules (`docs/sitemap-robots-plan.md` §4.1–4.2).\n *\n * They exist because the parse now keeps what it reads. Every one of them is a\n * few lines over a field `RobotsProbe` did not have a week ago, which is the\n * whole argument for having replaced two booleans with a model.\n *\n * The `robotstxt.*` prefix is deliberate and the plan settles it: the file and\n * the `<meta name=\"robots\">` tag are different subjects, and `robots.conflict`\n * already belongs to the tag.\n */\n\n/** RFC 9309 §2.4 — parsers need only honour the first 500 KiB. */\nconst ROBOTS_BYTE_LIMIT = 500 * 1024;\n\nconst robotstxtUnreachable: SiteRule = {\n id: \"robotstxt.unreachable\",\n severity: \"error\",\n summary: \"A robots.txt that errors is read as forbidding the whole site\",\n rigor: \"spec-required\",\n sources: [\"ietf-rfc9309\"],\n // A 404 is not a failure: §2.3.1.3 says an absent file allows everything,\n // and most sites that have none mean exactly that. Only a server that\n // answered badly, or did not answer, triggers the disallow-everything rule.\n appliesTo: (site) => site.robots !== undefined && isRobotsFailure(site.robots.status),\n check: ({ site, issue }) => {\n const probe = site.robots!;\n const detail =\n probe.status === 0 ? \"the request failed\" : `the origin answered ${probe.status}`;\n\n return issue({\n pageUrl: probe.url,\n message: `\\`robots.txt\\` could not be read: ${detail}. RFC 9309 §2.3.1.4 tells a crawler to assume a complete disallow for as long as this lasts — an outage on this one file takes the whole site out of the index.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst robotstxtOversized: SiteRule = {\n id: \"robotstxt.oversized\",\n severity: \"error\",\n summary: \"Rules past 500 KiB of robots.txt are not guaranteed to be read\",\n rigor: \"spec-required\",\n sources: [\"ietf-rfc9309\"],\n appliesTo: (site) => (site.robots?.byteLength ?? 0) > ROBOTS_BYTE_LIMIT,\n check: ({ site, issue }) => {\n const probe = site.robots!;\n const kib = Math.round(probe.byteLength / 1024);\n\n return issue({\n pageUrl: probe.url,\n message: `\\`robots.txt\\` is ${kib} KiB. A parser is only required to honour the first 500 KiB (RFC 9309 §2.4), so every rule past that point silently does not exist.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst robotstxtInvalidLine: SiteRule = {\n id: \"robotstxt.invalid-line\",\n severity: \"warning\",\n summary: \"Every line of robots.txt should parse as something\",\n rigor: \"spec-required\",\n sources: [\"ietf-rfc9309\"],\n appliesTo: (site) => (site.robots?.invalidLines.length ?? 0) > 0,\n check: ({ site, issue }) => {\n const probe = site.robots!;\n // One finding per file, not per line: a file with a hundred junk lines has\n // one defect, and the summary lesson says forty repeats of it are noise.\n const shown = probe.invalidLines.slice(0, 5);\n const rest = probe.invalidLines.length - shown.length;\n\n return issue({\n pageUrl: probe.url,\n message:\n `\\`robots.txt\\` has ${probe.invalidLines.length} line${probe.invalidLines.length === 1 ? \"\" : \"s\"} that parse as nothing: ` +\n shown.map((l) => `line ${l.line} (${l.reason})`).join(\", \") +\n `${rest > 0 ? `, and ${rest} more` : \"\"}. A crawler drops them silently, so the rule you meant to write is simply absent.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst robotstxtUnknownDirective: SiteRule = {\n id: \"robotstxt.unknown-directive\",\n severity: \"info\",\n summary: \"Non-standard robots.txt directives are read by some crawlers and ignored by others\",\n rigor: \"guideline\",\n sources: [\"ietf-rfc9309\", \"google-robots-intro\"],\n appliesTo: (site) => (site.robots?.unknownDirectives.length ?? 0) > 0,\n check: ({ site, issue }) => {\n const probe = site.robots!;\n const names = [...new Set(probe.unknownDirectives.map((d) => d.name))];\n\n // Deliberately not a defect. These parse, they are spelled correctly, and\n // some crawlers honour them — the finding exists so nobody counts on the\n // ones that do not.\n return issue({\n pageUrl: probe.url,\n message: `\\`robots.txt\\` uses ${names.map((n) => `\\`${n}\\``).join(\", \")}, which RFC 9309 does not define. Some crawlers honour ${names.length === 1 ? \"it\" : \"them\"} and Google ignores ${names.length === 1 ? \"it\" : \"them\"} — so this is worth knowing, not necessarily worth changing.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst robotstxtCrossOrigin: SiteRule = {\n id: \"robotstxt.cross-origin\",\n severity: \"warning\",\n summary: \"`/robots.txt` should not redirect to another origin\",\n rigor: \"vendor-spec\",\n sources: [\"ietf-rfc9309\"],\n appliesTo: (site) => site.robots?.redirects.crossOrigin === true,\n check: ({ site, issue }) => {\n const probe = site.robots!;\n\n return issue({\n pageUrl: probe.url,\n message: `\\`robots.txt\\` redirects to \\`${probe.redirects.finalUrl}\\`, on another origin. RFC 9309 §2.3.1.2 permits following it, so this works — but the policy for this site now lives somewhere this site does not control, and it is usually a proxy accident rather than a decision.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst robotstxtSitemapRelative: SiteRule = {\n id: \"robotstxt.sitemap.relative\",\n severity: \"error\",\n summary: \"A `Sitemap:` declaration must be an absolute URL\",\n rigor: \"spec-required\",\n sources: [\"sitemaps-protocol\", \"ietf-rfc9309\"],\n appliesTo: (site) => (site.robots?.sitemaps ?? []).some((s) => !isAbsolute(s.value)),\n check: ({ site, issue }) => {\n const probe = site.robots!;\n const relative = probe.sitemaps.filter((s) => !isAbsolute(s.value));\n\n return issue({\n pageUrl: probe.url,\n message: `\\`Sitemap:\\` must be a full URL: ${relative.map((s) => `line ${s.line} declares \\`${s.value}\\``).join(\", \")}. robots.txt is fetched on its own, so there is no page for a consumer to resolve a relative path against.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\n/**\n * A crawled page that asks to be indexed sits behind a `Disallow`.\n *\n * The generalisation of `robots.blocks-site` past `Disallow: /`, and the rule\n * the RFC 9309 matcher was written for. The site-wide case is the loud one;\n * this is the quiet one — a single path rule, added for a reason that made\n * sense once, still shadowing a section of the site that has since been given\n * pages that ask to be found.\n *\n * `robots.blocks-site` takes precedence: when the whole origin is disallowed,\n * every page is blocked and saying so once is the finding.\n */\nconst robotsBlocksPage: SiteRule = {\n id: \"robots.blocks-page\",\n severity: \"error\",\n summary: \"A page asking to be indexed must not be disallowed by robots.txt\",\n rigor: \"vendor-spec\",\n sources: [\"ietf-rfc9309\", \"google-robots-intro\"],\n appliesTo: (site) => site.robots?.found === true && robotsAllows(site.robots.groups, \"/\").allowed,\n check: ({ site, issue }) => {\n const probe = site.robots!;\n\n return site.pages.flatMap((page) => {\n if (!metaRobotsTokens(page).has(\"index\")) return [];\n\n let path: string;\n try {\n const url = new URL(page.fetch.finalUrl);\n path = `${url.pathname}${url.search}`;\n } catch {\n return [];\n }\n\n const decision = robotsAllows(probe.groups, path);\n if (decision.allowed || !decision.rule) return [];\n\n return issue({\n pageUrl: page.fetch.finalUrl,\n message: `Page declares \\`<meta name=\"robots\" content=\"index\">\\` but \\`robots.txt\\` line ${decision.rule.line} disallows \\`${decision.rule.pattern}\\` for \\`${decision.group}\\`. robots.txt wins: the page is never fetched, so the tag asking for it is never read.`,\n origin: { kind: \"computed\" },\n });\n });\n },\n};\n\n/**\n * The sitemap rules (`docs/sitemap-robots-plan.md` §4.3–4.4).\n *\n * Every one of them replaces a field of `SitemapDiagnostics` that was declared\n * and never written — `mixedHost`, `mixedProtocol`, `lastmodIssues`. The plan\n * says why they belonged in rules rather than in a diagnostics bag: a number\n * on a report tells you something is wrong somewhere, and a finding tells you\n * which entry, in which document, and what the specification says about it.\n *\n * Three rules from those sections are deliberately absent, because the model\n * cannot answer them honestly yet. `sitemap.limits.exceeded` counts entries\n * **per document** and today's `urlCount` is a total across all of them;\n * `sitemap.index.nested` needs to know a child was itself an index;\n * `sitemap.entry.out-of-scope` needs to know which document declared each\n * entry. All three arrive with the document tree, and shipping them now would\n * mean rules that quietly only work on a site with one flat sitemap.\n */\n\n/** `<changefreq>` values the protocol defines. Anything else is not one. */\nconst CHANGEFREQ = new Set([\"always\", \"hourly\", \"daily\", \"weekly\", \"monthly\", \"yearly\", \"never\"]);\n\n/** How many offending entries a finding names before it starts counting. */\nconst SAMPLE = 5;\n\n/** `n` entries, listing the first few — forty repeats of one defect is noise. */\nfunction sample(locs: string[]): string {\n const shown = locs.slice(0, SAMPLE);\n const rest = locs.length - shown.length;\n return `${shown.map((l) => `\\`${l}\\``).join(\", \")}${rest > 0 ? `, and ${rest} more` : \"\"}`;\n}\n\nconst sitemapMissing: SiteRule = {\n id: \"sitemap.missing\",\n severity: \"warning\",\n summary: \"A site should publish a sitemap\",\n rigor: \"guideline\",\n sources: [\"sitemaps-protocol\", \"google-sitemaps\"],\n // A warning rather than an error, and the plan settles it: a small, fully\n // linked site genuinely may not need one. Google's own guidance says\n // \"usually worth having\", which is exactly a guideline.\n appliesTo: (site) => site.discovery !== undefined && !site.discovery.diagnostics.found,\n check: ({ site, issue }) =>\n issue({\n pageUrl: `${site.origin}/sitemap.xml`,\n message:\n \"No sitemap was found — not declared in `robots.txt`, and not at a well-known path. \" +\n \"Discovery then depends entirely on what links to what, which is the part of a site nobody audits.\",\n origin: { kind: \"computed\" },\n }),\n};\n\nconst sitemapUnparsable: SiteRule = {\n id: \"sitemap.unparsable\",\n severity: \"error\",\n summary: \"A located sitemap must be a well-formed `<urlset>` or `<sitemapindex>`\",\n rigor: \"spec-required\",\n sources: [\"sitemaps-protocol\"],\n appliesTo: (site) =>\n site.discovery?.diagnostics.found === true && !site.discovery.diagnostics.wellFormed,\n check: ({ site, issue }) => {\n const diagnostics = site.discovery!.diagnostics;\n return issue({\n pageUrl: diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message:\n \"A sitemap was served but does not parse as XML. The usual cause is an HTML error page \" +\n \"answered with a 200 — which reads as a healthy sitemap to anything that only checks the status.\",\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst sitemapEmpty: SiteRule = {\n id: \"sitemap.empty\",\n severity: \"warning\",\n summary: \"A sitemap that parses should list something\",\n rigor: \"guideline\",\n sources: [\"sitemaps-protocol\"],\n // Only when the crawl found pages: an empty sitemap on a site with no pages\n // is consistent, and saying otherwise would be noise.\n appliesTo: (site) =>\n site.discovery?.diagnostics.wellFormed === true &&\n site.discovery.urls.length === 0 &&\n site.pages.length > 0,\n check: ({ site, issue }) => {\n const diagnostics = site.discovery!.diagnostics;\n return issue({\n pageUrl: diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `The sitemap parses and lists no URLs, while the crawl found ${site.pages.length}. goflag falls back to crawling — a consumer that trusts the sitemap has nothing to read.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst sitemapIndexChildError: SiteRule = {\n id: \"sitemap.index.child-error\",\n severity: \"error\",\n summary: \"Every child of a sitemap index must be reachable and parseable\",\n rigor: \"spec-required\",\n sources: [\"sitemaps-protocol\"],\n appliesTo: (site) => (site.discovery?.diagnostics.childSitemapErrors ?? 0) > 0,\n check: ({ site, issue }) => {\n const diagnostics = site.discovery!.diagnostics;\n const total = diagnostics.childSitemapCount;\n\n return issue({\n pageUrl: diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `${diagnostics.childSitemapErrors} of ${total} child sitemaps could not be read. The index declares an inventory and part of it is missing, so whatever those documents listed is invisible — and nothing says how much that is.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst sitemapEntryInvalidUrl: SiteRule = {\n id: \"sitemap.entry.invalid-url\",\n severity: \"error\",\n summary: \"Every `<loc>` must be an absolute, parseable URL\",\n rigor: \"spec-required\",\n sources: [\"sitemaps-protocol\"],\n appliesTo: (site) => (site.discovery?.urls ?? []).some((entry) => !isAbsolute(entry.loc)),\n check: ({ site, issue }) => {\n const bad = site.discovery!.urls.filter((entry) => !isAbsolute(entry.loc));\n return issue({\n pageUrl: site.discovery!.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `${bad.length} \\`<loc>\\` value${bad.length === 1 ? \" is\" : \"s are\"} not an absolute URL: ${sample(bad.map((e) => e.loc))}. A sitemap is fetched on its own, so a consumer has nothing to resolve them against.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\n/**\n * The two ceilings sitemaps.org puts on a single document, verbatim:\n *\n * > each Sitemap file that you provide must have no more than 50,000 URLs and\n * > must be no larger than 50MB (52,428,800 bytes)\n *\n * and, for an index:\n *\n * > Sitemap index files may not list more than 50,000 Sitemaps and must be no\n * > larger than 50MB (52,428,800 bytes)\n *\n * Same two numbers either way, counted over a different thing — which is why\n * this is one rule rather than two, and why it needs the document tree: both\n * are stated **per document**, and a site-wide total answers neither.\n */\nconst DOCUMENT_URL_LIMIT = 50_000;\nconst DOCUMENT_BYTE_LIMIT = 52_428_800;\n\n/** What a given document declares, counted the way its own ceiling is written. */\nfunction declaredCount(doc: SitemapDocument): number {\n return doc.kind === \"index\" ? doc.childLocs.length : doc.urlCount;\n}\n\nfunction oversizedDocuments(site: SiteContext): SitemapDocument[] {\n return (site.discovery?.documents ?? []).filter(\n (doc) =>\n doc.kind !== \"unparsable\" &&\n (declaredCount(doc) > DOCUMENT_URL_LIMIT || doc.byteLength > DOCUMENT_BYTE_LIMIT),\n );\n}\n\n/**\n * A sitemap document is over one of the protocol's two ceilings.\n *\n * Not a style point: a consumer is entitled to stop reading at the limit, so\n * everything past it is dead weight that looks published and is not. The\n * failure is silent from every angle — the file serves, it parses, and the\n * entries beyond the cut simply never get crawled.\n *\n * Measured on the **uncompressed** body, because the ceiling is about what a\n * consumer must parse rather than what crossed the wire. A gzipped document is\n * over the limit at the same size a plain one is.\n */\nconst sitemapLimitsExceeded: SiteRule = {\n id: \"sitemap.limits.exceeded\",\n severity: \"error\",\n summary: \"No single sitemap document may exceed 50,000 entries or 50 MB\",\n rigor: \"spec-required\",\n sources: [\"sitemaps-protocol\"],\n appliesTo: (site) => oversizedDocuments(site).length > 0,\n check: ({ site, issue }) =>\n oversizedDocuments(site).map((doc) => {\n const count = declaredCount(doc);\n const noun = doc.kind === \"index\" ? \"child sitemaps\" : \"URLs\";\n const over: string[] = [];\n if (count > DOCUMENT_URL_LIMIT) {\n over.push(`${count.toLocaleString(\"en-US\")} ${noun} against a ceiling of 50,000`);\n }\n if (doc.byteLength > DOCUMENT_BYTE_LIMIT) {\n over.push(\n `${(doc.byteLength / 1_048_576).toFixed(1)} MB uncompressed against a ceiling of 50 MB`,\n );\n }\n\n return issue({\n pageUrl: doc.url,\n message: `This sitemap document declares ${over.join(\" and \")}. A consumer may stop reading at the limit, so everything past it is published in name only — split the document and reference the parts from an index.`,\n origin: { kind: \"computed\" },\n });\n }),\n};\n\n/**\n * A sitemap lists URLs outside the directory it is served from.\n *\n * sitemaps.org scopes a document's authority by its own location, with an\n * example rather than a rule of thumb:\n *\n * > A Sitemap file located at http://example.com/catalog/sitemap.xml can\n * > include any URLs starting with http://example.com/catalog/ but can not\n * > include URLs starting with http://example.com/images/\n *\n * A root-level sitemap is exempt by construction: its directory is `/`, so\n * every path on the host is under it. That is most sitemaps, which is why this\n * rule is quiet on every site in this repository and still worth having — the\n * sites it catches are the ones that split sitemaps per section and serve them\n * from those sections.\n *\n * Entries on another host are skipped rather than counted twice:\n * `sitemap.entry.cross-host` is the finding for those, and reporting one URL\n * under two rules teaches a reader to discount both.\n */\nfunction outOfScopeEntries(site: SiteContext): { loc: string; from: string }[] {\n const out: { loc: string; from: string }[] = [];\n\n for (const entry of site.discovery?.urls ?? []) {\n if (!entry.documentUrl) continue;\n\n let scope: URL;\n let loc: URL;\n try {\n scope = new URL(entry.documentUrl);\n loc = new URL(entry.loc);\n } catch {\n continue;\n }\n\n // Everything up to and including the last `/` — the directory the document\n // is served from.\n const prefix = scope.pathname.replace(/[^/]*$/, \"\");\n if (prefix === \"/\") continue;\n if (loc.origin !== scope.origin) continue;\n if (loc.pathname.startsWith(prefix)) continue;\n\n out.push({ loc: entry.loc, from: entry.documentUrl });\n }\n\n return out;\n}\n\nconst sitemapEntryOutOfScope: SiteRule = {\n id: \"sitemap.entry.out-of-scope\",\n severity: \"error\",\n summary: \"A sitemap may only list URLs under its own directory\",\n rigor: \"spec-required\",\n sources: [\"sitemaps-protocol\"],\n appliesTo: (site) => outOfScopeEntries(site).length > 0,\n check: ({ site, issue }) => {\n const bad = outOfScopeEntries(site);\n // Grouped by the document that overreached: the fix is per document — move\n // the entries, or move the sitemap to the root — and one finding per stray\n // URL would bury that under repetition.\n const byDocument = new Map<string, string[]>();\n for (const { loc, from } of bad) {\n byDocument.set(from, [...(byDocument.get(from) ?? []), loc]);\n }\n\n return [...byDocument].map(([from, locs]) =>\n issue({\n pageUrl: from,\n message: `${locs.length} entr${locs.length === 1 ? \"y is\" : \"ies are\"} outside this sitemap's directory \\`${new URL(from).pathname.replace(/[^/]*$/, \"\")}\\`: ${sample(locs)}. A sitemap only speaks for the path it is served from, so a consumer may drop ${locs.length === 1 ? \"it\" : \"them\"} — serve the document from the root, or move the entries into a sitemap that covers them.`,\n origin: { kind: \"computed\" },\n }),\n );\n },\n};\n\nconst sitemapEntryCrossHost: SiteRule = {\n id: \"sitemap.entry.cross-host\",\n severity: \"error\",\n summary: \"A sitemap should only list URLs on its own host\",\n rigor: \"spec-required\",\n sources: [\"sitemaps-protocol\"],\n // `www` and the apex are different hosts to a consumer, which is the case\n // this actually catches: a sitemap generated against one and served on the\n // other. The cross-submission escape hatch needs the other host's robots.txt\n // and arrives with the cross-artefact rules.\n appliesTo: (site) => crossHostEntries(site).length > 0,\n check: ({ site, issue }) => {\n const bad = crossHostEntries(site);\n return issue({\n pageUrl: site.discovery!.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `${bad.length} entr${bad.length === 1 ? \"y names a host\" : \"ies name hosts\"} other than this sitemap's: ${sample(bad)}. A consumer may drop them — the sitemap only speaks for the host that serves it.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst sitemapEntryProtocolMismatch: SiteRule = {\n id: \"sitemap.entry.protocol-mismatch\",\n severity: \"warning\",\n summary: \"A sitemap should not mix `http` and `https` entries\",\n rigor: \"spec-required\",\n sources: [\"sitemaps-protocol\"],\n appliesTo: (site) => protocolsIn(site).size > 1,\n check: ({ site, issue }) => {\n const protocols = [...protocolsIn(site)].sort();\n return issue({\n pageUrl: site.discovery!.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `The sitemap lists both ${protocols.join(\" and \")} URLs. One of the two sets names pages the site does not serve at those addresses, and a consumer has no way to tell which.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst sitemapLastmodInvalid: SiteRule = {\n id: \"sitemap.lastmod.invalid\",\n severity: \"warning\",\n summary: \"`<lastmod>` must be a W3C Datetime, and must not be in the future\",\n rigor: \"spec-required\",\n sources: [\"sitemaps-protocol\", \"google-sitemaps\"],\n appliesTo: (site) => badLastmods(site).length > 0,\n check: ({ site, issue }) => {\n const bad = badLastmods(site);\n const malformed = bad.filter((e) => e.reason === \"malformed\");\n const future = bad.filter((e) => e.reason === \"future\");\n\n const parts: string[] = [];\n if (malformed.length > 0) {\n parts.push(\n `${malformed.length} not a W3C Datetime (${sample(malformed.map((e) => e.value))})`,\n );\n }\n if (future.length > 0) {\n parts.push(`${future.length} dated in the future (${sample(future.map((e) => e.value))})`);\n }\n\n return issue({\n pageUrl: site.discovery!.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `\\`<lastmod>\\` values a consumer cannot use: ${parts.join(\"; \")}. Google ignores the field entirely when it stops trusting it, so one bad batch costs the whole site the signal.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst sitemapFieldInvalid: SiteRule = {\n id: \"sitemap.field.invalid\",\n severity: \"warning\",\n summary: \"`<changefreq>` and `<priority>` must hold the values the protocol defines\",\n rigor: \"spec-required\",\n sources: [\"sitemaps-protocol\", \"google-sitemaps\"],\n appliesTo: (site) => badFields(site).length > 0,\n check: ({ site, issue }) => {\n const bad = badFields(site);\n return issue({\n pageUrl: site.discovery!.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `${bad.length} entr${bad.length === 1 ? \"y carries a field\" : \"ies carry fields\"} outside the protocol's values: ${sample(bad)}. Google ignores both fields either way — so this is worth fixing or deleting, never worth trusting.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nfunction sitemapHost(site: SiteContext): string | undefined {\n const url = site.discovery?.diagnostics.sitemapUrl;\n try {\n return url ? new URL(url).host : new URL(site.origin).host;\n } catch {\n return undefined;\n }\n}\n\nfunction crossHostEntries(site: SiteContext): string[] {\n const host = sitemapHost(site);\n if (!host) return [];\n\n return (site.discovery?.urls ?? [])\n .filter((entry) => {\n if (!isAbsolute(entry.loc)) return false;\n try {\n return new URL(entry.loc).host !== host;\n } catch {\n return false;\n }\n })\n .map((entry) => entry.loc);\n}\n\nfunction protocolsIn(site: SiteContext): Set<string> {\n const protocols = new Set<string>();\n for (const entry of site.discovery?.urls ?? []) {\n try {\n protocols.add(new URL(entry.loc).protocol.replace(\":\", \"\"));\n } catch {\n continue;\n }\n }\n return protocols;\n}\n\n/**\n * W3C Datetime, as the sitemap protocol requires: a date, optionally with a\n * time and a timezone. Deliberately stricter than `Date.parse`, which accepts\n * `March 4 2026` and every other thing a human might type.\n */\nconst W3C_DATETIME = /^\\d{4}(-\\d{2}(-\\d{2}(T\\d{2}:\\d{2}(:\\d{2}(\\.\\d+)?)?(Z|[+-]\\d{2}:\\d{2}))?)?)?$/;\n\nfunction badLastmods(site: SiteContext): Array<{ value: string; reason: \"malformed\" | \"future\" }> {\n const out: Array<{ value: string; reason: \"malformed\" | \"future\" }> = [];\n // Compared against the run's own clock rather than a fixed date, and only\n // flagged past a day of slack: a build that stamps \"now\" in a timezone ahead\n // of the auditor is not a defect.\n const tomorrow = Date.now() + 24 * 60 * 60 * 1000;\n\n for (const entry of site.discovery?.urls ?? []) {\n const value = entry.lastmod?.trim();\n if (!value) continue;\n\n if (!W3C_DATETIME.test(value)) {\n out.push({ value, reason: \"malformed\" });\n continue;\n }\n const parsed = Date.parse(value);\n if (Number.isFinite(parsed) && parsed > tomorrow) out.push({ value, reason: \"future\" });\n }\n return out;\n}\n\nfunction badFields(site: SiteContext): string[] {\n const out: string[] = [];\n for (const entry of site.discovery?.urls ?? []) {\n const changefreq = entry.changefreq?.trim().toLowerCase();\n if (changefreq && !CHANGEFREQ.has(changefreq)) {\n out.push(`${entry.loc} — changefreq \\`${entry.changefreq}\\``);\n continue;\n }\n const priority = entry.priority?.trim();\n if (priority === undefined) continue;\n const value = Number(priority);\n if (!Number.isFinite(value) || value < 0 || value > 1) {\n out.push(`${entry.loc} — priority \\`${entry.priority}\\``);\n }\n }\n return out;\n}\n\n/**\n * Where the two artefacts meet the crawl (`docs/sitemap-robots-plan.md` §4.5).\n *\n * The plan calls these \"the expensive ones, and they are all judgments nothing\n * makes today\". Four of the six are expensive in reasoning rather than in\n * requests — they compare a sitemap entry against a page goflag already\n * fetched, or against a robots.txt it already parsed — so they ship here.\n * `sitemap.entry.unreachable` and `.entry.redirects` need to probe URLs the\n * crawl never visited, and wait for that.\n *\n * Each one is a contradiction between two things the site says about itself,\n * which is the class of defect this whole tool exists for: nothing is broken\n * when you look at either half alone.\n */\n\n/** A URL reduced to what makes it the same page: no fragment, no trailing slash. */\nfunction sameUrlKey(url: string): string | undefined {\n try {\n const parsed = new URL(url);\n parsed.hash = \"\";\n const path = parsed.pathname.replace(/\\/+$/, \"\");\n return `${parsed.origin}${path || \"/\"}${parsed.search}`;\n } catch {\n return undefined;\n }\n}\n\n/** Crawled pages, keyed the way a sitemap entry would name them. */\nfunction pagesByUrl(site: SiteContext): Map<string, Page> {\n const byUrl = new Map<string, Page>();\n for (const page of site.pages) {\n const key = sameUrlKey(page.fetch.finalUrl);\n if (key) byUrl.set(key, page);\n }\n return byUrl;\n}\n\n/** Whether a page asks not to be indexed, by meta tag or by header. */\nfunction saysNoindex(page: Page): boolean {\n if (metaRobotsTokens(page).has(\"noindex\")) return true;\n const header = page.fetch.headers[\"x-robots-tag\"] ?? \"\";\n return header\n .split(\",\")\n .map((token) => token.trim().toLowerCase())\n .includes(\"noindex\");\n}\n\nconst sitemapEntryBlockedByRobots: SiteRule = {\n id: \"sitemap.entry.blocked-by-robots\",\n severity: \"error\",\n summary: \"A sitemap must not list URLs that robots.txt forbids fetching\",\n rigor: \"vendor-spec\",\n sources: [\"ietf-rfc9309\", \"sitemaps-protocol\"],\n // Skipped when the whole origin is disallowed: `robots.blocks-site` is that\n // finding, and repeating it once per sitemap entry would bury it.\n appliesTo: (site) =>\n site.robots?.found === true &&\n robotsAllows(site.robots.groups, \"/\").allowed &&\n (site.discovery?.urls.length ?? 0) > 0,\n check: ({ site, issue }) => {\n const probe = site.robots!;\n const blocked: string[] = [];\n\n for (const entry of site.discovery!.urls) {\n let path: string;\n try {\n const url = new URL(entry.loc);\n path = `${url.pathname}${url.search}`;\n } catch {\n continue;\n }\n if (!robotsAllows(probe.groups, path).allowed) blocked.push(entry.loc);\n }\n\n if (blocked.length === 0) return [];\n return issue({\n pageUrl: site.discovery!.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `${blocked.length} sitemap entr${blocked.length === 1 ? \"y is\" : \"ies are\"} disallowed by \\`robots.txt\\`: ${sample(blocked)}. The sitemap says \"index this\" and robots.txt says \"never fetch it\" — both cannot hold, and robots.txt is the one that decides.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst sitemapEntryNoindex: SiteRule = {\n id: \"sitemap.entry.noindex\",\n severity: \"warning\",\n summary: \"A sitemap must not list URLs that ask not to be indexed\",\n rigor: \"vendor-spec\",\n sources: [\"sitemaps-protocol\", \"google-robots-meta\"],\n // Only the crawled pages can be judged: a sitemap entry goflag never fetched\n // has no `noindex` to have seen, and assuming one either way would invent a\n // finding or hide one.\n appliesTo: (site) => (site.discovery?.urls.length ?? 0) > 0,\n check: ({ site, issue }) => {\n const byUrl = pagesByUrl(site);\n const conflicting: string[] = [];\n\n for (const entry of site.discovery!.urls) {\n const key = sameUrlKey(entry.loc);\n const page = key ? byUrl.get(key) : undefined;\n if (page && saysNoindex(page)) conflicting.push(entry.loc);\n }\n\n if (conflicting.length === 0) return [];\n return issue({\n pageUrl: site.discovery!.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `${conflicting.length} sitemap entr${conflicting.length === 1 ? \"y declares\" : \"ies declare\"} \\`noindex\\`: ${sample(conflicting)}. \"Please index this\" and \"do not index this\" are the same site's two answers to one question.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst sitemapEntryNonCanonical: SiteRule = {\n id: \"sitemap.entry.non-canonical\",\n severity: \"warning\",\n summary: \"A sitemap should list canonical URLs\",\n rigor: \"vendor-spec\",\n sources: [\"sitemaps-protocol\", \"google-canonicalization\"],\n appliesTo: (site) => (site.discovery?.urls.length ?? 0) > 0,\n check: ({ site, issue }) => {\n const byUrl = pagesByUrl(site);\n const wrong: string[] = [];\n\n for (const entry of site.discovery!.urls) {\n const key = sameUrlKey(entry.loc);\n const page = key ? byUrl.get(key) : undefined;\n const canonical = page?.links.canonical;\n if (!page || !canonical) continue;\n\n const canonicalKey = sameUrlKey(canonical);\n // Only when the page names a *different* page. A canonical that differs\n // by a trailing slash is the same page said twice, which `sameUrlKey`\n // already folds together.\n if (canonicalKey && canonicalKey !== key) {\n wrong.push(`${entry.loc} → ${canonical}`);\n }\n }\n\n if (wrong.length === 0) return [];\n return issue({\n pageUrl: site.discovery!.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `${wrong.length} sitemap entr${wrong.length === 1 ? \"y names a page whose canonical\" : \"ies name pages whose canonicals\"} point elsewhere: ${sample(wrong)}. The sitemap is a list of what to index, so it should name the URL the site itself prefers.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst sitemapOrphans: SiteRule = {\n id: \"sitemap.orphans\",\n severity: \"warning\",\n summary: \"Indexable pages the crawl found should be listed in the sitemap\",\n rigor: \"guideline\",\n sources: [\"sitemaps-protocol\", \"google-sitemaps\"],\n // One finding with a count and a sample, not one per page — the same shape\n // translation holes take, and for the same reason: forty repeats of one\n // omission is noise, and the omission is a property of the sitemap.\n appliesTo: (site) => site.discovery?.diagnostics.found === true && site.discovery.urls.length > 0,\n check: ({ site, issue }) => {\n const listed = new Set(\n site.discovery!.urls.map((entry) => sameUrlKey(entry.loc)).filter(Boolean),\n );\n\n const orphans = site.pages\n .filter((page) => !saysNoindex(page))\n .map((page) => ({ page, key: sameUrlKey(page.fetch.finalUrl) }))\n .filter(({ key }) => key !== undefined && !listed.has(key))\n .map(({ page }) => page.fetch.finalUrl);\n\n if (orphans.length === 0) return [];\n return issue({\n pageUrl: site.discovery!.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `${orphans.length} crawled page${orphans.length === 1 ? \"\" : \"s\"} ask${orphans.length === 1 ? \"s\" : \"\"} to be indexed and ${orphans.length === 1 ? \"is\" : \"are\"} absent from the sitemap: ${sample(orphans)}. A consumer that reads the sitemap rather than following links will never see ${orphans.length === 1 ? \"it\" : \"them\"}.`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\n/**\n * The last two of §4.5, and the only rules in the catalogue whose subject had\n * to be fetched on purpose.\n *\n * Everything else about a sitemap is a comparison between things goflag\n * already had. These two ask what is actually served at a URL, which is why\n * they waited for `probeSitemapEntries` — and why that pass answers from the\n * crawl and the link audit first, and only fetches what is left.\n */\n\n/** What the probe pass found, or nothing when it did not run. */\nfunction entryProbes(site: SiteContext): SitemapEntryProbe[] {\n return [...(site.sitemapEntries?.byUrl.values() ?? [])];\n}\n\n/**\n * The sentence a finding owes when the caps stopped the pass short.\n *\n * \"3 entries are unreachable\" out of a sitemap where 400 were never checked is\n * a true sentence that reads as a false one — it implies the other 397 are\n * fine. Saying how many went unchecked is what makes the number honest.\n */\nfunction coverageNote(site: SiteContext): string {\n const unprobed = site.sitemapEntries?.unprobed ?? 0;\n if (unprobed === 0) return \"\";\n return ` ${unprobed} further entr${unprobed === 1 ? \"y was\" : \"ies were\"} not checked — this count is a floor, not a total.`;\n}\n\nconst sitemapEntryUnreachable: SiteRule = {\n id: \"sitemap.entry.unreachable\",\n severity: \"error\",\n summary: \"Every URL a sitemap lists must answer\",\n rigor: \"vendor-spec\",\n sources: [\"sitemaps-protocol\", \"google-sitemaps\"],\n appliesTo: (site) => entryProbes(site).some((probe) => isDead(probe.status)),\n check: ({ site, issue }) => {\n const dead = entryProbes(site).filter((probe) => isDead(probe.status));\n\n return issue({\n pageUrl: site.discovery!.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `${dead.length} sitemap entr${dead.length === 1 ? \"y does\" : \"ies do\"} not answer: ${sample(dead.map((probe) => `${probe.url} (${probe.status === 0 ? \"no response\" : `HTTP ${probe.status}`})`))}. A sitemap is a list of pages to index, so every dead entry spends crawl budget on a promise the site does not keep.${coverageNote(site)}`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\nconst sitemapEntryRedirects: SiteRule = {\n id: \"sitemap.entry.redirects\",\n severity: \"warning\",\n summary: \"A sitemap should list final URLs, not URLs that redirect\",\n rigor: \"guideline\",\n sources: [\"sitemaps-protocol\", \"google-sitemaps\"],\n appliesTo: (site) => entryProbes(site).some((probe) => probe.redirected && !isDead(probe.status)),\n check: ({ site, issue }) => {\n const moved = entryProbes(site).filter((probe) => probe.redirected && !isDead(probe.status));\n\n return issue({\n pageUrl: site.discovery!.diagnostics.sitemapUrl ?? `${site.origin}/sitemap.xml`,\n message: `${moved.length} sitemap entr${moved.length === 1 ? \"y redirects\" : \"ies redirect\"}: ${sample(moved.map((probe) => `${probe.url} → ${probe.finalUrl}`))}. Google asks for the final URL, and every hop is crawl budget spent plus one more chance for a consumer to disagree about which address is the page.${coverageNote(site)}`,\n origin: { kind: \"computed\" },\n });\n },\n};\n\n/** 4xx, 5xx, or no response at all. A 3xx is a redirect, not a death. */\nfunction isDead(status: number): boolean {\n return status === 0 || status >= 400;\n}\n\n/** A status that means the file could not be read, as opposed to absent. */\nfunction isRobotsFailure(status: number): boolean {\n return status === 0 || status >= 500;\n}\n\nfunction isAbsolute(value: string): boolean {\n try {\n const url = new URL(value);\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n } catch {\n return false;\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 hreflangClusterIncomplete,\n iconsIcoMissing,\n robotsBlocksPage,\n robotsBlocksSite,\n robotstxtCrossOrigin,\n robotstxtInvalidLine,\n robotstxtOversized,\n robotstxtSitemapRelative,\n robotstxtUnknownDirective,\n robotstxtUnreachable,\n sitemapEmpty,\n sitemapEntryBlockedByRobots,\n sitemapEntryCrossHost,\n sitemapEntryInvalidUrl,\n sitemapEntryNoindex,\n sitemapEntryNonCanonical,\n sitemapEntryOutOfScope,\n sitemapEntryProtocolMismatch,\n sitemapEntryRedirects,\n sitemapEntryUnreachable,\n sitemapFieldInvalid,\n sitemapIndexChildError,\n sitemapLastmodInvalid,\n sitemapLimitsExceeded,\n sitemapMissing,\n sitemapOrphans,\n sitemapUnparsable,\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 // Stamped from the descriptor, never from the check: a rule does not\n // get to claim a rigor per finding. Absent on the three rules that\n // predate the field, which is how the gap stays visible instead of\n // being filled with a plausible citation.\n ...(rule.rigor ? { rigor: rule.rigor, sources: rule.sources ?? [] } : {}),\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 { isKnownLanguageTag, looksLikeLocaleSegment } from \"./bcp47\";\nimport type { Page } from \"./types\";\n\nexport { isKnownLanguageTag };\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/** 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 SitemapDocument,\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 /** Uncompressed length of `body`, in bytes. `0` when there is no body. */\n byteLength: number;\n /** The payload was inflated, or the server declared `content-encoding: gzip`. */\n gzipped: boolean;\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 // The parse keeps each declaration's line number; discovery only needs the\n // value, and the line matters to the rules that judge the file.\n const declaredUrls = robots.sitemaps.map((entry) => entry.value);\n\n const candidates: SitemapCandidate[] = [];\n for (const declared of declaredUrls) {\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: declaredUrls.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 const documents: SitemapDocument[] = [];\n let truncated = false;\n\n documents.push({\n url: candidate.url,\n status: doc.status,\n byteLength: doc.byteLength,\n gzipped: doc.gzipped,\n kind: parsed.kind === \"index\" ? \"index\" : \"urlset\",\n childLocs: parsed.kind === \"index\" ? [...parsed.sitemaps] : [],\n // What the document declares, not what survived dedupe and the cap: the\n // ceiling is on the file's contents, and a post-dedupe count would answer\n // a question the protocol never asked.\n urlCount: parsed.kind === \"urlset\" ? parsed.urls.length : 0,\n declaredInRobots: candidate.declaredInRobots,\n });\n\n if (parsed.kind === \"urlset\") {\n truncated = pushEntries(collected, seen, parsed.urls, maxUrls, candidate.url);\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 // Recorded before the `continue`, so a child that could not be read\n // is still a node of the tree. Dropping it here is what made the\n // count in `childSitemapErrors` the only surviving trace of it.\n documents.push({\n url: childUrl,\n status: childDoc.status,\n byteLength: childDoc.byteLength,\n gzipped: childDoc.gzipped,\n kind: \"unparsable\",\n childLocs: [],\n urlCount: 0,\n declaredInRobots: declaredUrls.includes(childUrl),\n parentUrl: candidate.url,\n });\n diagnostics.childSitemapErrors += 1;\n diagnostics.warnings.push(`Child sitemap unreachable: ${childUrl}`);\n continue;\n }\n const childParsed = parseSitemap(childDoc.body);\n documents.push({\n url: childUrl,\n status: childDoc.status,\n byteLength: childDoc.byteLength,\n gzipped: childDoc.gzipped,\n // A child that is itself an index is recorded as one. This run still\n // treats it as an error below, because that is what it has always\n // done and replacing that verdict needs a source no specification\n // provides — see `docs/sitemap-robots-plan.md` §4.3. The tree now\n // says which of the two it was, which is the part that was lost.\n kind: childParsed.kind === \"unknown\" ? \"unparsable\" : childParsed.kind,\n childLocs: childParsed.kind === \"index\" ? [...childParsed.sitemaps] : [],\n urlCount: childParsed.kind === \"urlset\" ? childParsed.urls.length : 0,\n declaredInRobots: declaredUrls.includes(childUrl),\n parentUrl: candidate.url,\n });\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, childUrl)) 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 documents,\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 {\n origin,\n baseUrl,\n source: \"well-known\",\n urls: [],\n documents: [],\n diagnostics,\n truncated: false,\n };\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 // No document declared these: the crawl found them by following links. An\n // empty tree is the honest answer, and the rules that read it are the ones\n // that judge documents — which this run has none of.\n documents: [],\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 documentUrl: string,\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 // Stamped here rather than in the parser: the parser reads a body and has\n // no idea which URL it came from, and the first document to claim a `<loc>`\n // is the one whose scope that entry is judged against.\n out.push({ ...entry, documentUrl });\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, byteLength: 0, gzipped: false };\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 // Measured on the inflated text either way: the protocol's ceiling is\n // what a consumer must parse, not what crossed the wire, and a gzipped\n // 50 MB sitemap is over the limit however small the transfer was.\n try {\n return withLength(res.status, gunzipSync(buf).toString(\"utf8\"), true);\n } catch {\n // Already-decoded (or not really gzip) — fall back to raw text.\n return withLength(res.status, buf.toString(\"utf8\"), true);\n }\n }\n return withLength(res.status, await res.text(), false);\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\", byteLength: 0, gzipped: false };\n } finally {\n cleanup();\n restoreTls();\n }\n}\n\n/** A read body, measured. `Buffer.byteLength` counts UTF-8 octets, not code units. */\nfunction withLength(status: number, body: string, gzipped: boolean): FetchedDoc {\n return { status, body, byteLength: Buffer.byteLength(body, \"utf8\"), gzipped };\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 { SitemapAlternate, SitemapUrlEntry } from \"./types\";\n\n/**\n * A selection, as `$(el)` returns one. Derived rather than imported: the node\n * types live in `domhandler`, which cheerio does not re-export and which this\n * package has no reason to depend on directly.\n */\ntype Selection = ReturnType<cheerio.CheerioAPI>;\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 */\n/**\n * The `<xhtml:link rel=\"alternate\" hreflang>` children of a `<url>` entry.\n *\n * Matched on the **local name**, like every other lookup in this file: cheerio\n * in `xmlMode` keeps the prefix on `tagName`, so `find(\"link\")` returns nothing\n * on the `xhtml:link` real sitemaps emit — which is exactly how 150 of them in\n * `fixtures/sites/tancrede/sitemap.xml` were being discarded silently. A\n * namespace-agnostic match also takes the `<link>` a sitemap written without\n * the prefix would carry.\n *\n * `rel` is checked but tolerated when absent: the sitemap protocol has no other\n * use for a `link` child, and a missing attribute is not worth dropping a\n * cluster declaration over.\n */\nfunction alternatesOf(node: Selection): SitemapAlternate[] {\n const out: SitemapAlternate[] = [];\n const seen = new Set<string>();\n\n node.children().each((_, el) => {\n const tag = \"tagName\" in el ? (el.tagName ?? \"\") : \"\";\n if (tag.split(\":\").pop()?.toLowerCase() !== \"link\") return;\n\n const attrs = \"attribs\" in el ? el.attribs : undefined;\n if (!attrs) return;\n\n const rel = (attrs.rel ?? \"alternate\").trim().toLowerCase();\n if (rel !== \"alternate\") return;\n\n const hreflang = (attrs.hreflang ?? \"\").trim();\n const href = (attrs.href ?? \"\").trim();\n if (!hreflang || !href) return;\n\n // A cluster declaring the same tag twice is declaring it once and\n // contradicting itself; the first wins, as everywhere else here.\n const key = `${hreflang.toLowerCase()}\\0${href}`;\n if (seen.has(key)) return;\n seen.add(key);\n\n out.push({ hreflang, href });\n });\n\n return out;\n}\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 const alternates = alternatesOf(node);\n if (alternates.length > 0) entry.alternates = alternates;\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 * 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 * 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 * What is actually served at the URLs a sitemap promises.\n *\n * `docs/sitemap-robots-plan.md` §4.5 asks for two judgments — an entry that\n * 404s, and one that redirects — and both need an answer per URL that the\n * rules cannot go and get: a `SiteRule` is a pure function of a `SiteContext`,\n * the same discipline that keeps page rules testable without a network.\n *\n * **Nothing here is fetched twice.** The plan's word is \"global dedupe\", and a\n * sitemap entry usually already has an answer by the time this runs: the crawl\n * visited it, or the link audit probed it because some page linked to it. Only\n * what is left over is fetched, which on a well-built site is close to nothing\n * — the sitemap and the link graph describe the same pages. On a site where\n * they do not, the leftovers *are* the finding.\n *\n * `via` records which of the three it was, so a report can say where its\n * answer came from rather than implying it went and looked.\n */\n\nimport { checkLink } from \"../links/check\";\nimport type { LinkCheck } from \"../links/types\";\nimport type { SitemapEntryProbe } from \"../types\";\n\nexport interface ProbeEntriesOptions {\n /** Answers the crawl already has: URL → status. */\n crawled: Map<string, number>;\n /** Answers the link audit already has. */\n checked: Map<string, LinkCheck>;\n /** Hard cap on URLs fetched here. The link audit's, for the same reason. */\n maxProbes: number;\n concurrency?: number;\n signal?: AbortSignal;\n timeoutMs?: number;\n allowInsecureTls?: boolean;\n}\n\nexport interface ProbeEntriesResult {\n byUrl: Map<string, SitemapEntryProbe>;\n /** Entries with no answer at all, because the cap stopped this pass. */\n unprobed: number;\n}\n\n/**\n * Answer for as many entries as the caps allow.\n *\n * The count of what it could not reach is returned rather than swallowed: a\n * rule that says \"3 entries are unreachable\" out of a sitemap where 400 were\n * never checked is telling a true sentence that reads as a false one, and\n * `unprobed` is what lets the finding say so.\n */\nexport async function probeSitemapEntries(\n locs: readonly string[],\n options: ProbeEntriesOptions,\n): Promise<ProbeEntriesResult> {\n const byUrl = new Map<string, SitemapEntryProbe>();\n const toFetch: string[] = [];\n\n for (const loc of locs) {\n if (byUrl.has(loc)) continue;\n\n const crawled = options.crawled.get(loc);\n if (crawled !== undefined) {\n byUrl.set(loc, { url: loc, status: crawled, via: \"crawl\", finalUrl: loc, redirected: false });\n continue;\n }\n\n const checked = options.checked.get(loc);\n if (checked) {\n byUrl.set(loc, fromCheck(loc, checked, \"link-audit\"));\n continue;\n }\n\n toFetch.push(loc);\n }\n\n const budget = toFetch.slice(0, options.maxProbes);\n const unprobed = toFetch.length - budget.length;\n\n const queue = [...budget];\n const workers = Math.min(options.concurrency ?? 6, queue.length);\n await Promise.all(\n Array.from({ length: workers }, async () => {\n for (let url = queue.pop(); url !== undefined; url = queue.pop()) {\n const check = await checkLink(url, {\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n allowInsecureTls: options.allowInsecureTls,\n }).catch(() => undefined);\n if (check) byUrl.set(url, fromCheck(url, check, \"probe\"));\n }\n }),\n );\n\n return { byUrl, unprobed };\n}\n\nfunction fromCheck(\n url: string,\n check: LinkCheck,\n via: SitemapEntryProbe[\"via\"],\n): SitemapEntryProbe {\n return {\n url,\n status: check.status,\n via,\n finalUrl: check.finalUrl,\n // A redirect that resolved, or one that did not: either way the entry did\n // not name the URL that answers, which is what the rule is about.\n redirected: check.finalUrl !== url || check.redirectChain.length > 0,\n };\n}\n","/**\n * Which URLs are the same page in another language, when the path cannot say.\n *\n * The matrix keys a row on the pathname minus its locale segment, so a site\n * that translates its slugs — `/en/pricing` and `/fr/tarifs` — gets two rows,\n * each filled in one locale, and two translation holes on a pair that is\n * perfectly translated. Measured: 16 false holes out of 17 routes on a\n * nine-pair site (`docs/i18n-cluster-plan.md` §1).\n *\n * Two sources declare the pairing, and they are complementary rather than\n * competing (§9):\n *\n * - the **sitemap**'s `xhtml:link` entries, which survive sampling: a `<url>`\n * entry names its whole cluster whether or not any member was fetched.\n * Structural coverage draws the two locales of a slug-translating family\n * from disjoint pages — 0 of 8 pairs had both sides sampled (§2) — so this\n * is the only source that works in the default mode;\n * - the crawled pages' **`<head>`**, which is the canonical mechanism and the\n * one most correct sites actually use. It needs both members in hand, so it\n * is a no-op exactly where sampling bites; it is what fixes the frequent\n * site that declares properly in its `<head>` and nothing in its sitemap,\n * which was earning 4 mismatch warnings and 4 false holes (§9.1).\n *\n * The sitemap wins where both answer. Three lines this must not cross, all of\n * them judged objections (§6, §9.2):\n *\n * 1. **A declaration decides identity, never existence.** It says \"these URLs\n * are one page\"; it does not say the pages are there. The same fixture\n * that supplies the clusters advertises six targets it does not serve, and\n * believing those would bury six real holes.\n * 2. **The label cannot depend on the membership.** A key derived from \"the\n * smallest member\" is renamed when a locale is added, which moves every\n * finding's fingerprint and reddens a baseline on a site where nothing\n * moved. So the anchor is `x-default` — a declaration the site makes about\n * itself, unchanged when a member joins — and a cluster without one is not\n * merged at all.\n * 3. **No `canonical` edge.** Self-referential per locale everywhere it was\n * measured, already consumed as a duplicate signal, and the largest\n * silent-merge surface the design was offered.\n */\n\nimport { splitRoute } from \"./i18n\";\nimport type { SitemapUrlEntry } from \"./sitemap/types\";\nimport type { Page } from \"./types\";\n\nexport interface ClusterIndex {\n /**\n * The row a URL belongs to, or `undefined` when nothing declared one — in\n * which case the caller keeps its pathname-derived route and today's\n * behaviour is unchanged.\n */\n routeOf(url: string): string | undefined;\n /**\n * Normalised URL → row. Exposed so two indexes can be combined, and so the\n * combination can tell agreement from contradiction rather than guessing.\n */\n rows: ReadonlyMap<string, string>;\n /** How many clusters were formed. Reported so a merge is never invisible. */\n size: number;\n /**\n * URLs a second entry tried to move into a different cluster. Kept rather\n * than resolved: a site declaring one URL into two clusters is contradicting\n * itself, and the report should say so rather than pick a winner quietly.\n */\n conflicts: string[];\n /**\n * Clusters the site declared and goflag declined to use, for want of an\n * anchor it could name the row after. Counted rather than swallowed: a\n * declaration we saw and did not act on is exactly the kind of quiet\n * degradation this report is supposed to admit to.\n */\n refused: number;\n}\n\n/** Trailing slash and fragment dropped; origin, path and query kept. */\nfunction normalise(raw: string): string | null {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n return null;\n }\n url.hash = \"\";\n const path = url.pathname.length > 1 ? url.pathname.replace(/\\/+$/, \"\") : url.pathname;\n return `${url.origin}${path}${url.search}`;\n}\n\nconst X_DEFAULT = \"x-default\";\n\n/** One cluster per distinct row: the rows *are* the clusters. */\nfunction clusterCount(rows: ReadonlyMap<string, string>): number {\n return new Set(rows.values()).size;\n}\n\nfunction indexOf(rows: Map<string, string>, conflicts: string[], refused: number): ClusterIndex {\n return {\n rows,\n routeOf: (url) => {\n const key = normalise(url);\n return key === null ? undefined : rows.get(key);\n },\n size: clusterCount(rows),\n conflicts,\n refused,\n };\n}\n\n/**\n * Build the cluster index from the sitemap's own cluster declarations.\n *\n * Only `xhtml:link rel=\"alternate\"` is read — no canonical edge, per the\n * constraint above.\n */\nexport function buildClusterIndex(entries: readonly SitemapUrlEntry[]): ClusterIndex {\n const rows = new Map<string, string>();\n const conflicts: string[] = [];\n let refused = 0;\n\n for (const entry of entries) {\n const alternates = entry.alternates;\n if (!alternates || alternates.length === 0) continue;\n\n const xDefault = alternates.find((a) => a.hreflang.trim().toLowerCase() === X_DEFAULT);\n // No anchor, no merge. Picking one from the members would make the row's\n // name a function of who is in it, which is the fingerprint churn this\n // exists to avoid.\n if (!xDefault) {\n refused++;\n continue;\n }\n\n const anchorUrl = normalise(xDefault.href);\n if (!anchorUrl) {\n refused++;\n continue;\n }\n\n const { route: anchorRoute } = splitRoute(new URL(anchorUrl).pathname);\n\n // The entry's own URL belongs to the cluster it declares, even when the\n // alternate list omits it — a site is not obliged to list itself, though\n // Google asks it to.\n const members = [entry.loc, ...alternates.map((a) => a.href)];\n\n for (const member of members) {\n const url = normalise(member);\n if (!url) continue;\n\n const existing = rows.get(url);\n if (existing === undefined) {\n rows.set(url, anchorRoute);\n continue;\n }\n // First writer wins, like every other map in this layer, and the second\n // claim is surfaced rather than dropped.\n if (existing !== anchorRoute && !conflicts.includes(url)) conflicts.push(url);\n }\n }\n\n return indexOf(rows, conflicts, refused);\n}\n\n/** What one crawled page's `<head>` declares about its cluster. */\ninterface HeadDeclaration {\n /** Normalised alternate targets, minus the page itself and minus x-default. */\n peers: Set<string>;\n /**\n * The declared `x-default` target, or null when the page declares none — or\n * declares two different ones, which is a contradiction we refuse to resolve.\n */\n xDefault: string | null;\n}\n\nfunction readHeads(pages: readonly Page[]): Map<string, HeadDeclaration> {\n const seen = new Map<string, HeadDeclaration>();\n\n for (const page of pages) {\n const self = normalise(page.fetch.finalUrl);\n if (self === null || seen.has(self)) continue;\n\n const peers = new Set<string>();\n const xDefaults = new Set<string>();\n for (const alt of page.links.alternates) {\n const href = normalise(alt.href);\n if (href === null) continue;\n if (alt.isXDefault) xDefaults.add(href);\n else if (href !== self) peers.add(href);\n }\n\n seen.set(self, { peers, xDefault: xDefaults.size === 1 ? [...xDefaults][0]! : null });\n }\n\n return seen;\n}\n\n/**\n * Build the cluster index from reciprocal `<head>` alternates.\n *\n * An edge exists between two **crawled** pages only when each one's `<head>`\n * points at the other. A one-sided declaration asserts an identity its supposed\n * peer does not confirm, and acting on it is the silent-merge surface §6\n * refused; requiring reciprocity is what makes this safe enough to run\n * alongside the sitemap rather than instead of it.\n *\n * A connected component merges only when its members name **one** `x-default`\n * and that target is itself a member. The second half is not decoration:\n * pointing `x-default` at the site's home page is a common field mistake, and\n * without the membership test every page on such a site would try to merge onto\n * the `/` row and collapse the whole audit into one route. With it, the home\n * page is not reciprocally linked to `/en/pricing`, so nothing merges — the\n * guard is structural rather than a list of special cases.\n *\n * On a site with no `hreflang` at all this is a no-op by construction: no\n * alternates, no edges, no clusters. It neither fixes nor hides the founding\n * bug — `hreflang.missing` reads `alternates.length` and is untouched — and\n * like the sitemap index it only ever moves a cell, so it cannot fill a hole.\n */\nexport function buildHeadClusterIndex(pages: readonly Page[]): ClusterIndex {\n const seen = readHeads(pages);\n\n // Union-find over reciprocal edges only.\n const parent = new Map<string, string>();\n for (const url of seen.keys()) parent.set(url, url);\n const find = (start: string): string => {\n let root = start;\n while (parent.get(root) !== root) root = parent.get(root)!;\n let cursor = start;\n while (parent.get(cursor) !== root) {\n const next = parent.get(cursor)!;\n parent.set(cursor, root);\n cursor = next;\n }\n return root;\n };\n\n for (const [url, decl] of seen) {\n for (const peer of decl.peers) {\n // Reciprocity, both halves required: the peer must have been crawled,\n // and it must point back.\n if (!seen.get(peer)?.peers.has(url)) continue;\n const [a, b] = [find(url), find(peer)];\n if (a !== b) parent.set(a, b);\n }\n }\n\n const components = new Map<string, string[]>();\n for (const url of seen.keys()) {\n const root = find(url);\n const members = components.get(root);\n if (members) members.push(url);\n else components.set(root, [url]);\n }\n\n const rows = new Map<string, string>();\n let refused = 0;\n\n for (const members of components.values()) {\n // A page alone in its component declared nothing reciprocal; there is no\n // cluster to name, and nothing to refuse either.\n if (members.length < 2) continue;\n\n const declared = new Set(\n members.map((m) => seen.get(m)?.xDefault).filter((x): x is string => Boolean(x)),\n );\n // Members that declare no `x-default` do not veto the merge — that is\n // `x-default-missing`'s finding, and refusing here would punish the same\n // page twice. Members that disagree about it do.\n if (declared.size !== 1) {\n refused++;\n continue;\n }\n\n const anchor = [...declared][0]!;\n if (!members.includes(anchor)) {\n refused++;\n continue;\n }\n\n const { route } = splitRoute(new URL(anchor).pathname);\n for (const member of members) rows.set(member, route);\n }\n\n // A URL belongs to exactly one component, so this index cannot contradict\n // itself. Contradiction only becomes possible against the sitemap.\n return indexOf(rows, [], refused);\n}\n\n/**\n * Combine two cluster indexes, `primary` winning wherever both answer.\n *\n * Used to put the sitemap ahead of the `<head>`: the sitemap's declaration\n * survives sampling and the `<head>`'s does not, so where the two disagree the\n * one that saw the whole site decides. The disagreement is recorded as a\n * conflict rather than resolved quietly, exactly as two sitemap entries\n * claiming one URL already are.\n */\nexport function combineClusterIndexes(\n primary: ClusterIndex,\n secondary: ClusterIndex,\n): ClusterIndex {\n const rows = new Map(secondary.rows);\n const conflicts = [...primary.conflicts];\n const add = (url: string) => {\n if (!conflicts.includes(url)) conflicts.push(url);\n };\n for (const url of secondary.conflicts) add(url);\n\n for (const [url, route] of primary.rows) {\n const claimed = rows.get(url);\n if (claimed !== undefined && claimed !== route) add(url);\n rows.set(url, route);\n }\n\n return indexOf(rows, conflicts, primary.refused + secondary.refused);\n}\n\n/**\n * How many clusters exist only because the `<head>` declared them — the rows\n * `secondary` supplies for URLs `primary` never mentioned.\n *\n * Reported rather than inferred from a difference of totals: on a site that\n * declares both ways the two indexes describe the *same* clusters, so\n * subtracting counts would claim a gain that is not there.\n */\nexport function clustersOnlyFrom(primary: ClusterIndex, secondary: ClusterIndex): number {\n const routes = new Set<string>();\n for (const [url, route] of secondary.rows) {\n if (!primary.rows.has(url)) routes.add(route);\n }\n return routes.size;\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 * The asset probe — D8 of `docs/og-plan.md`.\n *\n * Three rules need to know what is actually served at a URL a page declared:\n * `og.image.reachable`, `icons.unreachable` and `icons.sizes-mismatch`. None of\n * them can find out for themselves, because a `Rule` is a pure synchronous\n * function of one `Extraction` and that purity is what makes the whole catalogue\n * testable without a network.\n *\n * So the network happens here, once per distinct URL, in a pass the report\n * orchestrator runs between the crawl and the lint; the answers are folded back\n * into the extraction as a side table the rules look up. The alternative\n * considered and rejected was widening the link auditor: an `og:image` is a\n * `<meta>`, never a link, so it would have had to grow a second input anyway —\n * and rules would have had to read a report section instead of their own model.\n *\n * **Only the head of the file is read.** A `Range` request asks for the first\n * few kilobytes, and a server that ignores it has its body cancelled after the\n * same number of bytes. Reachability needs a status; the intrinsic size needs a\n * header and no more. Downloading a 900 KB preview image to learn it is 1200\n * pixels wide would be a rude way to audit somebody's site.\n */\n\nimport type { AssetProbe, AssetSize } from \"../types\";\nimport { combineSignals } from \"./abort\";\n\nexport interface AssetProbeOptions {\n signal?: AbortSignal;\n timeoutMs?: number;\n}\n\n/**\n * Enough for any header this decodes: a PNG states its size in the first 24\n * bytes, and an ICO directory is 6 + 16 bytes per image.\n */\nconst HEAD_BYTES = 8 * 1024;\n\n/**\n * Whether a content type names something a client would render as an image.\n *\n * `image/*` covers the honest answers. ICO is spelled four ways in the wild —\n * `image/x-icon` is what most servers send and `image/vnd.microsoft.icon` is\n * the registered one — and neither is guaranteed to survive a proxy with\n * opinions, hence the two `application/` spellings.\n */\nexport function isImageContentType(contentType: string | undefined): boolean {\n if (!contentType) return false;\n return (\n contentType.startsWith(\"image/\") ||\n contentType === \"application/ico\" ||\n contentType === \"application/x-ico\"\n );\n}\n\n/** Ask what is served at `url`, and read only its header. */\nexport async function probeAsset(\n url: string,\n options: AssetProbeOptions = {},\n): Promise<AssetProbe> {\n const { signal, cleanup } = combineSignals(options.signal, options.timeoutMs ?? 8_000);\n\n try {\n const res = await fetch(url, {\n signal,\n redirect: \"follow\",\n headers: { range: `bytes=0-${HEAD_BYTES - 1}`, accept: \"image/*,*/*;q=0.8\" },\n });\n\n const contentType = res.headers.get(\"content-type\")?.split(\";\")[0]?.trim().toLowerCase();\n // 206 is the answer to the Range above; 200 means the server ignored it and\n // is sending the whole file, which `readHead` stops reading.\n const served = res.status === 200 || res.status === 206;\n const ok = served && isImageContentType(contentType);\n\n const head = served ? await readHead(res) : undefined;\n const sizes = head ? decodeSizes(head) : undefined;\n\n return { url, status: res.status, ok, contentType, ...(sizes ? { sizes } : {}) };\n } catch {\n return { url, status: 0, ok: false };\n } finally {\n cleanup();\n }\n}\n\n/** Read at most `HEAD_BYTES` from the body, then hang up. */\nasync function readHead(res: Response): Promise<Buffer | undefined> {\n if (!res.body) return undefined;\n\n const reader = res.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n try {\n while (total < HEAD_BYTES) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n chunks.push(value);\n total += value.byteLength;\n }\n }\n } catch {\n return chunks.length > 0 ? Buffer.concat(chunks) : undefined;\n } finally {\n // The point of the whole exercise: stop the transfer rather than let the\n // rest of a preview image arrive for nothing.\n await reader.cancel().catch(() => undefined);\n }\n\n // Trimmed, not merely stopped. The loop above ends *after* the chunk that\n // crosses the threshold, so a server sending one large chunk hands back far\n // more than `HEAD_BYTES` — which made the JPEG walk's answer depend on how\n // the body happened to be framed on the wire. PNG and ICO never noticed,\n // their headers being at a fixed offset.\n return chunks.length > 0 ? Buffer.concat(chunks).subarray(0, HEAD_BYTES) : undefined;\n}\n\nconst PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);\n\n/**\n * The intrinsic sizes a file declares about itself.\n *\n * PNG, ICO and JPEG. The first two are a fixed offset; the third is a marker\n * walk, and it was left out with a condition attached — \"work to add the day a\n * site declares `sizes` on one\". `og.image.sizes-mismatch` is that day: it\n * compares a declared `og:image:width`/`height` against the file, and a site\n * declaring those on a JPEG cover is the ordinary case rather than the exotic\n * one. Without the walk that rule answers `na` on exactly the file most worth\n * catching — stereo-house ships a 337-byte 1×1 placeholder declared 1200×630.\n *\n * SVG is still out, and for a different reason than effort: it frequently\n * declares no pixel size at all, so there is often nothing to read.\n *\n * Returning nothing for the rest stays the honest answer: a rule that receives\n * no size answers `na` instead of guessing.\n */\nconst JPEG_SOI = 0xffd8;\n\n/**\n * The frame headers that carry a picture's dimensions.\n *\n * `SOF0`–`SOF15` minus the three markers that share the range and are not\n * frames at all: `C4` is a Huffman table, `C8` is reserved, `CC` is an\n * arithmetic-coding table. Reading a length out of one of those and calling it\n * a size is how a decoder returns confident nonsense.\n */\nconst JPEG_FRAME_MARKERS = new Set([\n 0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,\n]);\n\n/**\n * Walk the segments to the first frame header.\n *\n * A JPEG states its size in a segment whose offset nothing declares: EXIF, ICC\n * profiles and thumbnails all sit in front of it, at whatever length they\n * please. So it is a walk — each segment is `FF <marker> <length:2>`, and the\n * length counts itself. Standalone markers carry no length, and `SOS` is where\n * entropy-coded data begins and the segment structure ends.\n *\n * Everything here gives up rather than guesses: a truncated header, a scan\n * reached before a frame, or a nonsensical length all return `undefined`.\n */\nfunction decodeJpegSize(head: Buffer): AssetSize | undefined {\n let at = 2;\n\n while (at + 3 < head.length) {\n if (head[at] !== 0xff) return undefined;\n\n const marker = head[at + 1]!;\n // Fill bytes: any number of `FF` may pad the gap before a marker.\n if (marker === 0xff) {\n at += 1;\n continue;\n }\n // Standalone: SOI, TEM and the eight restart markers carry no payload.\n if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {\n at += 2;\n continue;\n }\n // EOI, or the start of the scan. Either way there is no frame ahead.\n if (marker === 0xd9 || marker === 0xda) return undefined;\n\n const length = head.readUInt16BE(at + 2);\n if (length < 2) return undefined;\n\n if (JPEG_FRAME_MARKERS.has(marker)) {\n // `FF <marker> <length:2> <precision:1> <height:2> <width:2>`\n if (at + 9 > head.length) return undefined;\n return { width: head.readUInt16BE(at + 7), height: head.readUInt16BE(at + 5) };\n }\n\n at += 2 + length;\n }\n\n return undefined;\n}\n\nfunction decodeSizes(head: Buffer): AssetSize[] | undefined {\n if (head.length >= 24 && head.subarray(0, 8).equals(PNG_SIGNATURE)) {\n return [{ width: head.readUInt32BE(16), height: head.readUInt32BE(20) }];\n }\n\n if (head.length >= 4 && head.readUInt16BE(0) === JPEG_SOI) {\n const size = decodeJpegSize(head);\n if (size) return [size];\n // Fell off the end of the header, or hit the scan first. Unknown, which is\n // not the same as a mismatch.\n return undefined;\n }\n\n // ICONDIR: reserved 0, type 1 (icon), then the image count.\n if (head.length >= 6 && head.readUInt16LE(0) === 0 && head.readUInt16LE(2) === 1) {\n const count = head.readUInt16LE(4);\n if (count === 0 || head.length < 6 + 16 * count) return undefined;\n\n const sizes: AssetSize[] = [];\n for (let index = 0; index < count; index++) {\n const at = 6 + 16 * index;\n // A zero byte means 256 — the format has one byte per dimension.\n sizes.push({ width: head[at] || 256, height: head[at + 1] || 256 });\n }\n return sizes;\n }\n\n return undefined;\n}\n","import type { FaviconProbe } from \"../types\";\nimport { combineSignals } from \"./abort\";\n\nexport interface FaviconProbeOptions {\n signal?: AbortSignal;\n timeoutMs?: number;\n}\n\n/**\n * Ask the origin for `/favicon.ico`.\n *\n * `HEAD` first, because the answer is a status and a content type and the\n * bytes are nobody's business here; a `GET` follows when the server rejects\n * `HEAD`, which some do with 405 or 501. Nothing reads the body either way.\n *\n * **A 200 is not enough.** A site that serves its SPA shell for every unknown\n * path answers 200 with `text/html`, and treating that as an icon would make\n * this rule silently pass on exactly the sites it exists for. So the content\n * type has to say image — the same soft-404 reasoning the link checker\n * already applies to pages.\n */\nexport async function probeFavicon(\n origin: string,\n options: FaviconProbeOptions = {},\n): Promise<FaviconProbe> {\n const url = new URL(\"/favicon.ico\", origin).toString();\n const { signal, cleanup } = combineSignals(options.signal, options.timeoutMs ?? 5_000);\n\n try {\n let res = await fetch(url, { method: \"HEAD\", signal, redirect: \"follow\" });\n if (res.status === 405 || res.status === 501) {\n res = await fetch(url, { signal, redirect: \"follow\" });\n }\n\n const contentType = res.headers.get(\"content-type\")?.split(\";\")[0]?.trim().toLowerCase();\n\n return {\n url,\n status: res.status,\n found: res.ok && isIconType(contentType),\n contentType,\n };\n } catch {\n return { url, status: 0, found: false };\n } finally {\n cleanup();\n }\n}\n\n/**\n * Whether a content type names something a client would accept as an icon.\n *\n * `image/*` covers the honest answers, and the ICO type is spelled four\n * different ways in the wild — `image/x-icon` is what most servers send,\n * `image/vnd.microsoft.icon` is the registered one, and neither is guaranteed\n * to start with `image/` once a proxy has had an opinion.\n */\nfunction isIconType(contentType: string | undefined): boolean {\n if (!contentType) return false;\n return (\n contentType.startsWith(\"image/\") ||\n contentType === \"application/ico\" ||\n contentType === \"application/x-ico\"\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 * Cross-page questions — the site-wide half of the prose family.\n *\n * `./prose.ts` asks a page a question and hands an agent the evidence. This\n * asks the *site* one. The mechanism exists because a rule was rendering a\n * verdict without saying by what authority: `hreflang.sitemap-mismatch` carried\n * `rigor: null` **and** `severity: warning`, which is a refusal to say how\n * authoritative a claim is, followed by the claim. A warning *is* a verdict.\n *\n * ## Why the evidence is computed here and resolved there\n *\n * A `ProseRule` declares dotted paths and the runner resolves them out of the\n * `Extraction`. That is deliberate: a rule cannot smuggle a judgment into\n * evidence it did not compute. A site question cannot work that way — what an\n * agent needs is *which locales the two artefacts disagree about*, and no path\n * into a `SiteContext` holds that; only a comparison produces it.\n *\n * So the guarantee moves from mechanism to contract: **an evidence bundle holds\n * observations, never conclusions.** Two sorted lists of locale tags are\n * evidence. A field called `isWrong` would not be, and neither would a sentence.\n * The `prose` string is where the question goes, and it is the only place a\n * judgment is invited.\n *\n * ## Why these are `scope: \"prose\"` in the catalogue and not a fourth scope\n *\n * `catalog.ts` splits rules by scope, and `apps/website` renders exactly three\n * lists off that split. A fourth value would leave these rules in none of them\n * — visible in `rules.json`, absent from the documentation, which is the silent\n * gap this repository keeps finding and closing. What matters about a prose\n * rule is that it renders **no verdict**; where it looked is a detail its own\n * summary carries.\n */\n\nimport { splitRoute } from \"../core/i18n\";\nimport type { SiteContext } from \"./site-types\";\nimport type { AdvisoryFinding, Rigor } from \"./types\";\n\n/** One question, asked about one URL, with the observations behind it. */\nexport interface SiteAdvisoryDraft {\n /** The page a human would open to answer it. */\n pageUrl: string;\n /**\n * Observations only. See the contract above: this bundle is what an agent\n * reasons over, so a conclusion placed here would be goflag making the\n * judgment it declined to make.\n */\n evidence: Record<string, unknown>;\n}\n\n/**\n * A cross-page question. The shape mirrors `SiteRule` minus everything that\n * expresses a verdict — no `severity`, no `check` returning issues.\n */\nexport interface SiteProseRule {\n id: string;\n /** One-line statement of what is being asked, for the catalogue. */\n summary: string;\n /** The question itself, in the second person, with a truth value. */\n prose: string;\n /** Why it is worth asking, in the registry's own words. */\n why?: string;\n /**\n * Left unset where no document supports the question — which is the case\n * this mechanism was built for, and the honest reading rather than a gap.\n */\n rigor?: Rigor;\n sources?: string[];\n /** Gate on the subject existing, exactly as `SiteRule.appliesTo` does. */\n appliesTo?: (site: SiteContext) => boolean;\n ask: (site: SiteContext) => SiteAdvisoryDraft[];\n}\n\n/**\n * The `<head>` advertises a locale the sitemap does not list.\n *\n * This was a `SiteRule` until 2026-08-15, and it is the reason this file\n * exists. Splitting `hreflang.sitemap-mismatch` sourced one half —\n * `hreflang.cluster-incomplete`, which Google's reciprocity requirement backs —\n * and left this one with nothing behind it. Checked at the source rather than\n * assumed: Google calls its three declaration methods *\"equivalent from\n * Google's perspective\"*, discourages combining them (*\"there's no benefit in\n * Search\"*), and nowhere requires an hreflang-declared page to appear in a\n * sitemap at all.\n *\n * A page correctly cross-linked and deliberately kept out of the sitemap is\n * doing nothing wrong. But two generators deriving one intent and disagreeing\n * is a defect somewhere — and goflag cannot say which of the two is right.\n * That is precisely a question, so it is asked as one.\n */\nconst hreflangSitemapMismatch: SiteProseRule = {\n id: \"hreflang.sitemap-mismatch\",\n summary: \"`<head>` alternates advertise a locale the sitemap does not list\",\n prose:\n \"Your `<head>` advertises a translation your sitemap does not list. Is the sitemap \" +\n \"missing an entry, or is the alternate pointing at a page you did not mean to publish?\",\n why:\n \"The two are derived from one intent by different code paths, so a disagreement means \" +\n \"one of them is wrong. No specification says which: the declaration methods are \" +\n \"equivalent, and a page can legitimately be cross-linked and kept out of the sitemap.\",\n appliesTo: (site) => site.localeAxis.multilingual && (site.discovery?.urls.length ?? 0) > 0,\n ask: (site) => {\n const axis = new Set(site.localeAxis.locales.map((l) => l.toLowerCase()));\n const bySitemap = new Map<string, Set<string>>();\n\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 { locale } = splitRoute(pathname);\n if (locale === \"x-default\") continue;\n // Same guard as the sourced half, for the same reason: `splitRoute` reads\n // a segment by shape alone, and `api`, `doc` and `www` all pass it.\n if (!axis.has(locale.toLowerCase())) continue;\n const route = site.clusterRouteOf?.(entry.loc) ?? splitRoute(pathname).route;\n const set = bySitemap.get(route) ?? new Set<string>();\n set.add(locale.toLowerCase());\n bySitemap.set(route, set);\n }\n\n const drafts: SiteAdvisoryDraft[] = [];\n\n for (const page of site.pages) {\n const head = 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) head.add(tag);\n }\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 = site.clusterRouteOf?.(page.fetch.finalUrl) ?? splitRoute(pathname).route;\n const inSitemap = bySitemap.get(route);\n if (!inSitemap || inSitemap.size === 0) continue;\n\n const unlisted = [...head].filter((l) => !inSitemap.has(l)).sort();\n if (unlisted.length === 0) continue;\n\n drafts.push({\n pageUrl: page.fetch.finalUrl,\n evidence: {\n route,\n // Both sides, verbatim and sorted, so an agent can see the shape of\n // the disagreement rather than take our word for its size.\n headAdvertises: [...head].sort(),\n sitemapLists: [...inSitemap].sort(),\n advertisedButUnlisted: unlisted,\n },\n });\n }\n\n return drafts;\n },\n};\n\nexport const SITE_PROSE_RULES: readonly SiteProseRule[] = [hreflangSitemapMismatch];\n\nexport function getSiteProseRule(id: string): SiteProseRule | undefined {\n return SITE_PROSE_RULES.find((rule) => rule.id === id);\n}\n\n/**\n * Run the site questions and bundle their evidence.\n *\n * Mirrors `collectAdvisories` in `./advisory.ts`, including its verdict: every\n * entry is `needs-judgment`, always. A site advisory never reaches\n * `siteIssues`, never moves the summary counts, and never touches the exit\n * code — the same containment the page advisories have, and the whole point of\n * moving a rule here.\n */\nexport function collectSiteAdvisories(\n site: SiteContext,\n rules: readonly SiteProseRule[] = SITE_PROSE_RULES,\n): (AdvisoryFinding & { pageUrl: string })[] {\n const out: (AdvisoryFinding & { pageUrl: string })[] = [];\n\n for (const rule of rules) {\n if (rule.appliesTo && !rule.appliesTo(site)) continue;\n for (const draft of rule.ask(site)) {\n out.push({\n ruleId: rule.id,\n kind: \"prose\",\n prose: rule.prose,\n // `null` where nothing supports the question, which is exactly the\n // state this rule is in and the reason it stopped being a verdict.\n rigor: rule.rigor ?? null,\n sources: rule.sources ?? [],\n evidence: draft.evidence,\n verdict: \"needs-judgment\",\n pageUrl: draft.pageUrl,\n });\n }\n }\n\n return out;\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 * 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 // The origin is exempt, and the scan phase above is the precedent: it\n // fetches these same pages at `scanConcurrency` with `perHost: Infinity`.\n // Capping the check phase at 3 against that host made the two phases\n // disagree about the same server, and on a deployed site it is where the\n // audit spends itself: measured on openfinanceguide, 3821 same-origin\n // links at ~700 ms a round trip, three at a time — 14.8 minutes of an\n // 18.5-minute job. Against a local `--start` run the cap never binds,\n // which is why the plan's own 7-minute figure held and CI's did not.\n //\n // `checkConcurrency` still bounds it: at most 8 in flight, where the scan\n // phase already runs 4.\n unlimitedHost: hostOf(discovery.origin),\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 /**\n * One host exempt from `perHost` — the origin under audit. Politeness is\n * owed to other people's servers; the origin is the site the caller pointed\n * this tool at, and the scan phase already fetches it with no per-host cap\n * at all.\n */\n unlimitedHost?: string;\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 const cap = entry.host === opts.unlimitedHost ? Infinity : perHost;\n if ((hostActive.get(entry.host) ?? 0) < cap) {\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 { ICU_KNOWS_LANGUAGES, ICU_UNAVAILABLE_WARNING } from \"../lib/core/bcp47\";\nimport {\n buildI18nMatrix,\n countUnverifiedAlternates,\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 { probeSitemapEntries } from \"../lib/core/sitemap/entries\";\nimport {\n buildClusterIndex,\n buildHeadClusterIndex,\n clustersOnlyFrom,\n combineClusterIndexes,\n} from \"../lib/core/clusters\";\nimport { selectByStructure } from \"../lib/core/coverage\";\nimport { probeAsset } from \"../lib/core/probes/assets\";\nimport { probeFavicon } from \"../lib/core/probes/favicon\";\nimport { probeManifest } from \"../lib/core/probes/manifest\";\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 type { Extraction } from \"../lib/rules/extraction/types\";\nimport { DEFAULT_PROFILE, rulesForProfile } from \"../lib/rules/profiles\";\nimport { PROSE_RULES } from \"../lib/rules/prose\";\nimport { collectSiteAdvisories } from \"../lib/rules/site-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 { AssetProbe, ManifestProbe, 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 * Emit each page's extraction — what its `<head>` declared, as the rules\n * read it. Off by default: it is the observation, not a finding, and on a\n * large crawl it is the biggest section of the report.\n *\n * `goflag preview` sets it, because a share card is drawn from the\n * declarations and not from the verdicts about them.\n *\n * Unlike `conformance` and `advisories`, this section stops at the report:\n * `summarize()` does not carry it — a summary of an observation is the\n * observation with the useful parts removed — and no terminal renderer\n * prints it. Its one reader is `renderPreview`.\n */\n extractions?: 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 * The warning owed by pages that asked for the browser and did not get it.\n *\n * A page whose `<head>` looks empty is re-rendered in Chromium precisely so a\n * client-rendered app is not reported as missing everything it declares at\n * runtime. When Playwright or its binary is absent the escalation is skipped\n * and the page is judged on its unhydrated shell — which produces the exact\n * phantom findings the escalation exists to prevent, a full column of\n * `title.missing` and `description.missing` down an SPA.\n *\n * Until now the reason was written onto the `Page` and copied nowhere: not into\n * the report, not into `warnings`, not onto the terminal. Somebody reading\n * \"missing everything\" had no way to tell a broken site from a missing browser,\n * and the install docs told them to wait for a note that was never printed.\n *\n * Returns `null` when nothing was blocked, so the caller adds no empty noise.\n */\nexport function blockedEscalationWarning(pages: readonly Page[]): string | null {\n const blocked = pages.filter((p) => p.extractor.escalationBlocked);\n if (blocked.length === 0) return null;\n\n // One message, not one per page: the cause is the runtime, so N copies of it\n // would bury every other warning on a site that is entirely client-rendered.\n return (\n `${blocked.length} page(s) looked client-rendered and were judged on their static ` +\n `HTML, because the browser could not be started: ${blocked[0]!.extractor.escalationBlocked} ` +\n `Their metadata findings may be phantoms — install Playwright and Chromium, or pass ` +\n `--static to say the static HTML is what you meant to judge.`\n );\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 */\n/**\n * Fetch each distinct `<link rel=\"manifest\">` once and hand the result to the\n * pages that declared it.\n *\n * The pages are freshly built by the crawl and nothing has read them yet, so\n * attaching the probe here is the cheapest place to put site knowledge in\n * front of a rule that must stay a pure function of one page. A failed fetch\n * is deliberately left absent rather than recorded as an empty manifest: the\n * rules distinguish \"never looked at\" from \"looked at and found nothing\", and\n * only the second is evidence.\n */\nasync function attachManifests(\n pages: Page[],\n options: { signal?: AbortSignal; timeoutMs?: number },\n): Promise<void> {\n const wanted = new Set(\n pages.map((page) => page.links.manifest?.href).filter((href): href is string => Boolean(href)),\n );\n if (wanted.size === 0) return;\n\n const probed = new Map<string, ManifestProbe>();\n await Promise.all(\n [...wanted].map(async (href) => {\n const probe = await probeManifest(href, options).catch(() => undefined);\n if (probe) probed.set(href, probe);\n }),\n );\n\n for (const page of pages) {\n const href = page.links.manifest?.href;\n const probe = href ? probed.get(href) : undefined;\n if (probe) page.probes = { ...page.probes, manifest: probe };\n }\n}\n\n/**\n * Probe every distinct asset URL the pages declared, and hand the answers back.\n *\n * D8 of `docs/og-plan.md`. Three rules — `og.image.reachable`,\n * `icons.unreachable`, `icons.sizes-mismatch` — need to know what is served at\n * a URL, and none of them can find out: a `Rule` is a pure synchronous function\n * of one `Extraction`, and keeping it that way is what makes the catalogue\n * testable with no network at all. So the network happens here.\n *\n * Deduplicated across the whole run, because the same icon is declared by every\n * page on the site and the same card by every page of a family. On this\n * repository's own site that is two URLs for seventy-four pages.\n *\n * Relative `og:image` values are skipped rather than resolved: a crawler cannot\n * resolve them either, which is the entire content of `og.image.absolute`, and\n * fetching one here would let this pass answer a question the tag does not.\n *\n * `--no-external` is honoured, exactly as the link auditor honours it. A card\n * on a CDN is legitimately off-origin, and a run told not to touch other\n * people's servers must not touch them for an image either.\n */\nasync function probeAssets(\n pages: Page[],\n options: {\n origin: string;\n checkExternal: boolean;\n signal?: AbortSignal;\n timeoutMs?: number;\n concurrency?: number;\n },\n): Promise<void> {\n const sameOrigin = (url: string): boolean => {\n try {\n return new URL(url).origin === new URL(options.origin).origin;\n } catch {\n return false;\n }\n };\n const wantedUrl = (url: string): boolean =>\n /^https?:\\/\\//i.test(url) && (options.checkExternal || sameOrigin(url));\n const wanted = new Set<string>();\n for (const page of pages) {\n for (const image of page.openGraph.images) {\n const url = image.url.value.trim();\n if (wantedUrl(url)) wanted.add(url);\n }\n for (const icon of page.links.icons) {\n if (wantedUrl(icon.href)) wanted.add(icon.href);\n }\n }\n if (wanted.size === 0) return;\n\n const queue = [...wanted];\n const probed = new Map<string, AssetProbe>();\n const workers = Math.min(options.concurrency ?? 6, queue.length);\n\n await Promise.all(\n Array.from({ length: workers }, async () => {\n for (let url = queue.pop(); url !== undefined; url = queue.pop()) {\n const probe = await probeAsset(url, {\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n }).catch(() => undefined);\n if (probe) probed.set(url, probe);\n }\n }),\n );\n\n for (const page of pages) {\n const mine: Record<string, AssetProbe> = {};\n for (const image of page.openGraph.images) {\n const probe = probed.get(image.url.value.trim());\n if (probe) mine[probe.url] = probe;\n }\n for (const icon of page.links.icons) {\n const probe = probed.get(icon.href);\n if (probe) mine[probe.url] = probe;\n }\n // An empty map still says \"the pass ran and found nothing to fetch\", which\n // is what the rules need to tell apart from \"no pass ran\".\n page.assets = mine;\n }\n}\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 // Built from the WHOLE sitemap, never from the selection: structural\n // coverage decides which pages get cells, never which clusters exist, and a\n // `<url>` entry names its whole cluster whether or not a member was sampled.\n // That is the property that makes a sitemap declaration usable where pairing\n // from the crawled `<head>`s is not — the two locales of a slug-translating\n // family draw disjoint samples (docs/i18n-cluster-plan.md §2). The `<head>`\n // index joins it after the crawl, below, and only where this one is silent.\n const sitemapClusters = buildClusterIndex(discovery?.urls ?? []);\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 // `/favicon.ico` is the same shape of fact as robots.txt: one file, one\n // path, one answer for the whole origin. Asked once here rather than per\n // page, for the same reason.\n const favicon = await probeFavicon(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 // Said out loud rather than absorbed. A retry that nobody hears about turns\n // \"the site answered\" and \"the site answered the second time we asked\" into\n // the same report, and the difference is the whole reason a page went\n // missing from a baseline in the first place. It does not gate — the page was\n // audited, there is no hole — but a number that climbs run after run is a\n // site getting slower, and this is where you would see it.\n if (crawlResult.recovered.length > 0) {\n warnings.push(\n `${crawlResult.recovered.length} page(s) failed once and answered on retry: ` +\n `${crawlResult.recovered.slice(0, 3).join(\", \")}` +\n `${crawlResult.recovered.length > 3 ? `, +${crawlResult.recovered.length - 3} more` : \"\"}.`,\n );\n }\n\n const escalationWarning = blockedEscalationWarning(pages);\n if (escalationWarning) warnings.push(escalationWarning);\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 // --- Web App Manifest --------------------------------------------------\n //\n // The crawl runs with `probes: false`: robots.txt and the sitemap belong to\n // the origin and are fetched once above, so probing them per page would be\n // N copies of one answer. The manifest is the exception — it is declared by\n // the page, nothing above fetches it, and `icons.manifest-mismatch` is a\n // judgment on what it says. Without this the rule could only ever answer\n // `na`, which is the \"written, tested, called by nobody\" failure this\n // repository has now recorded six times.\n //\n // De-duplicated by URL, because in practice every page names the same\n // manifest and a finding is not worth fetching one file five hundred times.\n await attachManifests(htmlPages, { signal: options.signal, timeoutMs: options.timeoutMs });\n\n // --- Declared assets ----------------------------------------------------\n //\n // The one place a rule's subject is fetched rather than parsed. See\n // `probeAssets` for why it lives here and not inside a rule.\n await probeAssets(htmlPages, {\n origin,\n checkExternal: options.checkExternal ?? true,\n signal: options.signal,\n timeoutMs: options.timeoutMs,\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 const extractions: Extraction[] = [];\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.extractions) {\n extractions.push(extraction);\n }\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 // The fingerprint above is unchanged on purpose: it keys on the rule,\n // the route and the occurrence, so carrying more of the finding cannot\n // move a baseline entry (`./diff.ts` matches on `id` alone).\n rigor: issue.rigor,\n sources: issue.sources,\n observed: issue.observed,\n expected: issue.expected,\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 // `locale.invalid` asks ICU whether a tag names a language that exists. On a\n // small-icu Node it cannot, and falls back to checking the shape — which is\n // the right call for an auditor, since the alternative is a page of findings\n // that are all false. It is not the right call to keep quiet about: a check\n // that stopped checking has to say so, like every other partial answer in\n // this report.\n if (!ICU_KNOWS_LANGUAGES) warnings.push(ICU_UNAVAILABLE_WARNING);\n\n // The second pairing source, and the one most correct sites actually use:\n // reciprocal `hreflang` in the crawled `<head>`s. It needs both members in\n // hand, so it does nothing where sampling splits a family — which is exactly\n // where the sitemap declaration works, and why the two are complementary\n // rather than redundant (docs/i18n-cluster-plan.md §9). The sitemap wins\n // wherever both answer.\n const headClusters = buildHeadClusterIndex(htmlPages);\n const clusters = combineClusterIndexes(sitemapClusters, headClusters);\n const clustersFromHead = clustersOnlyFrom(sitemapClusters, headClusters);\n\n const matrix = buildI18nMatrix(htmlPages, {\n declaredUrls: sitemapUrls,\n locales: localeAxis.locales,\n clusterRouteOf: clusters.routeOf,\n });\n // A site that puts one URL in two clusters is contradicting itself, and the\n // first declaration wins silently unless this says otherwise.\n if (clusters.conflicts.length > 0) {\n warnings.push(\n `${clusters.conflicts.length} URL(s) are declared in two different hreflang clusters by ` +\n `the sitemap and/or the pages' \\`<head>\\`; the sitemap's declaration was kept: ` +\n `${clusters.conflicts.slice(0, 3).join(\", \")}` +\n `${clusters.conflicts.length > 3 ? `, +${clusters.conflicts.length - 3} more` : \"\"}.`,\n );\n }\n // Cells nothing vouches for: filled by an `hreflang` on some page, absent\n // from the sitemap, never fetched. They fill the cell, so they hide the hole\n // they might be — and this changes no verdict, on purpose. Refusing to\n // believe them would invent holes on every site using `sitemap: false`\n // (docs/i18n-cluster-plan.md §10.3); the count is what lets that call be made\n // on a measurement later instead of an argument now.\n const unverifiedAlternates = countUnverifiedAlternates(matrix);\n if (unverifiedAlternates > 0) {\n warnings.push(\n `${unverifiedAlternates} translation${unverifiedAlternates === 1 ? \"\" : \"s\"} in the matrix ` +\n `${unverifiedAlternates === 1 ? \"is\" : \"are\"} vouched for only by an \\`hreflang\\` on ` +\n `another page — not crawled, and not in the sitemap. They are counted as present, so a ` +\n `page advertised but not served reads as translated rather than missing.`,\n );\n }\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 // --- 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 // --- What the sitemap promises -----------------------------------------\n //\n // Runs here, after the link audit, so it can skip every URL the crawl or\n // the link engine already answered. On a site whose sitemap and link graph\n // describe the same pages that is nearly all of them; where they diverge,\n // the leftovers are exactly what `sitemap.entry.unreachable` is about.\n //\n // This is also why the cross-page rules moved below the link audit: they\n // used to run before it and could not see any of this.\n const sitemapEntries = discovery\n ? await probeSitemapEntries(\n discovery.urls.map((entry) => entry.loc),\n {\n crawled: new Map(\n crawlResult.pages.map((page) => [page.fetch.finalUrl, page.fetch.status]),\n ),\n checked: new Map(Object.values(linkReport.checks).map((check) => [check.url, check])),\n maxProbes: effectiveMaxPages,\n signal: options.signal,\n timeoutMs: options.timeoutMs,\n allowInsecureTls: options.allowInsecureTls,\n },\n )\n : undefined;\n\n // --- Cross-page rules ---------------------------------------------------\n const siteContext: SiteContext = {\n origin,\n pages: htmlPages,\n matrix,\n localeAxis,\n discovery,\n robots,\n favicon,\n sitemapEntries,\n // Same index the matrix was built from, so a rule that groups by route\n // and the grid that reports holes cannot disagree about which URLs are\n // one page.\n clusterRouteOf: clusters.routeOf,\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 rigor: draft.rigor,\n sources: draft.sources,\n };\n });\n\n // Cross-page questions, on the same terms as the per-page ones: only when\n // asked for, never in `siteIssues`, never in the summary counts, never in the\n // exit code. A question that could redden a pipeline would be a verdict again.\n if (options.advisories) {\n for (const advisory of collectSiteAdvisories(siteContext)) {\n advisories.push({\n ...advisory,\n id: fingerprint(\"advisory\", advisory.ruleId, routeKey(advisory.pageUrl)),\n });\n }\n }\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 ...(options.extractions ? { extractions } : {}),\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 ...(unverifiedAlternates > 0 ? { unverifiedAlternates } : {}),\n // Only when the site declared clusters. A merge that changes which rows\n // exist must be visible: the alternative is a hole count that moved for\n // a reason nothing in the report explains.\n ...(clusters.size > 0 || clusters.refused > 0\n ? {\n declaredClusters: {\n count: clusters.size,\n ...(clusters.conflicts.length > 0 ? { conflicts: clusters.conflicts.length } : {}),\n // Which source did the work. On a site that declares both ways\n // the two indexes describe the same clusters, so this counts the\n // rows only the `<head>` supplied rather than a difference of\n // totals that would claim a gain that is not there.\n ...(clustersFromHead > 0 ? { fromHead: clustersFromHead } : {}),\n // Declared and not used, for want of an anchor to name the row\n // after. A declaration we saw and did not act on has to be said.\n ...(clusters.refused > 0 ? { refused: clusters.refused } : {}),\n },\n }\n : {}),\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 // Crawl-derived: no sitemap document was read, so the tree is empty rather\n // than absent. The document rules ask \"what did each document declare?\" and\n // must see that nothing declared anything.\n documents: [],\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 // The rigor rides here rather than on every finding in the full report:\n // a rollup is one rule, so it is one tag on one line. Without it a reader\n // deciding what to fix first cannot tell a specification from folklore.\n const rigor = issue.rigor ? c(ANSI.dim, ` [${issue.rigor}]`) : \"\";\n lines.push(` ${sev} ${c(ANSI.cyan, issue.ruleId)}${rigor} ${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 * The share-card preview — one self-contained HTML document.\n *\n * `renderPreview` is a pure function of a `GoflagReport`, exactly like the\n * three terminal renderers beside it. It reads `report.extractions`, which is\n * the only section that carries what a page *declared* rather than what a rule\n * *decided*: a page can pass every `og.*` rule and still unfurl badly, and no\n * violations list can show that.\n *\n * Two things this file refuses to do, and they are the point.\n *\n * **It draws no geometry it cannot source.** Every surface carries the same\n * rigor scale the rule catalogue uses, because the surfaces are not equally\n * documented: Facebook, LinkedIn and WhatsApp publish real numbers; Slack calls\n * its own rendering a \"micro-approximation\"; Discord publishes nothing at all;\n * X's card documentation is not reachable, and its shape changed twice since\n * 2023. A preview that renders all seven identically states seven things with\n * one confidence, and six of them are not earned.\n *\n * **It escapes everything.** The input is a site under audit, which is to say\n * untrusted text, and the output is a file a human opens in a browser. Values\n * go through `esc`; URLs that are not `http(s)` never become an `src`.\n *\n * Opening the document asks for the image URLs the pages declared, which is\n * what makes it a preview rather than a table. Those URLs are the site's own\n * choice and need not point at the audited origin, so the document ships a CSP\n * that allows images and nothing else — no script, no frame, no fetch — and\n * every image is `referrerpolicy=\"no-referrer\"`. Rendering itself never touches\n * the network.\n */\n\nimport { splitRoute } from \"../lib/core/i18n\";\nimport type { TagOrigin } from \"../lib/core/types\";\nimport type {\n Extraction,\n ExtractionAsset,\n ExtractionJsonLd,\n ExtractionOpenGraphImage,\n Fact,\n} from \"../lib/rules/extraction/types\";\nimport type { GoflagReport, SeoIssue, SiteIssue } from \"./types\";\n\n/**\n * What the rail pins. `SiteIssue` is the same shape minus `observed` /\n * `expected`, and some of its ids are head ids — see `findingsPanel`.\n */\ntype HeadFinding = SeoIssue | SiteIssue;\n\nexport interface RenderPreviewOptions {\n /** Document `<title>`. Defaults to the audited origin. */\n title?: string;\n}\n\n/** Rule id prefixes whose findings belong on a card rather than in a report. */\nconst HEAD_PREFIXES = [\"og.\", \"twitter.\", \"icons.\", \"title.\", \"description.\", \"canonical.\"];\n\n/**\n * `og.image.ratio`'s own bands, repeated here rather than imported: the rule\n * scores one number and this labels a picture, and the day the rule's bands\n * move is not automatically the day the caption should.\n */\nconst RATIO_IDEAL = [1.7, 2.1] as const;\nconst RATIO_ACCEPTABLE = [1, 3] as const;\n\nconst ESCAPES: Record<string, string> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&#39;\",\n};\n\nfunction esc(value: string): string {\n return value.replace(/[&<>\"']/g, (c) => ESCAPES[c] ?? c);\n}\n\n/**\n * Rule messages mark code spans with backticks — `render-terminal` strips them\n * because a terminal has nowhere to put them, and HTML does.\n *\n * Applied to catalogue text only, and after `esc`. Several rule messages\n * interpolate what the page said inside those backticks (`og.image.absolute`\n * quotes the URL it rejected), so this runs over escaped text on purpose: the\n * worst a site can do is put a `<code>` where it did not belong.\n */\nfunction codeSpans(escaped: string): string {\n return escaped.replace(/`([^`]+)`/g, \"<code>$1</code>\");\n}\n\n/** Cap a value echoed back into the document — a data: URI is unbounded. */\nfunction clip(value: string, max = 300): string {\n return value.length > max ? `${value.slice(0, max)}…` : value;\n}\n\n/**\n * Pretty-print a parsed JSON-LD block, or nothing.\n *\n * `JSON.stringify` is the first thing that walks this value: the extractor's\n * own type walker skips `@type`, so a page can carry a chain of them thousands\n * deep, parse cleanly, and blow the stack here. It also indents, which makes\n * the output quadratic in depth — 50 KB of hostile page measured 50 MB of HTML\n * before this cap existed. A site does not get to veto its own preview.\n */\nfunction prettyJson(value: unknown): string | undefined {\n try {\n const out = JSON.stringify(value, null, 2);\n if (out === undefined) return undefined;\n return out.length > 20_000 ? `${out.slice(0, 20_000)}\\n… truncated` : out;\n } catch {\n return undefined;\n }\n}\n\n/**\n * A URL fit to become an `src`, or nothing.\n *\n * A relative `og:image` is a finding (`og.image.absolute`), not something to\n * resolve silently — showing it as text is how the preview agrees with the rule\n * instead of hiding it.\n */\nfunction safeUrl(value: string | undefined): string | undefined {\n if (!value) return undefined;\n try {\n const url = new URL(value);\n return url.protocol === \"http:\" || url.protocol === \"https:\" ? url.href : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction hostOf(value: string): string {\n try {\n return new URL(value).host;\n } catch {\n return value;\n }\n}\n\nfunction pathOf(value: string): string {\n try {\n const { pathname, search } = new URL(value);\n return `${pathname}${search}`;\n } catch {\n return value;\n }\n}\n\n/** Name the tag a value came from, in the words the page would have used. */\nfunction originLabel(origin: TagOrigin): string {\n switch (origin.kind) {\n case \"title\":\n return \"<title>\";\n case \"meta\":\n if (origin.property) return origin.property;\n if (origin.name) return origin.name;\n if (origin.httpEquiv) return `http-equiv=${origin.httpEquiv}`;\n return \"<meta>\";\n case \"link\":\n return `<link rel=\"${origin.rel}\">`;\n case \"html\":\n return `<html ${origin.attribute}>`;\n case \"json-ld\":\n return `JSON-LD #${origin.index}`;\n case \"header\":\n return `header: ${origin.name}`;\n case \"computed\":\n return \"computed\";\n }\n}\n\n/** One value with the tag it came from, so a fallback is visible as a fallback. */\ninterface Resolved {\n value: string;\n from: string;\n}\n\nfunction resolve(...facts: Array<Fact<string> | undefined>): Resolved | undefined {\n for (const fact of facts) {\n if (fact && fact.value.trim() !== \"\") {\n return { value: fact.value, from: originLabel(fact.origin) };\n }\n }\n return undefined;\n}\n\ninterface CardImage {\n /** What the page said, verbatim. */\n declared: string;\n /** The same URL when it is safe to load; absent for anything else. */\n src?: string;\n alt?: string;\n from: string;\n /** How many `og:image` the page declared; consumers take the first. */\n count: number;\n declaredSize?: { width: number; height: number };\n probedSize?: { width: number; height: number };\n probe?: ExtractionAsset;\n ratio?: number;\n}\n\ninterface Card {\n url: string;\n host: string;\n path: string;\n /** What each surface shows, already resolved through its own fallbacks. */\n ogTitle?: Resolved;\n ogDescription?: Resolved;\n documentTitle?: Resolved;\n metaDescription?: Resolved;\n siteName?: Resolved;\n xTitle?: Resolved;\n image?: CardImage;\n icon?: string;\n themeColor?: string;\n twitterCard?: string;\n locale?: string;\n alternates: string[];\n jsonLd: ExtractionJsonLd[];\n rendering: Extraction[\"rendering\"];\n status: number;\n}\n\n/** A usable pair, or nothing: a CMS that means \"unknown\" writes 0, not absent. */\nfunction usable(size: { width: number; height: number } | undefined) {\n return size && size.width > 0 && size.height > 0 ? size : undefined;\n}\n\nfunction imageOf(extraction: Extraction): CardImage | undefined {\n const og: ExtractionOpenGraphImage | undefined = extraction.openGraph.images[0];\n // `content=\"\"` is kept verbatim by the extractor, so `??` would let an empty\n // `og:image` hide a perfectly good `twitter:image` behind it.\n const ogUrl = og?.url.value.trim() ? og.url.value : undefined;\n const declared = ogUrl ?? extraction.twitter.image?.value;\n if (!declared || declared.trim() === \"\") return undefined;\n\n // The probe map is keyed by the trimmed declared URL — `rules/index.ts` looks\n // it up the same way, and a preview that keyed it differently would report\n // \"not probed\" on an image the audit had just fetched.\n const probe = extraction.assets?.[declared.trim()];\n const declaredSize = usable(\n og?.width && og.height ? { width: og.width.value, height: og.height.value } : undefined,\n );\n const probedSize = usable(probe?.sizes?.[0]);\n const size = declaredSize ?? probedSize;\n\n return {\n declared,\n src: safeUrl(declared),\n alt: og?.alt?.value ?? extraction.twitter.imageAlt?.value,\n from: ogUrl ? \"og:image\" : \"twitter:image\",\n count: extraction.openGraph.images.length,\n declaredSize,\n probedSize,\n probe,\n // Nothing in the codebase computes this. `og.image.ratio` scores it and\n // keeps the number to itself — and rounds to two decimals before banding,\n // which is why this rounds too: a caption that bands 1.69731 as \"cropped\"\n // beside a rule that bands 1.70 as ideal makes the page argue with itself.\n ratio: size ? Math.round((size.width / size.height) * 100) / 100 : undefined,\n };\n}\n\nfunction cardOf(extraction: Extraction): Card {\n const { openGraph: og, twitter, meta, document: doc } = extraction;\n const url = extraction.http.finalUrl;\n return {\n url,\n host: hostOf(url),\n path: pathOf(url),\n ogTitle: resolve(og.title, doc.title),\n ogDescription: resolve(og.description, meta.description),\n documentTitle: resolve(doc.title),\n metaDescription: resolve(meta.description),\n siteName: resolve(og.siteName),\n xTitle: resolve(twitter.title, og.title, doc.title),\n image: imageOf(extraction),\n icon: safeUrl(extraction.links.icons[0]?.href),\n themeColor: meta.themeColor?.value,\n twitterCard: twitter.card?.value,\n locale: og.locale?.value ?? doc.lang?.value,\n alternates: og.localeAlternates.map((f) => f.value),\n jsonLd: extraction.jsonLd,\n rendering: extraction.rendering,\n status: extraction.http.status,\n };\n}\n\n// --- fragments ------------------------------------------------------------\n\nfunction ratioNote(image: CardImage): string {\n if (image.ratio === undefined) return \"no usable dimensions declared, and none decoded\";\n const value = image.ratio.toFixed(2);\n const source = image.declaredSize ? \"declared\" : \"decoded from the file\";\n if (image.ratio >= RATIO_IDEAL[0] && image.ratio <= RATIO_IDEAL[1]) {\n return `${value}:1 ${source} — inside the 1.91:1 band every card is laid out for`;\n }\n if (image.ratio >= RATIO_ACCEPTABLE[0] && image.ratio <= RATIO_ACCEPTABLE[1]) {\n return `${value}:1 ${source} — renderable, but cropped away from 1.91:1`;\n }\n return `${value}:1 ${source} — outside anything a card renders whole`;\n}\n\n/** Plain text — both call sites escape it, so escaping here would double it. */\nfunction probeNote(image: CardImage): string {\n if (!image.probe) return \"not probed on this run\";\n if (image.probe.ok) {\n const type = image.probe.contentType ? `, ${image.probe.contentType}` : \"\";\n return `answered ${image.probe.status}${type}`;\n }\n return image.probe.status === 0\n ? \"the request failed — nothing answered\"\n : `answered ${image.probe.status}, which is not an image`;\n}\n\n/** The image, or the honest reason there is nothing to draw. */\nfunction imageBox(card: Card, className: string): string {\n const image = card.image;\n if (!image) {\n return `<div class=\"${className} shot empty\"><span>no <code>og:image</code></span></div>`;\n }\n if (image.probe && !image.probe.ok) {\n return (\n `<div class=\"${className} shot broken\">` +\n `<span>the image does not answer</span>` +\n `<em>${esc(probeNote(image))}</em>` +\n `</div>`\n );\n }\n if (!image.src) {\n return (\n `<div class=\"${className} shot empty\">` +\n `<span>not an absolute http(s) URL</span>` +\n `<em>${esc(clip(image.declared))}</em>` +\n `</div>`\n );\n }\n const alt = image.alt ? esc(image.alt) : \"\";\n // `no-referrer` because opening this file asks the audited origin for the\n // image, and the local path of the analyst's preview is nobody's business.\n return `<div class=\"${className} shot\"><img src=\"${esc(image.src)}\" alt=\"${alt}\" loading=\"lazy\" referrerpolicy=\"no-referrer\"></div>`;\n}\n\nfunction frame(args: {\n name: string;\n rigor: \"vendor-spec\" | \"guideline\" | \"heuristic\" | \"unsourced\";\n body: string;\n note: string;\n source: string;\n trap?: { label: string; text: string };\n}): string {\n const trap = args.trap\n ? `<p class=\"trap\"><b>${esc(args.trap.label)}</b>${esc(args.trap.text)}</p>`\n : \"\";\n return `<article class=\"frame\">\n <header><span class=\"name\">${esc(args.name)}</span><span class=\"badge ${args.rigor.replace(\"-\", \"\")}\">${esc(args.rigor)}</span></header>\n <div class=\"stage\">${args.body}</div>\n <footer><p>${args.note}</p>${trap}<span class=\"src\">${esc(args.source)}</span></footer>\n</article>`;\n}\n\nfunction favicon(card: Card): string {\n return card.icon\n ? `<img class=\"fav\" src=\"${esc(card.icon)}\" alt=\"\" loading=\"lazy\">`\n : `<span class=\"fav placeholder\">${esc(card.host.slice(0, 1).toUpperCase())}</span>`;\n}\n\n// --- surfaces -------------------------------------------------------------\n\nfunction googleSurface(card: Card): string {\n const title = card.documentTitle?.value ?? card.ogTitle?.value ?? card.url;\n const description = card.metaDescription?.value;\n const body = `<div class=\"serp\">\n <div class=\"site\">${favicon(card)}<span class=\"who\"><b>${esc(card.siteName?.value ?? card.host)}</b><span>${esc(card.host)}${esc(card.path === \"/\" ? \"\" : ` › ${card.path.replace(/^\\//, \"\").split(\"/\").join(\" › \")}`)}</span></span></div>\n <h4>${esc(title)}</h4>\n ${description ? `<p class=\"clamp2\">${esc(description)}</p>` : `<p class=\"none\">no <code>meta description</code> — the snippet is written from the page body</p>`}\n</div>`;\n return frame({\n name: \"Google — search result\",\n rigor: \"heuristic\",\n body,\n note:\n \"Not a card: no image, no ratio. <code>og:title</code> is one of the documented sources \" +\n \"for the title link; <code>og:description</code> is not documented as a snippet source \" +\n \"anywhere — the snippet comes from the page or from <code>meta description</code>.\",\n source: \"developers.google.com/search/docs/appearance/title-link · /snippet\",\n trap: {\n label: \"Not drawn here\",\n text:\n \"Google publishes no character or pixel limit — only truncation “to fit the device \" +\n \"width”. The two-line snippet above is the observed desktop shape, not a specification, \" +\n \"and the 600px / 155-character figures in circulation are third-party measurements that \" +\n \"disagree with each other.\",\n },\n });\n}\n\nfunction openGraphSurface(card: Card): string {\n const image = card.image;\n const body = `<div class=\"ogwrap\">\n ${imageBox(card, \"og\")}\n <dl class=\"facts\">\n <dt>title</dt><dd>${card.ogTitle ? `${esc(card.ogTitle.value)} <span class=\"from\">${esc(card.ogTitle.from)}</span>` : `<span class=\"none\">absent</span>`}</dd>\n <dt>description</dt><dd>${card.ogDescription ? `${esc(card.ogDescription.value)} <span class=\"from\">${esc(card.ogDescription.from)}</span>` : `<span class=\"none\">absent</span>`}</dd>\n <dt>image</dt><dd>${image ? `<span class=\"mono\">${esc(clip(image.declared))}</span> <span class=\"from\">${esc(image.from)}${image.count > 1 ? ` · 1 of ${image.count}` : \"\"}</span>` : `<span class=\"none\">absent</span>`}</dd>\n <dt>size</dt><dd>${image ? esc(ratioNote(image)) : `<span class=\"none\">—</span>`}</dd>\n <dt>fetched</dt><dd>${image ? esc(probeNote(image)) : `<span class=\"none\">—</span>`}</dd>\n <dt>alt</dt><dd>${image?.alt ? esc(image.alt) : `<span class=\"none\">absent</span>`}</dd>\n <dt>locale</dt><dd>${card.locale ? esc(card.locale) : `<span class=\"none\">absent</span>`}${card.alternates.length > 0 ? ` <span class=\"from\">+ ${esc(card.alternates.join(\", \"))}</span>` : \"\"}</dd>\n </dl>\n</div>`;\n return frame({\n name: \"Open Graph — the source card\",\n rigor: \"vendor-spec\",\n body,\n note:\n \"The one fully specified surface: 1200×630 recommended, 600×315 floor, 200×200 absolute \" +\n \"minimum, 8MB, 1.91:1. Every surface below consumes this image, each in its own way.\",\n source: \"developers.facebook.com/docs/sharing/webmasters/images · ogp.me\",\n });\n}\n\nfunction xSurface(card: Card): string {\n const title = card.xTitle?.value;\n const body = `<div class=\"xc\">\n <div class=\"xshot\">\n ${imageBox(card, \"x\")}\n <div class=\"overlay\">${title ? `<span class=\"lab\">${esc(title)}</span>` : \"\"}<span class=\"dom\">${esc(card.host)}</span></div>\n </div>\n</div>`;\n return frame({\n name: \"X — link card\",\n rigor: \"unsourced\",\n body,\n note:\n `Image, headline over the image, domain. Nothing else. This page declares ` +\n `<code>twitter:card</code> ${card.twitterCard ? `as <code>${esc(card.twitterCard)}</code>` : `nowhere, so X falls back to <code>og:*</code>`}.`,\n source: \"X card documentation is not publicly reachable — dated behaviour, not a spec\",\n trap: {\n label: \"The old card is a lie\",\n text:\n \"X removed both headline and description on 4 October 2023. The headline came back in \" +\n \"January 2024 as small text over the image; the description never did. Headline-below-image \" +\n \"exists only for ads since May 2025. Any tool still drawing three stacked rows is drawing \" +\n \"a card that has not existed for years.\",\n },\n });\n}\n\nfunction linkedInSurface(card: Card): string {\n const title = card.ogTitle?.value ?? card.url;\n const narrow = card.image?.declaredSize && card.image.declaredSize.width < 401;\n const body = `<div class=\"lic\">\n ${imageBox(card, \"li\")}\n <div class=\"meta\"><b>${esc(title)}</b><span>${esc(card.host)}</span></div>\n</div>`;\n return frame({\n name: \"LinkedIn — post unfurl\",\n rigor: \"vendor-spec\",\n body,\n note:\n \"Four tags required — <code>og:title</code>, <code>og:image</code>, \" +\n \"<code>og:description</code>, <code>og:url</code> — and a published geometry: 1200×627 \" +\n \"minimum, 1.91:1, 5MB. Under 401px wide the card falls back to a thumbnail, which is the \" +\n \"only documented layout switch of any surface here.\",\n source: \"linkedin.com/help/linkedin/answer/a521928\",\n ...(narrow\n ? {\n trap: {\n label: \"Thumbnail\",\n text:\n `This image declares ${card.image?.declaredSize?.width}px wide, under LinkedIn's ` +\n `documented 401px cutoff — it renders as a small square, not as this card.`,\n },\n }\n : {}),\n });\n}\n\nfunction slackSurface(card: Card): string {\n const title = card.ogTitle?.value ?? card.url;\n const body = `<div class=\"slc\">\n <span class=\"who\">${favicon(card)}${esc(card.siteName?.value ?? card.host)}</span>\n <span class=\"t\">${esc(title)}</span>\n ${card.ogDescription ? `<p>${esc(card.ogDescription.value)}</p>` : \"\"}\n ${imageBox(card, \"sl\")}\n</div>`;\n return frame({\n name: \"Slack — unfurl\",\n rigor: \"guideline\",\n body,\n note:\n \"Slack documents what it reads — Open Graph and X Card metadata — and calls the result \" +\n \"“some micro-approximation of the content”. No geometry, no image minimum, no character \" +\n \"limit is published. The one hard number: past five links in a message, nothing expands.\",\n source: \"docs.slack.dev/messaging/unfurling-links-in-messages\",\n });\n}\n\nfunction discordSurface(card: Card): string {\n const title = card.ogTitle?.value ?? card.url;\n const accent = /^#[0-9a-f]{3,8}$/i.test(card.themeColor ?? \"\") ? card.themeColor : undefined;\n const body = `<div class=\"dcw\"><div class=\"dcc\"${accent ? ` style=\"border-left-color:${esc(accent)}\"` : \"\"}>\n <span class=\"site\">${esc(card.siteName?.value ?? card.host)}</span>\n <span class=\"t\">${esc(title)}</span>\n ${card.ogDescription ? `<p>${esc(card.ogDescription.value)}</p>` : \"\"}\n ${imageBox(card, \"dc\")}\n</div></div>`;\n return frame({\n name: \"Discord — embed\",\n rigor: \"unsourced\",\n body,\n note:\n \"Discord publishes nothing about link unfurls: no tag table, no geometry. The accent bar \" +\n `is drawn from ${accent ? `this page's <code>theme-color</code>` : \"a default, since this page declares no <code>theme-color</code>\"}, which is community-reported behaviour and not documented.`,\n source: \"no Discord source exists for link previews\",\n trap: {\n label: \"Wrong numbers, right document\",\n text:\n \"Discord's documented 256 / 4096 / 6000 character limits are real, and they govern embeds \" +\n \"a bot sends through the API. Using them as unfurl truncation would be a sourcing error of \" +\n \"exactly the kind the rigor scale exists to prevent.\",\n },\n });\n}\n\nfunction whatsAppSurface(card: Card): string {\n const title = card.ogTitle?.value ?? card.url;\n const body = `<div class=\"wac\"><div class=\"wab\"><div class=\"q\">\n ${imageBox(card, \"wa\")}\n <div class=\"txt\"><b class=\"clamp2\">${esc(title)}</b>${card.ogDescription ? `<p class=\"clamp2\">${esc(card.ogDescription.value)}</p>` : \"\"}<span>${esc(card.host)}</span></div>\n</div><div class=\"msg\">${esc(card.url)}</div></div></div>`;\n return frame({\n name: \"WhatsApp — link preview\",\n rigor: \"vendor-spec\",\n body,\n note:\n \"The one messenger with a real spec from Meta: image under 600KB, at least 300px wide, \" +\n \"aspect ratio 4:1 or less, title in at most two lines, description in one or two — “80 \" +\n \"characters will suffice”. It is also the only surface where a 1200×630 card is more often \" +\n \"too heavy than too small.\",\n source: \"developers.facebook.com/documentation/business-messaging/whatsapp/link-previews\",\n });\n}\n\n// --- panels ---------------------------------------------------------------\n\n/**\n * Findings on the head, from both registries.\n *\n * `SiteIssue` carries the same shape and some of its ids wear a head prefix —\n * `icons.ico.missing` is site-scoped because a `/favicon.ico` belongs to an\n * origin, not to a page. Reading only `seoIssues` would print \"nothing the\n * catalogue judges\" on a page the catalogue had just judged.\n */\nfunction findingsPanel(issues: HeadFinding[]): string {\n const head = issues.filter((i) => HEAD_PREFIXES.some((p) => i.ruleId.startsWith(p)));\n const others = issues.length - head.length;\n const rest =\n others > 0\n ? `<p class=\"foot\">${others} other finding${others === 1 ? \"\" : \"s\"} on this page — they are in the report, not on a card.</p>`\n : \"\";\n if (head.length === 0) {\n return `<section class=\"panel\"><h3>Findings on the head</h3><p class=\"none\">Nothing the catalogue judges about this page's head. That is not the same as a card worth sharing — which is what the surfaces above are for.</p>${rest}</section>`;\n }\n const rows = head\n .map((issue) => {\n // `expected` is a `SeoIssue` field; a site-scoped finding has none, and\n // the catalogue drops the field for `SITE_RULES` anyway.\n const expected = \"expected\" in issue ? issue.expected : undefined;\n return `<li class=\"find ${esc(issue.severity)}\">\n <span class=\"rid\">${esc(issue.ruleId)}</span>\n <span class=\"msg\">${codeSpans(esc(issue.message))}</span>\n <span class=\"pin\">${esc(issue.severity)}${issue.rigor ? ` · ${esc(issue.rigor)}` : \"\"}${expected ? ` · expected ${codeSpans(esc(expected))}` : \"\"}</span>\n</li>`;\n })\n .join(\"\\n\");\n return `<section class=\"panel\"><h3>Findings on the head</h3><ul class=\"finds\">${rows}</ul>${rest}</section>`;\n}\n\nfunction jsonLdPanel(blocks: ExtractionJsonLd[]): string {\n const note =\n `<p class=\"foot\">Shown, never judged: no rule in the catalogue reads <code>jsonLd</code>, ` +\n `and that refusal is deliberate — nothing says which schema a page <em>should</em> carry.</p>`;\n if (blocks.length === 0) {\n return `<section class=\"panel\"><h3>JSON-LD <span class=\"badge unjudged\">extracted, unjudged</span></h3><p class=\"none\">No <code>application/ld+json</code> block on this page — and no rule can hold that against it.</p>${note}</section>`;\n }\n const chips = blocks\n .map((block) => {\n const label = block.parseError\n ? `#${block.index} ${block.parseError}`\n : `#${block.index} ${block.types.join(\", \") || \"no @type\"}`;\n return `<span class=\"chip${block.parseError ? \" bad\" : \"\"}\">${esc(label)}</span>`;\n })\n .join(\"\");\n const first = blocks.find((block) => block.data !== null);\n const pretty = first ? prettyJson(first.data) : undefined;\n const body =\n pretty === undefined\n ? `<pre>${esc(blocks[0]?.raw.slice(0, 2000) ?? \"\")}</pre>`\n : `<pre>${esc(pretty)}</pre>`;\n return `<section class=\"panel\"><h3>JSON-LD <span class=\"badge unjudged\">extracted, unjudged</span></h3><div class=\"chips\">${chips}</div>${body}${note}</section>`;\n}\n\nfunction renderingNote(card: Card): string {\n if (card.rendering.mode === \"static\") {\n return `read from the static HTML — what a crawler that runs no JavaScript sees`;\n }\n return card.rendering.escalated\n ? `read from the hydrated DOM, after the static head looked empty${card.rendering.escalationReason ? ` (${card.rendering.escalationReason})` : \"\"} — a crawler that runs no JavaScript sees less than this`\n : `read from the hydrated DOM — a crawler that runs no JavaScript may see less than this`;\n}\n\n/**\n * Count what a reader counts, not what UTF-16 does.\n *\n * The number matters because it is what a card's own size ladder reads: a\n * German title that gains 30% over its English source drops a step, and the\n * step is chosen on graphemes. The ladder itself is the site's — `@goflag/og`\n * refuses to ship default steps — so this reports the length and never the\n * step it would land on.\n */\nfunction graphemes(value: string): number {\n if (typeof Intl?.Segmenter === \"function\") {\n return [...new Intl.Segmenter(undefined, { granularity: \"grapheme\" }).segment(value)].length;\n }\n return [...value].length;\n}\n\n/** One row of the translation strip. */\ninterface Sibling {\n index: number;\n locale: string;\n path: string;\n ogLocale?: string;\n title: string;\n description: string;\n}\n\n/**\n * The same route in its other languages.\n *\n * Nobody else previews this axis, and goflag is the tool that already knows\n * it. What it shows is the text, not eight more pictures: the card is drawn\n * from the words, so the words side by side are what a divergence looks like\n * — a translation that outgrows its ladder, one locale still carrying the\n * source language, a description that only got written once.\n */\nfunction translationsPanel(current: Sibling, siblings: Sibling[]): string {\n if (siblings.length < 2) return \"\";\n const rows = siblings\n .map((s) => {\n const here = s.index === current.index;\n return `<tr${here ? ' class=\"here\"' : \"\"}>\n <td class=\"loc\">${here ? \"▸ \" : \"\"}${esc(s.locale)}</td>\n <td class=\"og\">${s.ogLocale ? esc(s.ogLocale) : `<span class=\"none\">no og:locale</span>`}</td>\n <td>${here ? esc(s.title) : `<a href=\"#page-${s.index}\">${esc(s.title)}</a>`}</td>\n <td class=\"num\">${graphemes(s.title)}</td>\n <td class=\"num\">${s.description ? graphemes(s.description) : `<span class=\"none\">—</span>`}</td>\n</tr>`;\n })\n .join(\"\\n\");\n return `<section class=\"panel\"><h3>This route in ${siblings.length} languages</h3>\n<div class=\"tw\"><table class=\"locs\">\n<thead><tr><th>path</th><th>og:locale</th><th>og:title</th><th class=\"num\">gr.</th><th class=\"num\">desc.</th></tr></thead>\n<tbody>${rows}</tbody>\n</table></div>\n<p class=\"foot\">Lengths are graphemes, which is what a card's size ladder counts. The ladder is the site's own — <code>@goflag/og</code> ships none — so this reports the length and not the step.</p></section>`;\n}\n\n// --- the route tree -------------------------------------------------------\n\n/**\n * One node of the route tree — a path segment, and what hangs off it.\n *\n * `segments` is a list rather than a string because a chain with nothing to\n * choose between its links is one row: a `/stet` that holds only `/1.6.3`\n * reads as `/stet/1.6.3`, and keeping them apart costs a level of indentation\n * and buys no decision.\n */\ninterface RouteNode {\n segments: string[];\n /** The page that lives exactly here, when the crawl read one. */\n index?: number;\n /** Insertion-ordered on purpose — see `buildRouteTree`. */\n children: Map<string, RouteNode>;\n /** Pages in this subtree, this node's own page included. */\n count: number;\n}\n\n/**\n * Split a path into the segments the tree branches on.\n *\n * A query string rides on the last segment rather than becoming one: `?` is not\n * a path separator, and two pages that differ only by their query must still\n * land on two different rows.\n */\nfunction pathSegments(path: string): string[] {\n const cut = path.indexOf(\"?\");\n const pathname = cut === -1 ? path : path.slice(0, cut);\n const search = cut === -1 ? \"\" : path.slice(cut);\n const segments = pathname.split(\"/\").filter(Boolean);\n if (search === \"\") return segments;\n return segments.length === 0\n ? [search]\n : [...segments.slice(0, -1), `${segments.at(-1)}${search}`];\n}\n\n/**\n * The crawled paths as a tree, single-child chains already folded away.\n *\n * Insertion order is kept rather than sorted: the rows carry `#page-N` anchors\n * into the sections below, and those sections are in crawl order. A tree that\n * sorted its own rows would read in one order and link into another.\n */\nfunction buildRouteTree(paths: string[]): RouteNode {\n const root: RouteNode = { segments: [], children: new Map(), count: 0 };\n paths.forEach((path, index) => {\n let node = root;\n node.count++;\n for (const segment of pathSegments(path)) {\n const child: RouteNode = node.children.get(segment) ?? {\n segments: [segment],\n children: new Map(),\n count: 0,\n };\n node.children.set(segment, child);\n child.count++;\n node = child;\n }\n // Two extractions on one path — a redirect that landed twice — keep the\n // first: the second has a section of its own, reachable from nothing here,\n // and that is better than a row that silently means one of two pages.\n node.index ??= index;\n });\n return { ...root, children: foldChildren(root.children) };\n}\n\n/** Fold every chain that offers no choice, recursively. */\nfunction foldChildren(children: Map<string, RouteNode>): Map<string, RouteNode> {\n return new Map([...children].map(([key, node]) => [key, foldChain(node)]));\n}\n\nfunction foldChain(node: RouteNode): RouteNode {\n let current = node;\n // A node that is itself a page keeps its own row — folding it away would\n // fold away the only link to it.\n while (current.index === undefined && current.children.size === 1) {\n const only = [...current.children.values()][0]!;\n current = { ...only, segments: [...current.segments, ...only.segments] };\n }\n return { ...current, children: foldChildren(current.children) };\n}\n\n/**\n * The tree, as nested `<details>`.\n *\n * Folding is the whole point at 300 pages, and this document ships a CSP that\n * allows images and nothing else — there is no script here to fold anything.\n * `<details>` is the one disclosure the browser implements on its own, which is\n * why the tree is built out of it rather than out of a control that would need\n * one line of JavaScript and cost the policy above.\n *\n * A folder's label toggles it; the `↗` beside it opens the page at that exact\n * path, because most folders here are pages too — `/fr/blog` is a route and a\n * parent at once. The link takes the click (it is the innermost activatable\n * element, so the disclosure never sees it) and the two actions stay separate.\n */\nfunction routeList(node: RouteNode, open: boolean, className = \"\"): string {\n const items = [...node.children.values()].map((child) => {\n const label = esc(child.segments.map((segment) => `/${segment}`).join(\"\"));\n if (child.children.size === 0) {\n return child.index === undefined\n ? `<li class=\"leaf\"><span>${label}</span></li>`\n : `<li class=\"leaf\"><a href=\"#page-${child.index}\">${label}</a></li>`;\n }\n // A folder the crawl never read as a page of its own — `/endpoints` under a\n // docs tree — still holds the slot open, or the counts down the column stop\n // lining up wherever one is missing.\n const self =\n child.index === undefined\n ? `<span class=\"go\"></span>`\n : `<a class=\"go\" href=\"#page-${child.index}\" title=\"the page at this path\">↗</a>`;\n return `<li><details${open ? \" open\" : \"\"}><summary><span class=\"seg\">${label}</span><span class=\"n\">${child.count}</span>${self}</summary>\n${routeList(child, open)}</details></li>`;\n });\n return `<ul${className ? ` class=\"${className}\"` : \"\"}>${items.join(\"\\n\")}</ul>`;\n}\n\n/**\n * Pages small enough to read at once are shown at once.\n *\n * A dozen routes folded into four collapsed rows is worse than the flat list\n * this replaced; three hundred is the case that needed a tree at all.\n */\nconst TREE_OPEN_MAX = 20;\n\n/** One `<meta>` or `<link>`, printed the way the page wrote it. */\nfunction tagLabel(tag: {\n name?: string;\n property?: string;\n httpEquiv?: string;\n rel?: string;\n href?: string;\n content?: string;\n}): string {\n const key =\n tag.property ?? tag.name ?? (tag.httpEquiv ? `http-equiv=${tag.httpEquiv}` : undefined);\n if (key) return `${key}${tag.content ? ` = ${clip(tag.content, 120)}` : \"\"}`;\n return `rel=${tag.rel ?? \"?\"}${tag.href ? ` → ${clip(tag.href, 120)}` : \"\"}`;\n}\n\n/**\n * What the browser shows and the unfurl never will.\n *\n * Only the escalation path produces this, so absence is silence rather than a\n * clean bill — the panel says which of the two it is instead of drawing an\n * empty box that reads like a pass.\n */\nfunction hydrationPanel(extraction: Extraction): string {\n const delta = extraction.hydration;\n if (!delta) {\n const why =\n extraction.rendering.mode === \"static\"\n ? \"this run never rendered the page, so there is no second reading to compare against\"\n : \"this run rendered the page without keeping a static reading to compare against\";\n return `<section class=\"panel\"><h3>Static vs hydrated</h3><p class=\"none\">Not established — ${why}.</p></section>`;\n }\n\n const lines = [\n ...delta.injectedMetas.map((m) => ({ sign: \"+\", text: tagLabel(m) })),\n ...delta.injectedLinks.map((l) => ({ sign: \"+\", text: tagLabel(l) })),\n ...delta.removedMetas.map((m) => ({ sign: \"−\", text: tagLabel(m) })),\n ...delta.removedLinks.map((l) => ({ sign: \"−\", text: tagLabel(l) })),\n ...(delta.titleChanged ? [{ sign: \"+\", text: \"<title> — rewritten after hydration\" }] : []),\n ...(delta.htmlLangChanged\n ? [{ sign: \"+\", text: \"<html lang> — changed after hydration\" }]\n : []),\n ...(delta.jsonLdBlocksAdded > 0\n ? [{ sign: \"+\", text: `${delta.jsonLdBlocksAdded} JSON-LD block(s) added by script` }]\n : []),\n ];\n\n if (lines.length === 0) {\n return `<section class=\"panel\"><h3>Static vs hydrated</h3><p class=\"none\">Both readings agree: every tag above is in the HTML before any script runs.</p></section>`;\n }\n\n const rows = lines\n .map(\n (l) =>\n `<li class=\"${l.sign === \"+\" ? \"added\" : \"removed\"}\"><span class=\"sign\">${l.sign}</span>${esc(l.text)}</li>`,\n )\n .join(\"\\n\");\n return `<section class=\"panel\"><h3>Static vs hydrated <span class=\"badge unjudged\">no rule reads this</span></h3>\n<ul class=\"delta\">${rows}</ul>\n<p class=\"foot\">A <code>+</code> is a tag only the browser has. Unfurlers run no JavaScript, so the cards above are drawn from something a crawler may never receive — and every <code>og.*</code> rule judges the declaration it was handed, not which pass handed it over.</p></section>`;\n}\n\nfunction pageSection(\n extraction: Extraction,\n issues: HeadFinding[],\n index: number,\n siblings: Sibling[],\n): string {\n const card = cardOf(extraction);\n const surfaces = [\n openGraphSurface(card),\n googleSurface(card),\n xSurface(card),\n linkedInSurface(card),\n slackSurface(card),\n discordSurface(card),\n whatsAppSurface(card),\n ].join(\"\\n\");\n return `<section class=\"page\" id=\"page-${index}\">\n <header class=\"pagehead\">\n <h2>${esc(card.path)}</h2>\n <p class=\"url\">${esc(card.url)}</p>\n <p class=\"meta\">${card.status} · ${esc(renderingNote(card))}${card.locale ? ` · ${esc(card.locale)}` : \"\"}</p>\n </header>\n <div class=\"surfaces\">${surfaces}</div>\n ${findingsPanel(issues)}\n ${translationsPanel(\n siblings.find((sib) => sib.index === index)!,\n siblings,\n )}\n ${hydrationPanel(extraction)}\n ${jsonLdPanel(card.jsonLd)}\n</section>`;\n}\n\n// --- document -------------------------------------------------------------\n\nconst STYLE = `\n:root{--paper:#fff;--paper2:#f7f8f7;--ink:#09090b;--ink2:#4c534f;--ink3:#6d7671;--line:#e5e6e5;--line2:#d3d6d4;--brand:#009966;--green:#007a55;--amber:#bb4d00;--red:#e7000b;--wash:#f2f6f4;\n--g-title:#1a0dab;--g-text:#4d5156;--x-bg:#000;--x-fg:#e7e9ea;--x-line:#2f3336;--li-strip:#f3f2ef;--sl-link:#1264a3;--sl-rail:#ddd;--dc-bg:#313338;--dc-embed:#2b2d31;--dc-fg:#dbdee1;--dc-dim:#b5bac1;--dc-link:#00a8fc;--wa-bg:#efeae2;--wa-bubble:#d9fdd3;--wa-quote:#d1f4cb;--wa-fg:#111b21;--wa-dim:#667781;\n--mono:ui-monospace,SFMono-Regular,\"SF Mono\",Menlo,Consolas,monospace;--ui:system-ui,-apple-system,\"Segoe UI\",Roboto,sans-serif}\n@media (prefers-color-scheme:dark){:root{--paper:#0a0a0c;--paper2:#121316;--ink:#fafafa;--ink2:#b6bcb9;--ink3:#878e8a;--line:#23252a;--line2:#32353b;--brand:#00d492;--green:#00d492;--amber:#ffb900;--red:#ff6467;--wash:#101614}}\n*{box-sizing:border-box}\nbody{margin:0;padding:0 20px 100px;background:var(--paper);color:var(--ink);font:15px/1.6 var(--ui);-webkit-font-smoothing:antialiased}\n.wrap{max-width:1120px;margin-inline:auto}\nh1,h2,h3,.badge,.chip,.rid,.src,.from,.mono,code,pre,dt,.pin,.nav{font-family:var(--mono)}\nh1{font-size:22px;letter-spacing:-.02em;margin:0 0 6px}\n.lede{color:var(--ink2);margin:0;max-width:70ch}\nheader.doc{padding:44px 0 24px;border-bottom:1px solid var(--line)}\n.nav{margin:22px 0 0;max-width:620px}\n.navnote{margin:0 0 8px;font-family:var(--ui);font-size:12px;color:var(--ink3)}\n.tree,.tree ul{list-style:none;margin:0;padding:0;font-size:12px}\n.tree ul{margin-left:6px;padding-left:12px;border-left:1px solid var(--line)}\n.tree summary{display:flex;align-items:center;gap:10px;padding:3px 7px;border-radius:3px;color:var(--ink2);cursor:pointer;list-style:none}\n.tree summary::-webkit-details-marker{display:none}\n.tree summary::before{content:\"▸\";flex:none;width:1ch;font-size:9px;color:var(--ink3)}\n.tree details[open]>summary::before{content:\"▾\"}\n.tree summary:hover,.tree summary:focus-visible{background:var(--wash);color:var(--ink)}\n.tree .seg{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.tree .n{margin-left:auto;flex:none;font-size:10px;color:var(--ink3);font-variant-numeric:tabular-nums}\n.tree .go{flex:none;width:1.2em;text-align:right;font-size:11px;text-decoration:none;opacity:.55}\n.tree .go:hover,.tree .go:focus-visible{opacity:1}\n.tree .leaf a,.tree .leaf span{display:block;padding:3px 7px 3px 24px;color:var(--ink2);text-decoration:none;border-radius:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.tree .leaf a:hover,.tree .leaf a:focus-visible{background:var(--wash);color:var(--brand)}\n.page{padding-top:52px}\n.pagehead h2{font-size:16px;margin:0 0 4px}\n.pagehead .url{margin:0;font-size:13px;color:var(--ink2);word-break:break-all}\n.pagehead .meta{margin:6px 0 0;font-size:12px;color:var(--ink3);font-family:var(--mono)}\n.surfaces{display:grid;grid-template-columns:repeat(auto-fit,minmax(400px,1fr));gap:22px;margin-top:22px}\n.frame{border:1px solid var(--line);border-radius:8px;background:var(--paper2);display:flex;flex-direction:column;overflow:hidden}\n.frame>header{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 13px;border-bottom:1px solid var(--line);background:var(--paper)}\n.frame>header .name{font-size:12px;font-weight:600}\n.frame .stage{padding:16px;display:flex;justify-content:center}\n.frame footer{margin-top:auto;border-top:1px solid var(--line);padding:11px 13px;font-size:12.5px;line-height:1.5;color:var(--ink2)}\n.frame footer p{margin:0 0 6px}\n.src{display:block;font-size:10.5px;color:var(--ink3);word-break:break-all}\n.badge{font-size:10px;letter-spacing:.06em;text-transform:uppercase;padding:3px 7px;border-radius:3px;border:1px solid var(--line2);color:var(--ink3);white-space:nowrap}\n.badge.vendorspec{color:var(--green);border-color:currentColor}\n.badge.guideline,.badge.heuristic{color:var(--amber);border-color:currentColor}\n.badge.unsourced{color:var(--red);border-color:currentColor}\n.badge.unjudged{color:var(--ink3);text-transform:none;letter-spacing:0}\n.trap{margin:8px 0 0;padding-left:10px;border-left:2px solid var(--red);font-size:12px}\n.trap b{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--red)}\n.shot{width:100%;overflow:hidden;background:#0d0e10}\n.shot img{display:block;width:100%;height:auto}\n.shot.empty,.shot.broken{aspect-ratio:1.91/1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;background:var(--wash);color:var(--ink3);font-size:12.5px;text-align:center;padding:12px;border:1px dashed var(--line2)}\n.shot.broken{border-color:var(--red);color:var(--red)}\n.shot em{font-family:var(--mono);font-size:11px;font-style:normal;opacity:.85;word-break:break-all}\n.ogwrap{width:100%;display:flex;flex-direction:column;gap:14px}\n.facts{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin:0;font-size:12.5px}\n.facts dt{color:var(--ink3);font-size:11px}\n.facts dd{margin:0;word-break:break-word}\n.from{font-family:var(--mono);font-size:10.5px;color:var(--brand)}\n.none{color:var(--ink3);font-style:italic}\n.mono{font-family:var(--mono);font-size:11.5px;word-break:break-all}\n.clamp2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}\n.serp{width:100%;max-width:600px;font-family:Arial,var(--ui)}\n.serp .site{display:flex;align-items:center;gap:10px;margin-bottom:4px}\n.fav{width:26px;height:26px;border-radius:50%;flex:none;object-fit:contain;background:var(--wash)}\n.fav.placeholder{display:grid;place-items:center;font-family:var(--mono);font-size:12px;color:var(--ink2)}\n.serp .who b{display:block;font-size:14px;font-weight:400}\n.serp .who span{font-size:12px;color:var(--ink3)}\n.serp h4{font-size:19px;line-height:1.3;font-weight:400;color:var(--g-title);margin:4px 0 3px}\n.serp p{font-size:13.5px;line-height:1.56;margin:0;color:var(--g-text)}\n@media (prefers-color-scheme:dark){.serp h4{color:#8ab4f8}.serp p{color:var(--ink2)}}\n.xc{width:100%;max-width:440px}\n.xshot{position:relative;border:1px solid var(--x-line);border-radius:14px;overflow:hidden;background:var(--x-bg)}\n.overlay{position:absolute;left:10px;right:10px;bottom:10px;display:flex;flex-direction:column;align-items:flex-start;gap:4px}\n.overlay .lab{background:#000000bf;color:var(--x-fg);font-size:12px;line-height:1.25;padding:3px 7px;border-radius:4px;max-width:100%}\n.overlay .dom{font-size:11px;color:#fffc;text-shadow:0 1px 2px #000}\n.lic{width:100%;max-width:440px;border:1px solid var(--line2);border-radius:2px;overflow:hidden}\n.lic .meta{padding:10px 12px;background:var(--li-strip);color:#000}\n.lic .meta b{display:block;font-size:14px;line-height:1.35}\n.lic .meta span{font-size:12px;color:#00000099}\n.slc{width:100%;max-width:440px;padding:2px 0 2px 12px;border-left:4px solid var(--sl-rail)}\n.slc .who{display:flex;align-items:center;gap:7px;font-size:12.5px;font-weight:700;margin-bottom:3px}\n.slc .who .fav{width:16px;height:16px;border-radius:3px}\n.slc .t{display:block;font-size:14px;font-weight:700;color:var(--sl-link);margin-bottom:3px}\n.slc p{font-size:13px;line-height:1.45;margin:0 0 8px}\n.slc .shot{max-width:360px;border-radius:8px}\n.dcw{width:100%;max-width:440px;background:var(--dc-bg);padding:11px;border-radius:8px}\n.dcc{background:var(--dc-embed);border-left:4px solid var(--brand);border-radius:4px;padding:11px 13px 13px}\n.dcc .site{font-size:11.5px;color:var(--dc-fg)}\n.dcc .t{display:block;font-size:15px;font-weight:600;color:var(--dc-link);line-height:1.3;margin:5px 0}\n.dcc p{font-size:13px;line-height:1.42;color:var(--dc-dim);margin:0 0 9px}\n.dcc .shot{border-radius:4px}\n.wac{width:100%;max-width:330px;background:var(--wa-bg);padding:12px;border-radius:8px}\n.wab{background:var(--wa-bubble);border-radius:8px;padding:4px}\n.wab .q{background:var(--wa-quote);border-radius:6px;overflow:hidden}\n.wab .txt{padding:8px 10px;color:var(--wa-fg)}\n.wab .txt b{display:block;font-size:12.5px;line-height:1.3}\n.wab .txt p{font-size:12px;color:var(--wa-dim);margin:2px 0 0;line-height:1.35}\n.wab .txt span{font-size:11px;color:var(--wa-dim)}\n.wab .msg{padding:5px 9px 3px;font-size:13px;color:var(--wa-fg);word-break:break-all}\n.panel{margin-top:22px;border:1px solid var(--line);border-radius:8px;padding:16px 18px;background:var(--paper2)}\n.panel h3{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink3);margin:0 0 12px;display:flex;align-items:center;gap:10px}\n.panel .foot{margin:12px 0 0;font-size:12.5px;color:var(--ink3)}\n.finds{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}\n.find{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;padding:9px 0 9px 12px;border-left:3px solid var(--ink3);border-bottom:1px solid var(--line)}\n.find:last-child{border-bottom:0}\n.find.error{border-left-color:var(--red)}\n.find.warning{border-left-color:var(--amber)}\n.find .rid{font-size:12.5px;font-weight:600}\n.find .msg{font-size:13.5px;color:var(--ink2)}\n.find .pin{grid-column:1/-1;font-size:11px;color:var(--ink3)}\n.tw{overflow-x:auto}\ntable.locs{border-collapse:collapse;width:100%;font-size:13px}\ntable.locs th{text-align:left;font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink3);font-weight:600;padding:0 12px 7px 0;border-bottom:1px solid var(--line)}\ntable.locs td{padding:7px 12px 7px 0;border-bottom:1px solid var(--line);vertical-align:top}\ntable.locs tr:last-child td{border-bottom:0}\ntable.locs .loc,table.locs .og{font-family:var(--mono);font-size:12px;white-space:nowrap}\ntable.locs .num{font-family:var(--mono);text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}\ntable.locs tr.here td{color:var(--ink);font-weight:600}\ntable.locs tr.here .loc{color:var(--brand)}\n.delta{list-style:none;margin:0;padding:0;font-family:var(--mono);font-size:12.5px;line-height:1.7}\n.delta li{display:flex;gap:9px;word-break:break-word}\n.delta .sign{flex:none;width:1ch;font-weight:700}\n.delta .added .sign,.delta li.added{color:var(--green)}\n.delta .removed .sign,.delta li.removed{color:var(--red)}\n.chips{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:12px}\n.chip{font-size:11px;padding:3px 8px;border:1px solid var(--line2);border-radius:3px;color:var(--ink2)}\n.chip.bad{color:var(--red);border-color:currentColor}\npre{margin:0;padding:13px 15px;background:#121416;color:#d8dbde;border-radius:6px;font-size:11.5px;line-height:1.55;overflow-x:auto}\ncode{font-size:.9em;background:var(--wash);border:1px solid var(--line);border-radius:3px;padding:.05em .3em}\nfooter.doc{margin-top:70px;padding-top:20px;border-top:1px solid var(--line);font-family:var(--mono);font-size:11.5px;color:var(--ink3)}\na{color:var(--brand)}\n:focus-visible{outline:2px solid var(--brand);outline-offset:2px}\n`;\n\n/**\n * Render the whole preview.\n *\n * A report without `extractions` renders a document that says so rather than an\n * empty one: the caller asked for a preview and got a report that cannot make\n * one, and that is a fact about the run, not an error to swallow.\n */\nexport function renderPreview(report: GoflagReport, options: RenderPreviewOptions = {}): string {\n const title = options.title ?? `goflag preview — ${hostOf(report.url)}`;\n const extractions = report.extractions ?? [];\n const issuesByPage = new Map<string, HeadFinding[]>();\n for (const issue of [...report.seoIssues, ...report.siteIssues]) {\n const bucket = issuesByPage.get(issue.pageUrl);\n if (bucket) bucket.push(issue);\n else issuesByPage.set(issue.pageUrl, [issue]);\n }\n\n // Two different facts, and they read differently: a report that was never\n // asked for extractions, and a crawl that reached no page worth extracting.\n const empty =\n report.extractions === undefined\n ? `This report carries no extractions, so there is nothing to draw. Pass <code>extractions: true</code> to <code>runAudit</code>, or use <code>goflag preview &lt;url&gt;</code>, which does.`\n : `The crawl reached no HTML page it could read, so there is nothing to draw. Every page was unreachable, not HTML, or a declared duplicate of another.`;\n\n // Group by locale-free route, on the same derivation the cross-page rules\n // use — comparing two artefacts is only meaningful if both are normalised\n // identically, and this compares a page with its own translations.\n const rows: Sibling[] = extractions.map((extraction, index) => {\n const path = pathOf(extraction.http.finalUrl);\n return {\n index,\n locale: splitRoute(path).locale,\n path,\n ogLocale: extraction.openGraph.locale?.value,\n title: cardOf(extraction).ogTitle?.value ?? path,\n description: cardOf(extraction).ogDescription?.value ?? \"\",\n };\n });\n const byRoute = new Map<string, Sibling[]>();\n extractions.forEach((extraction, index) => {\n const route = splitRoute(pathOf(extraction.http.finalUrl)).route;\n byRoute.set(route, [...(byRoute.get(route) ?? []), rows[index]!]);\n });\n\n const body =\n extractions.length === 0\n ? `<section class=\"page\"><p class=\"none\">${empty}</p></section>`\n : extractions\n .map((extraction, index) =>\n pageSection(\n extraction,\n issuesByPage.get(extraction.http.finalUrl) ?? [],\n index,\n byRoute.get(splitRoute(pathOf(extraction.http.finalUrl)).route) ?? [],\n ),\n )\n .join(\"\\n\");\n\n const nav =\n extractions.length > 1\n ? `<nav class=\"nav\">\n <p class=\"navnote\">${extractions.length} pages, by path. A folder folds; the ↗ beside one opens the page at that path itself.</p>\n ${routeList(\n buildRouteTree(extractions.map((extraction) => pathOf(extraction.http.finalUrl))),\n extractions.length <= TREE_OPEN_MAX,\n \"tree\",\n )}\n</nav>`\n : \"\";\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<meta name=\"robots\" content=\"noindex\">\n<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; img-src http: https: data:; style-src 'unsafe-inline'\">\n<title>${esc(title)}</title>\n<style>${STYLE}</style>\n</head>\n<body>\n<div class=\"wrap\">\n<header class=\"doc\">\n <h1>${esc(title)}</h1>\n <p class=\"lede\">What each surface makes of ${esc(report.url)}, drawn from what the pages declared. Each surface carries the rigor of the geometry it is drawn from — three of the seven publish one, two publish nothing at all, and Google is not a card.</p>\n ${nav}\n</header>\n${body}\n<footer class=\"doc\">${esc(`${extractions.length} page${extractions.length === 1 ? \"\" : \"s\"} · audited ${report.finishedAt} · profile ${report.profile} · goflag`)}</footer>\n</div>\n</body>\n</html>\n`;\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 { Rigor, 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 /**\n * How authoritative the rule is. Constant within a group — a rollup is one\n * rule — which is what makes the summary the right place to show it: one tag\n * per line instead of one per finding.\n */\n rigor?: Rigor;\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 rigor: issue.rigor,\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":";;;AAaA,SAAS,kBAAkB;AAQpB,SAAS,SAAS,QAAwB;AAC/C,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,MAAM;AACxB,UAAM,OAAO,EAAE,SAAS,QAAQ,QAAQ,EAAE,KAAK;AAC/C,WAAO,GAAG,IAAI,GAAG,EAAE,MAAM;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQO,SAAS,UAAU,QAAwB;AAChD,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,MAAM;AACxB,UAAM,OAAO,EAAE,SAAS,QAAQ,QAAQ,EAAE,KAAK;AAC/C,WAAO,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,EAAE,MAAM;AAAA,EACtC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,SAAS,YAAY,aAAqB,OAAyB;AACxE,QAAM,SAAS,WAAW,QAAQ,EAAE,OAAO,MAAM,KAAK,IAAQ,CAAC,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AAC1F,SAAO,GAAG,QAAQ,IAAI,MAAM;AAC9B;;;ACvDA,YAAY,aAAa;;;ACqelB,IAAM,sBAAsB;;;AC1d5B,SAAS,mBAAmB,SAAwC;AACzE,QAAM,SAAwB,CAAC;AAC/B,MAAI,eAAe;AAEnB,aAAW,KAAK,SAAS;AACvB,QAAI,EAAE,MAAM,YAAY,MAAM,sBAAuB;AACrD,UAAM,MAAM,EAAE,SAAS,KAAK,KAAK;AACjC,QAAI,IAAI,WAAW,GAAG;AACpB,aAAO,KAAK;AAAA,QACV,OAAO;AAAA,QACP;AAAA,QACA,MAAM;AAAA,QACN,YAAY;AAAA,QACZ,OAAO,CAAC;AAAA,MACV,CAAC;AACD;AAAA,IACF;AAEA,UAAM,UAAU,mBAAmB,GAAG;AACtC,QAAI;AACJ,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM,OAAO;AAAA,IAC7B,SAAS,KAAK;AACZ,eAAS;AACT,mBAAa,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IAC9D;AAEA,WAAO,KAAK;AAAA,MACV,OAAO;AAAA,MACP;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA,OAAO,WAAW,OAAO,CAAC,IAAI,aAAa,MAAM;AAAA,IACnD,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAmBA,IAAM,YAAY;AAGX,SAAS,aAAa,MAAyB;AACpD,QAAM,MAAM,oBAAI,IAAY;AAC5B,QAAM,MAAM,KAAK,CAAC;AAClB,SAAO,CAAC,GAAG,GAAG;AAChB;AAEA,SAAS,MAAM,MAAe,KAAkB,OAAqB;AACnE,MAAI,SAAS,QAAQ,SAAS,OAAW;AACzC,MAAI,QAAQ,UAAW;AACvB,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,eAAW,QAAQ,KAAM,OAAM,MAAM,KAAK,QAAQ,CAAC;AACnD;AAAA,EACF;AACA,MAAI,OAAO,SAAS,SAAU;AAE9B,QAAM,MAAM;AACZ,QAAM,IAAI,IAAI,OAAO;AACrB,MAAI,OAAO,MAAM,UAAU;AACzB,QAAI,IAAI,CAAC;AAAA,EACX,WAAW,MAAM,QAAQ,CAAC,GAAG;AAC3B,eAAW,KAAK,GAAG;AACjB,UAAI,OAAO,MAAM,SAAU,KAAI,IAAI,CAAC;AAAA,IACtC;AAAA,EACF;AAEA,QAAM,QAAQ,IAAI,QAAQ;AAC1B,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,eAAW,QAAQ,MAAO,OAAM,MAAM,KAAK,QAAQ,CAAC;AAAA,EACtD;AAGA,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,GAAG;AACxC,QAAI,MAAM,WAAW,MAAM,SAAU;AACrC,QAAI,KAAK,OAAO,MAAM,SAAU,OAAM,GAAG,KAAK,QAAQ,CAAC;AAAA,EACzD;AACF;AAEA,SAAS,mBAAmB,GAAmB;AAC7C,SAAO,EACJ,WAAW,UAAU,GAAG,EACxB,WAAW,UAAU,GAAG,EACxB,WAAW,SAAS,GAAG,EACvB,WAAW,UAAU,GAAG,EACxB,WAAW,QAAQ,GAAG,EACtB,WAAW,QAAQ,GAAG,EACtB,WAAW,UAAU,GAAG,EACxB,WAAW,SAAS,GAAG;AAC5B;;;AFrFO,SAAS,cACd,MACA,SACmF;AACnF,QAAM,IAAY,aAAK,IAAI;AAC3B,QAAM,OAAO,YAAY,GAAG,QAAQ,OAAO;AAE3C,QAAM,MAAM,WAAW,CAAC;AACxB,QAAM,OAAO,iBAAiB,KAAK,IAAI;AACvC,QAAM,YAAY,eAAe,GAAG;AACpC,QAAM,UAAU,aAAa,GAAG;AAChC,QAAM,QAAQ,WAAW,KAAK,IAAI;AAClC,QAAM,SAAS,mBAAmB,IAAI,OAAO;AAE7C,SAAO;AAAA,IACL,eAAe;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAMA,SAAS,WAAW,GAAgC;AAClD,QAAM,OAAO,EAAE,MAAM,EAAE,MAAM;AAC7B,QAAM,SAAS,EAAE,WAAW,EAAE,MAAM;AAQpC,QAAM,OAAgB;AAAA,IACpB,OAAO,UAAU,CAAC;AAAA,IAClB,UAAU,KAAK,MAAM,MAAM;AAAA,IAC3B,SAAS,KAAK,MAAM,KAAK;AAAA,IACzB,UAAU,KAAK,QAAQ,MAAM;AAAA,IAC7B,OAAO,CAAC;AAAA,IACR,OAAO,CAAC;AAAA,IACR,SAAS,CAAC;AAAA,EACZ;AAEA,IAAE,MAAM,EAAE,KAAK,CAAC,GAAG,OAAO;AACxB,SAAK,MAAM,KAAK,UAAU,GAAG,EAAE,CAAC;AAAA,EAClC,CAAC;AACD,IAAE,MAAM,EAAE,KAAK,CAAC,GAAG,OAAO;AACxB,SAAK,MAAM,KAAK,UAAU,GAAG,EAAE,CAAC;AAAA,EAClC,CAAC;AACD,IAAE,oCAAoC,EAAE,KAAK,CAAC,GAAG,OAAO;AACtD,SAAK,QAAQ,KAAK,YAAY,GAAG,EAAE,CAAC;AAAA,EACtC,CAAC;AAED,SAAO;AACT;AAQA,SAAS,UAAU,GAA2C;AAC5D,QAAM,YAAY,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,CAAC;AACrD,MAAI,UAAW,QAAO;AACtB,MAAI;AACJ,IAAE,OAAO,EAAE,KAAK,CAAC,GAAG,OAAO;AACzB,QAAI,MAAO;AACX,QAAI,EAAE,EAAE,EAAE,QAAQ,KAAK,EAAE,SAAS,EAAG;AACrC,UAAM,OAAO,KAAK,EAAE,EAAE,EAAE,KAAK,CAAC;AAC9B,QAAI,KAAM,SAAQ;AAAA,EACpB,CAAC;AACD,SAAO,SAAS;AAClB;AAEA,SAAS,UAAU,GAAuB,IAAwD;AAChG,QAAM,aAAa,kBAAkB,GAAG,EAAE;AAC1C,SAAO;AAAA,IACL,MAAM,WAAW;AAAA,IACjB,UAAU,WAAW;AAAA,IACrB,WAAW,WAAW,YAAY;AAAA,IAClC,SAAS,WAAW;AAAA,IACpB,SAAS,WAAW;AAAA,IACpB;AAAA,EACF;AACF;AAEA,SAAS,UAAU,GAAuB,IAAwD;AAChG,QAAM,aAAa,kBAAkB,GAAG,EAAE;AAC1C,SAAO;AAAA,IACL,KAAK,WAAW,OAAO;AAAA,IACvB,MAAM,WAAW;AAAA,IACjB,UAAU,WAAW;AAAA,IACrB,MAAM,WAAW;AAAA,IACjB,OAAO,WAAW;AAAA,IAClB,OAAO,WAAW;AAAA,IAClB,aAAa,WAAW;AAAA,IACxB;AAAA,EACF;AACF;AAEA,SAAS,YACP,GACA,IACc;AACd,QAAM,aAAa,kBAAkB,GAAG,EAAE;AAC1C,SAAO;AAAA,IACL,MAAM,WAAW;AAAA,IACjB,KAAK,WAAW;AAAA,IAChB,SAAS,KAAK,EAAE,EAAE,EAAE,KAAK,CAAC,KAAK;AAAA,IAC/B;AAAA,EACF;AACF;AAMA,SAAS,iBAAiB,KAAc,MAAwB;AAC9D,QAAM,OAAoB,CAAC;AAE3B,MAAI,IAAI,OAAO;AACb,SAAK,QAAQ,QAAQ,IAAI,OAAO,EAAE,MAAM,QAAQ,GAAG,IAAI,KAAK;AAAA,EAC9D;AAEA,aAAW,KAAK,IAAI,OAAO;AACzB,UAAM,OAAO,EAAE,MAAM,YAAY;AACjC,QAAI,SAAS,iBAAiB,EAAE,YAAY,QAAW;AACrD,WAAK,cAAc,YAAY,EAAE,SAAS,CAAC;AAAA,IAC7C,WAAW,SAAS,cAAc,EAAE,YAAY,QAAW;AACzD,YAAM,OAAO,EAAE,QACZ,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC7B,WAAK,WAAW,YAAY,MAAM,CAAC;AAAA,IACrC,WAAW,SAAS,YAAY,EAAE,YAAY,QAAW;AACvD,WAAK,SAAS,YAAY,EAAE,SAAS,CAAC;AAAA,IACxC,WAAW,SAAS,iBAAiB,EAAE,YAAY,QAAW;AAC5D,WAAK,aAAa,YAAY,EAAE,SAAS,CAAC;AAAA,IAC5C,WAAW,SAAS,kBAAkB,EAAE,YAAY,QAAW;AAC7D,WAAK,cAAc,YAAY,EAAE,SAAS,CAAC;AAAA,IAC7C,WAAW,SAAS,cAAc,EAAE,YAAY,QAAW;AACzD,WAAK,WAAW,YAAY,EAAE,SAAS,CAAC;AAAA,IAC1C,WAAW,SAAS,YAAY,EAAE,YAAY,QAAW;AACvD,WAAK,SAAS,YAAY,EAAE,SAAS,CAAC;AAAA,IACxC,WAAW,SAAS,eAAe,EAAE,YAAY,QAAW;AAC1D,WAAK,YAAY,YAAY,EAAE,SAAS,CAAC;AAAA,IAC3C,WAAW,SAAS,eAAe,EAAE,YAAY,QAAW;AAC1D,WAAK,YAAY,YAAY,EAAE,SAAS,CAAC;AAAA,IAC3C,WAAW,SAAS,cAAc,EAAE,YAAY,QAAW;AACzD,WAAK,WAAW,YAAY,EAAE,SAAS,CAAC;AAAA,IAC1C,WAAW,SAAS,sBAAsB,EAAE,YAAY,QAAW;AACjE,WAAK,kBAAkB,YAAY,EAAE,SAAS,CAAC;AAAA,IACjD,WAAW,EAAE,YAAY,QAAW;AAClC,WAAK,UAAU,QAAQ,EAAE,SAAS,EAAE,MAAM,QAAQ,MAAM,UAAU,GAAG,EAAE,OAAO;AAAA,IAChF,WAAW,EAAE,WAAW,YAAY,MAAM,kBAAkB,EAAE,YAAY,QAAW;AACnF,YAAM,UAAU,mBAAmB,KAAK,EAAE,OAAO,IAAI,CAAC,GAAG,KAAK;AAC9D,UAAI,WAAW,CAAC,KAAK,SAAS;AAC5B,aAAK,UAAU,QAAQ,SAAS,EAAE,MAAM,QAAQ,WAAW,eAAe,GAAG,EAAE,OAAO;AAAA,MACxF;AAAA,IACF;AAAA,EACF;AAEA,aAAW,KAAK,IAAI,OAAO;AACzB,UAAM,MAAM,EAAE,KAAK,YAAY;AAC/B,QAAI,QAAQ,eAAe,EAAE,MAAM;AACjC,YAAM,WAAW,YAAY,EAAE,MAAM,IAAI;AACzC,WAAK,YAAY,QAAQ,UAAU,EAAE,MAAM,QAAQ,KAAK,YAAY,GAAG,EAAE,IAAI;AAAA,IAC/E;AAAA,EACF;AAEA,SAAO;AACT;AAMA,SAAS,eAAe,KAAyB;AAC/C,QAAM,KAAgB;AAAA,IACpB,kBAAkB,CAAC;AAAA,IACnB,QAAQ,CAAC;AAAA,IACT,SAAS,CAAC;AAAA,EACZ;AACA,MAAI;AAEJ,aAAW,KAAK,IAAI,OAAO;AACzB,UAAM,WAAW,EAAE,UAAU,YAAY;AACzC,QAAI,CAAC,UAAU,WAAW,KAAK,KAAK,EAAE,YAAY,OAAW;AAE7D,UAAM,QAAQ,EAAE;AAChB,YAAQ,UAAU;AAAA,MAChB,KAAK;AACH,WAAG,QAAQ,YAAY,OAAO,CAAC;AAC/B;AAAA,MACF,KAAK;AACH,WAAG,OAAO,YAAY,OAAO,CAAC;AAC9B;AAAA,MACF,KAAK;AACH,WAAG,MAAM,YAAY,OAAO,CAAC;AAC7B;AAAA,MACF,KAAK;AACH,WAAG,cAAc,YAAY,OAAO,CAAC;AACrC;AAAA,MACF,KAAK;AACH,WAAG,WAAW,YAAY,OAAO,CAAC;AAClC;AAAA,MACF,KAAK;AACH,WAAG,SAAS,YAAY,OAAO,CAAC;AAChC;AAAA,MACF,KAAK;AACH,WAAG,iBAAiB,KAAK,YAAY,OAAO,CAAC,CAAC;AAC9C;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,kBAAU,EAAE,KAAK,YAAY,OAAO,CAAC,EAAE;AACvC,WAAG,OAAO,KAAK,OAAO;AACtB;AAAA,MACF,KAAK;AACH,YAAI,QAAS,SAAQ,YAAY,YAAY,OAAO,CAAC;AACrD;AAAA,MACF,KAAK;AACH,YAAI,QAAS,SAAQ,OAAO,YAAY,OAAO,CAAC;AAChD;AAAA,MACF,KAAK,kBAAkB;AACrB,cAAM,IAAI,OAAO,SAAS,OAAO,EAAE;AACnC,YAAI,WAAW,OAAO,SAAS,CAAC,EAAG,SAAQ,QAAQ,YAAY,GAAG,CAAC;AACnE;AAAA,MACF;AAAA,MACA,KAAK,mBAAmB;AACtB,cAAM,IAAI,OAAO,SAAS,OAAO,EAAE;AACnC,YAAI,WAAW,OAAO,SAAS,CAAC,EAAG,SAAQ,SAAS,YAAY,GAAG,CAAC;AACpE;AAAA,MACF;AAAA,MACA,KAAK;AACH,YAAI,QAAS,SAAQ,MAAM,YAAY,OAAO,CAAC;AAC/C;AAAA,MACF;AACE,WAAG,QAAQ,KAAK,EAAE,UAAU,OAAO,YAAY,OAAO,CAAC,EAAE,CAAC;AAAA,IAC9D;AAAA,EACF;AAEA,SAAO;AACT;AAMA,SAAS,aAAa,KAA2B;AAC/C,QAAM,KAAkB,CAAC;AACzB,aAAW,KAAK,IAAI,OAAO;AACzB,UAAM,OAAO,EAAE,MAAM,YAAY;AACjC,QAAI,CAAC,MAAM,WAAW,UAAU,KAAK,EAAE,YAAY,OAAW;AAC9D,YAAQ,MAAM;AAAA,MACZ,KAAK;AACH,WAAG,OAAO,YAAY,EAAE,SAAS,CAAC;AAClC;AAAA,MACF,KAAK;AACH,WAAG,OAAO,YAAY,EAAE,SAAS,CAAC;AAClC;AAAA,MACF,KAAK;AACH,WAAG,UAAU,YAAY,EAAE,SAAS,CAAC;AACrC;AAAA,MACF,KAAK;AACH,WAAG,QAAQ,YAAY,EAAE,SAAS,CAAC;AACnC;AAAA,MACF,KAAK;AACH,WAAG,cAAc,YAAY,EAAE,SAAS,CAAC;AACzC;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,WAAG,QAAQ,YAAY,EAAE,SAAS,CAAC;AACnC;AAAA,MACF,KAAK;AACH,WAAG,WAAW,YAAY,EAAE,SAAS,CAAC;AACtC;AAAA,IACJ;AAAA,EACF;AACA,SAAO;AACT;AAMA,SAAS,WAAW,KAAc,MAAwB;AACxD,QAAM,MAAmB;AAAA,IACvB,YAAY,CAAC;AAAA,IACb,OAAO,CAAC;AAAA,IACR,OAAO,CAAC;AAAA,IACR,aAAa,CAAC;AAAA,IACd,eAAe,CAAC;AAAA,EAClB;AAEA,aAAW,KAAK,IAAI,OAAO;AACzB,UAAM,MAAM,EAAE,IAAI,YAAY;AAC9B,QAAI,QAAQ,eAAe,EAAE,MAAM;AACjC,UAAI,YAAY,YAAY,EAAE,MAAM,IAAI;AAAA,IAC1C,WAAW,QAAQ,eAAe,EAAE,YAAY,EAAE,MAAM;AACtD,YAAM,MAAyB;AAAA,QAC7B,UAAU,EAAE;AAAA,QACZ,MAAM,YAAY,EAAE,MAAM,IAAI;AAAA,QAC9B,YAAY,EAAE,SAAS,YAAY,MAAM;AAAA,MAC3C;AACA,UAAI,WAAW,KAAK,GAAG;AAAA,IACzB,WAAW,QAAQ,eAAe,EAAE,QAAQ,EAAE,QAAQ,WAAW,EAAE,IAAI,GAAG;AACxE,UAAI,MAAM,KAAK;AAAA,QACb,KAAK;AAAA,QACL,MAAM,EAAE,KAAK,YAAY;AAAA,QACzB,MAAM,YAAY,EAAE,MAAM,IAAI;AAAA,QAC9B,OAAO,EAAE,WAAW;AAAA,MACtB,CAAC;AAAA,IACH,YACG,QAAQ,UAAU,QAAQ,sBAAsB,QAAQ,oBACzD,EAAE,MACF;AACA,YAAM,OAAiB;AAAA,QACrB;AAAA,QACA,MAAM,YAAY,EAAE,MAAM,IAAI;AAAA,QAC9B,OAAO,EAAE;AAAA,QACT,MAAM,EAAE;AAAA,QACR,aAAa,WAAW,EAAE,KAAK;AAAA,MACjC;AACA,UAAI,MAAM,KAAK,IAAI;AAAA,IACrB,WAAW,QAAQ,cAAc,EAAE,MAAM;AACvC,UAAI,WAAW,EAAE,MAAM,YAAY,EAAE,MAAM,IAAI,GAAG,aAAa,EAAE,YAAY;AAAA,IAC/E,WAAW,QAAQ,gBAAgB,EAAE,MAAM;AACzC,UAAI,YAAY,KAAK,YAAY,EAAE,MAAM,IAAI,CAAC;AAAA,IAChD,WAAW,QAAQ,kBAAkB,EAAE,MAAM;AAC3C,UAAI,cAAc,KAAK,YAAY,EAAE,MAAM,IAAI,CAAC;AAAA,IAClD;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,WAAW,MAAuB;AACzC,QAAM,IAAI,KAAK,YAAY;AAC3B,SACE,MAAM,yBAAyB,MAAM,0BAA0B,MAAM;AAEzE;AASO,SAAS,WAAW,OAAoD;AAC7E,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,SAAO,MACJ,MAAM,KAAK,EACX,OAAO,OAAO,EACd,IAAI,CAAC,UAAU;AACd,QAAI,MAAM,YAAY,MAAM,MAAO,QAAO;AAC1C,UAAM,IAAI,iBAAiB,KAAK,KAAK;AACrC,QAAI,CAAC,EAAG,QAAO;AACf,UAAM,IAAI,OAAO,SAAS,EAAE,CAAC,GAAI,EAAE;AACnC,UAAM,IAAI,OAAO,SAAS,EAAE,CAAC,GAAI,EAAE;AACnC,QAAI,CAAC,OAAO,SAAS,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,EAAG,QAAO;AACvD,WAAO,EAAE,OAAO,GAAG,QAAQ,EAAE;AAAA,EAC/B,CAAC,EACA,OAAO,CAAC,MAAsD,MAAM,MAAS;AAClF;AAMA,SAAS,kBACP,IACA,IACwB;AACxB,QAAM,SAAiC,CAAC;AACxC,QAAM,UAAW,GAA4C;AAC7D,MAAI,SAAS;AACX,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,OAAO,GAAG;AAC5C,aAAO,EAAE,YAAY,CAAC,IAAI;AAAA,IAC5B;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,KAAK,IAAoC,MAAkC;AAClF,QAAM,IAAI,GAAG,KAAK,IAAI;AACtB,SAAO,MAAM,UAAa,MAAM,KAAK,SAAY;AACnD;AAEA,SAAS,KAAK,GAA+B;AAC3C,UAAQ,KAAK,IAAI,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAC7C;AAEA,SAAS,YAAY,GAAuB,YAAyB;AACnE,MAAI;AACJ,MAAI;AACF,WAAO,IAAI,IAAI,UAAU;AAAA,EAC3B,QAAQ;AACN,WAAO,IAAI,IAAI,uBAAuB;AAAA,EACxC;AACA,QAAM,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,MAAM;AACnD,MAAI,UAAU;AACZ,QAAI;AACF,aAAO,IAAI,IAAI,UAAU,IAAI;AAAA,IAC/B,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,YAAY,MAAc,MAAmB;AACpD,MAAI;AACF,WAAO,IAAI,IAAI,MAAM,IAAI,EAAE,SAAS;AAAA,EACtC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,QAAW,OAAU,QAAmB,KAA0B;AACzE,SAAO,EAAE,OAAO,QAAQ,IAAI;AAC9B;AAEA,SAAS,YAAe,OAAU,GAA2B;AAC3D,QAAM,SAAoB,EAAE,WACxB,EAAE,MAAM,QAAQ,UAAU,EAAE,SAAS,IACrC,EAAE,OACA,EAAE,MAAM,QAAQ,MAAM,EAAE,KAAK,IAC7B,EAAE,YACA,EAAE,MAAM,QAAQ,WAAW,EAAE,UAAU,IACvC,EAAE,MAAM,OAAO;AACvB,SAAO,EAAE,OAAO,QAAQ,KAAK,EAAE,QAAQ;AACzC;;;AGrcA,IAAM,aAAa;AAEZ,IAAM,aAAN,cAAyB,MAAM;AAAA,EAC3B;AAAA,EACS;AAAA,EAClB,YAAY,SAAiB,KAAa,SAA+B;AACvE,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,MAAM;AACX,SAAK,QAAQ,SAAS;AAAA,EACxB;AACF;AAcA,eAAsB,YACpB,KACA,UAA8B,CAAC,GACH;AAC5B,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,YAAY,QAAQ,aAAa;AAEvC,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AAAA,EACtB,QAAQ;AACN,UAAM,IAAI,WAAW,gBAAgB,GAAG,IAAI,GAAG;AAAA,EACjD;AACA,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU;AAC/D,UAAM,IAAI;AAAA,MACR,yBAAyB,OAAO,QAAQ;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAQ,IAAI,gBAAgB;AAClC,QAAM,gBAAgB,MAAM,MAAM,MAAM,QAAQ,QAAQ,MAAM;AAC9D,UAAQ,QAAQ,iBAAiB,SAAS,eAAe,EAAE,MAAM,KAAK,CAAC;AACvE,QAAM,UAAU;AAAA,IACd,MAAM,MAAM,MAAM,IAAI,WAAW,qBAAqB,GAAG,CAAC;AAAA,IAC1D;AAAA,EACF;AAEA,QAAM,aAAa,oBAAoB,QAAQ,gBAAgB;AAC/D,QAAM,QAAQ,YAAY,IAAI;AAE9B,MAAI;AACF,QAAI,aAAa,OAAO,SAAS;AACjC,QAAI,YAAY;AAChB,QAAI;AACJ,WAAO,MAAM;AACX,UAAI;AACF,mBAAW,MAAM,MAAM,YAAY;AAAA,UACjC,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,SAAS;AAAA,YACP,cAAc;AAAA,YACd,QAAQ;AAAA,UACV;AAAA,UACA,QAAQ,MAAM;AAAA,QAChB,CAAC;AAAA,MACH,SAAS,KAAK;AACZ,cAAM,IAAI,WAAW,0BAA0B,UAAU,KAAK,SAAS,GAAG,CAAC,IAAI,KAAK;AAAA,UAClF,OAAO;AAAA,QACT,CAAC;AAAA,MACH;AAEA,UAAI,WAAW,SAAS,MAAM,GAAG;AAC/B,cAAM,WAAW,SAAS,QAAQ,IAAI,UAAU;AAChD,YAAI,CAAC,UAAU;AACb,gBAAM,IAAI;AAAA,YACR,YAAY,SAAS,MAAM,SAAS,UAAU;AAAA,YAC9C;AAAA,UACF;AAAA,QACF;AACA,YAAI,aAAa,cAAc;AAC7B,gBAAM,IAAI;AAAA,YACR,uBAAuB,YAAY,yBAAyB,UAAU;AAAA,YACtE;AAAA,UACF;AAAA,QACF;AAEA,cAAM,SAAS,MAAM,OAAO,EAAE,MAAM,MAAM,MAAS;AACnD,qBAAa,IAAI,IAAI,UAAU,UAAU,EAAE,SAAS;AACpD,qBAAa;AACb;AAAA,MACF;AAEA,YAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAM,UAAU,gBAAgB,SAAS,OAAO;AAChD,YAAM,cAAc,uBAAuB,QAAQ,cAAc,CAAC;AAClE,YAAM,OAAkB;AAAA,QACtB,cAAc,OAAO,SAAS;AAAA,QAC9B,UAAU;AAAA,QACV,QAAQ,SAAS;AAAA,QACjB,YAAY,SAAS;AAAA,QACrB;AAAA,QACA,eAAe;AAAA,QACf,YAAY,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK;AAAA,QAChD,WAAW,OAAO,WAAW,MAAM,MAAM;AAAA,QACzC;AAAA,MACF;AACA,aAAO,EAAE,MAAM,KAAK;AAAA,IACtB;AAAA,EACF,UAAE;AACA,iBAAa,OAAO;AACpB,YAAQ,QAAQ,oBAAoB,SAAS,aAAa;AAC1D,eAAW;AAAA,EACb;AACF;AAEA,SAAS,WAAW,QAAyB;AAC3C,SAAO,WAAW,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW;AAC5F;AAEA,SAAS,gBAAgB,SAA0C;AACjE,QAAM,MAA8B,CAAC;AACrC,UAAQ,QAAQ,CAAC,GAAG,MAAM;AACxB,QAAI,EAAE,YAAY,CAAC,IAAI;AAAA,EACzB,CAAC;AACD,SAAO;AACT;AAEA,SAAS,uBAAuB,OAAoC;AAClE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,MAAM,QAAQ,GAAG;AAC9B,UAAQ,SAAS,KAAK,QAAQ,MAAM,MAAM,GAAG,IAAI,GAAG,KAAK,EAAE,YAAY;AACzE;AAEA,SAAS,oBAAoB,SAA0C;AACrE,MAAI,CAAC,QAAS,QAAO,MAAM;AAC3B,QAAM,WAAW,QAAQ,IAAI;AAC7B,UAAQ,IAAI,+BAA+B;AAC3C,SAAO,MAAM;AACX,QAAI,aAAa,QAAW;AAC1B,aAAO,QAAQ,IAAI;AAAA,IACrB,OAAO;AACL,cAAQ,IAAI,+BAA+B;AAAA,IAC7C;AAAA,EACF;AACF;AAEA,SAAS,SAAS,KAAsB;AACtC,MAAI,eAAe,MAAO,QAAO,IAAI;AACrC,SAAO,OAAO,GAAG;AACnB;;;ACrHA,IAAMA,cAAa;AAOZ,IAAM,2BAAN,cAAuC,MAAM;AAAA,EAChC;AAAA,EAClB,YAAY,SAAiB,SAA+B;AAC1D,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,QAAQ,SAAS;AAAA,EACxB;AACF;AAeA,eAAsB,gBACpB,KACA,UAAkC,CAAC,GACH;AAChC,QAAM,WAAW,QAAQ,YAAY,gBAAgB;AACrD,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,YAAY,QAAQ,aAAaA;AACvC,QAAM,YAAY,QAAQ,aAAa;AAEvC,MAAI,QAAQ,QAAQ,SAAS;AAC3B,UAAM,IAAI,yBAAyB,yCAAyC;AAAA,EAC9E;AAEA,QAAM,QAAQ,YAAY,IAAI;AAC9B,QAAM,UAAU,MAAM,SAAS,OAAO,EAAE,kBAAkB,QAAQ,iBAAiB,CAAC;AACpF,MAAI;AACJ,MAAI;AACF,WAAO,MAAM,QAAQ,QAAQ,EAAE,UAAU,CAAC;AAC1C,UAAM,MAAM,MAAM,KAAK,KAAK,KAAK,EAAE,WAAW,UAAU,CAAC;AACzD,UAAM,OAAO,MAAM,KAAK,QAAQ;AAChC,UAAM,OAAkB;AAAA,MACtB,cAAc;AAAA,MACd,UAAU,IAAI;AAAA,MACd,QAAQ,IAAI;AAAA,MACZ,YAAY,IAAI;AAAA,MAChB,SAAS,IAAI;AAAA,MACb,eAAe;AAAA,MACf,YAAY,KAAK,MAAM,YAAY,IAAI,IAAI,KAAK;AAAA,MAChD,WAAW,OAAO,WAAW,MAAM,MAAM;AAAA,MACzC,aAAa,IAAI,QAAQ,cAAc,GAAG,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,YAAY;AAAA,IAC9E;AACA,WAAO,EAAE,cAAc,MAAM,OAAO,KAAK;AAAA,EAC3C,UAAE;AACA,UAAM,MAAM,MAAM,EAAE,MAAM,MAAM,MAAS;AACzC,UAAM,QAAQ,MAAM,EAAE,MAAM,MAAM,MAAS;AAAA,EAC7C;AACF;AAgBO,SAAS,gBACd,SAA2B,MAAM,OAAO,YAAY,GAClC;AAClB,SAAO;AAAA,IACL,MAAM,OAAO,EAAE,iBAAiB,GAAG;AACjC,UAAI;AACJ,UAAI;AACF,cAAM,MAAM,OAAO;AAAA,MACrB,SAAS,KAAK;AACZ,cAAM,IAAI;AAAA,UACR;AAAA,UACA,EAAE,OAAO,IAAI;AAAA,QACf;AAAA,MACF;AAEA,UAAI;AACJ,UAAI;AACF,aAAK,MAAM,IAAI,SAAS,OAAO;AAAA,UAC7B,UAAU;AAAA,UACV,MAAM,mBAAmB,CAAC,6BAA6B,IAAI,CAAC;AAAA,QAC9D,CAAC;AAAA,MACH,SAAS,KAAK;AACZ,cAAM,IAAI;AAAA,UACR;AAAA,UACA,EAAE,OAAO,IAAI;AAAA,QACf;AAAA,MACF;AAEA,aAAO;AAAA,QACL,MAAM,QAAQ,EAAE,UAAU,GAAG;AAC3B,gBAAM,MAAM,MAAM,GAAG,WAAW;AAAA,YAC9B;AAAA,YACA,mBAAmB,CAAC,CAAC;AAAA,UACvB,CAAC;AACD,gBAAM,IAAI,MAAM,IAAI,QAAQ;AAC5B,iBAAO;AAAA,YACL,MAAM,KAAK,KAAK,MAAM;AACpB,oBAAM,WAAW,MAAM,EAAE,KAAK,KAAK;AAAA,gBACjC,WAAW,KAAK;AAAA,gBAChB,SAAS,KAAK;AAAA,cAChB,CAAC;AACD,oBAAM,SAAS,UAAU,OAAO,KAAK;AACrC,oBAAM,aAAa,UAAU,WAAW,KAAK;AAC7C,oBAAM,UAAU,WAAW,MAAM,SAAS,WAAW,IAAI,CAAC;AAC1D,qBAAO;AAAA,gBACL;AAAA,gBACA;AAAA,gBACA,UAAU,EAAE,IAAI;AAAA,gBAChB;AAAA,cACF;AAAA,YACF;AAAA,YACA,MAAM,UAAU;AACd,qBAAO,EAAE,QAAQ;AAAA,YACnB;AAAA,YACA,MAAM,QAAQ;AACZ,oBAAM,IAAI,MAAM;AAAA,YAClB;AAAA,UACF;AAAA,QACF;AAAA,QACA,MAAM,QAAQ;AACZ,gBAAM,GAAG,MAAM;AAAA,QACjB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;AC/KA,IAAM,qBAAqB,oBAAI,IAAI;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,oBACd,SACsB;AACtB,QAAM,UAAoB,CAAC;AAE3B,QAAM,WAAW,QAAQ,IAAI,OAAO,KAAK,EAAE,YAAY,KAAK;AAC5D,QAAMC,gBAAe,aAAa;AAClC,QAAM,mBAAmB,CAACA,iBAAgB,mBAAmB,IAAI,QAAQ;AACzE,MAAIA,cAAc,SAAQ,KAAK,eAAe;AAAA,WACrC,iBAAkB,SAAQ,KAAK,sBAAsB,QAAQ,IAAI,KAAK,GAAG;AAElF,QAAM,cAAc,QAAQ,KAAK,gBAAgB;AACjD,MAAI,YAAa,SAAQ,KAAK,gBAAgB;AAE9C,QAAMC,oBAAmB,QAAQ,KAAK,cAAc;AACpD,MAAIA,kBAAkB,SAAQ,KAAK,cAAc;AAEjD,QAAM,YACJ,QAAQ,UAAU,UAAU,UAC5B,QAAQ,UAAU,gBAAgB,UAClC,QAAQ,UAAU,OAAO,WAAW,KACpC,QAAQ,UAAU,SAAS,UAC3B,QAAQ,UAAU,QAAQ,UAC1B,QAAQ,UAAU,aAAa,UAC/B,QAAQ,UAAU,QAAQ,WAAW;AACvC,MAAI,UAAW,SAAQ,KAAK,SAAS;AAErC,QAAM,iBACJ,QAAQ,QAAQ,SAAS,UACzB,QAAQ,QAAQ,UAAU,UAC1B,QAAQ,QAAQ,UAAU;AAC5B,MAAI,eAAgB,SAAQ,KAAK,cAAc;AAE/C,QAAM,gBAAgB,QAAQ,OAAO,WAAW;AAChD,MAAI,cAAe,SAAQ,KAAK,YAAY;AAE5C,QAAMC,mBAAkB,QAAQ,MAAM,WAAW,WAAW;AAC5D,MAAIA,iBAAiB,SAAQ,KAAK,aAAa;AAK/C,QAAM,UACHF,iBAAgB,qBACjB,eACAC,qBACA,aACA,kBACA,iBACAC;AAEF,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,QAAQ,KAAK,IAAI,KAAK;AAAA,EAChC;AACF;;;ACrFO,SAAS,sBACd,YACA,cACgB;AAChB,QAAM,iBAAiB,IAAI,IAAI,WAAW,IAAI,MAAM,IAAI,OAAO,CAAC;AAChE,QAAM,mBAAmB,IAAI,IAAI,aAAa,IAAI,MAAM,IAAI,OAAO,CAAC;AAEpE,QAAM,sBAAsB,aAAa,IAAI,MAC1C,OAAO,CAAC,MAAM,CAAC,eAAe,IAAI,QAAQ,CAAC,CAAC,CAAC,EAC7C,IAAI,aAAa;AACpB,QAAM,qBAAqB,WAAW,IAAI,MACvC,OAAO,CAAC,MAAM,CAAC,iBAAiB,IAAI,QAAQ,CAAC,CAAC,CAAC,EAC/C,IAAI,aAAa;AAEpB,QAAM,iBAAiB,IAAI,IAAI,WAAW,IAAI,MAAM,IAAI,OAAO,CAAC;AAChE,QAAM,mBAAmB,IAAI,IAAI,aAAa,IAAI,MAAM,IAAI,OAAO,CAAC;AAEpE,QAAM,sBAAsB,aAAa,IAAI,MAC1C,OAAO,CAAC,MAAM,CAAC,eAAe,IAAI,QAAQ,CAAC,CAAC,CAAC,EAC7C,IAAI,aAAa;AACpB,QAAM,qBAAqB,WAAW,IAAI,MACvC,OAAO,CAAC,MAAM,CAAC,iBAAiB,IAAI,QAAQ,CAAC,CAAC,CAAC,EAC/C,IAAI,aAAa;AAEpB,SAAO;AAAA,IACL,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,eAAe,WAAW,IAAI,SAAS,SAAS,aAAa,IAAI,SAAS;AAAA,IAC1E,kBAAkB,WAAW,IAAI,YAAY,SAAS,aAAa,IAAI,YAAY;AAAA,IACnF;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,mBAAmB,KAAK,IAAI,GAAG,aAAa,OAAO,SAAS,WAAW,OAAO,MAAM;AAAA,EACtF;AACF;AAEA,SAAS,QAAQ,GAAuB;AAMtC,SAAO;AAAA,IACL,QAAQ,EAAE,QAAQ;AAAA,IAClB,QAAQ,EAAE,YAAY;AAAA,IACtB,SAAS,EAAE,aAAa;AAAA,IACxB,SAAS,EAAE,WAAW;AAAA,IACtB,QAAQ,EAAE,WAAW;AAAA,EACvB,EAAE,KAAK,GAAG;AACZ;AAEA,SAAS,QAAQ,GAAuB;AACtC,SAAO;AAAA,IACL,QAAQ,EAAE,OAAO;AAAA,IACjB,QAAQ,EAAE,QAAQ;AAAA,IAClB,SAAS,EAAE,YAAY;AAAA,IACvB,QAAQ,EAAE,QAAQ;AAAA,IAClB,QAAQ,EAAE,SAAS;AAAA,EACrB,EAAE,KAAK,GAAG;AACZ;AAEA,SAAS,cAAc,GAAe;AACpC,QAAM,MAAqD,CAAC;AAC5D,MAAI,EAAE,KAAM,KAAI,OAAO,EAAE;AACzB,MAAI,EAAE,SAAU,KAAI,WAAW,EAAE;AACjC,MAAI,EAAE,UAAW,KAAI,YAAY,EAAE;AACnC,MAAI,EAAE,YAAY,OAAW,KAAI,UAAU,EAAE;AAC7C,SAAO;AACT;AAEA,SAAS,cAAc,GAAe;AACpC,QAAM,MAAqD,EAAE,KAAK,EAAE,IAAI;AACxE,MAAI,EAAE,SAAS,OAAW,KAAI,OAAO,EAAE;AACvC,MAAI,EAAE,aAAa,OAAW,KAAI,WAAW,EAAE;AAC/C,SAAO;AACT;;;AClFO,SAAS,eACd,QACA,WAC8C;AAC9C,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAC5D,MAAI,CAAC,QAAQ;AACX,WAAO,EAAE,QAAQ,WAAW,QAAQ,SAAS,MAAM,aAAa,KAAK,EAAE;AAAA,EACzE;AACA,MAAI,OAAO,SAAS;AAClB,eAAW,MAAM;AACjB,WAAO,EAAE,QAAQ,WAAW,QAAQ,SAAS,MAAM,aAAa,KAAK,EAAE;AAAA,EACzE;AACA,QAAM,UAAU,MAAM,WAAW,MAAM;AACvC,SAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AACxD,SAAO;AAAA,IACL,QAAQ,WAAW;AAAA,IACnB,SAAS,MAAM;AACb,mBAAa,KAAK;AAClB,aAAO,oBAAoB,SAAS,OAAO;AAAA,IAC7C;AAAA,EACF;AACF;;;AClBA,eAAsB,cACpB,MACA,UAAgC,CAAC,GACT;AACxB,QAAM,EAAE,QAAQ,QAAQ,IAAI,eAAe,QAAQ,QAAQ,QAAQ,aAAa,GAAK;AACrF,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,MAAM,EAAE,QAAQ,UAAU,SAAS,CAAC;AAC5D,QAAI,CAAC,IAAI,GAAI,QAAO,EAAE,KAAK,MAAM,QAAQ,IAAI,QAAQ,OAAO,MAAM;AAClE,UAAM,MAAM,MAAM,IAAI,KAAK;AAC3B,QAAI;AACJ,QAAI;AACJ,QAAI;AACF,aAAO,KAAK,MAAM,GAAG;AAAA,IACvB,SAAS,KAAK;AACZ,mBAAa,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IAC9D;AACA,WAAO,EAAE,KAAK,MAAM,QAAQ,IAAI,QAAQ,OAAO,MAAM,KAAK,MAAM,WAAW;AAAA,EAC7E,QAAQ;AACN,WAAO,EAAE,KAAK,MAAM,QAAQ,GAAG,OAAO,MAAM;AAAA,EAC9C,UAAE;AACA,YAAQ;AAAA,EACV;AACF;;;ACRA,IAAM,kBAAkB,oBAAI,IAAI,CAAC,SAAS,UAAU,CAAC;AAOrD,IAAM,mBAAmB,oBAAI,IAAI,CAAC,eAAe,QAAQ,eAAe,gBAAgB,SAAS,CAAC;AAS3F,SAAS,YAAY,MAA2B;AACrD,QAAM,SAAwB,CAAC;AAC/B,QAAM,WAAmD,CAAC;AAC1D,QAAM,eAAoC,CAAC;AAC3C,QAAM,oBAA2D,CAAC;AAGlE,MAAI;AACJ,MAAI,kBAAkB;AAEtB,QAAM,QAAQ,KAAK,MAAM,YAAY;AAErC,aAAW,CAAC,OAAO,OAAO,KAAK,MAAM,QAAQ,GAAG;AAC9C,UAAM,OAAO,QAAQ;AACrB,UAAM,iBAAiB,QAAQ,QAAQ,QAAQ,EAAE;AACjD,UAAM,UAAU,eAAe,KAAK;AACpC,QAAI,QAAQ,WAAW,EAAG;AAE1B,UAAM,YAAY,QAAQ,QAAQ,GAAG;AACrC,QAAI,cAAc,IAAI;AACpB,mBAAa,KAAK,EAAE,MAAM,KAAK,SAAS,QAAQ,mBAAmB,CAAC;AACpE;AAAA,IACF;AAEA,UAAM,OAAO,QAAQ,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,YAAY;AAC5D,UAAM,QAAQ,QAAQ,MAAM,YAAY,CAAC,EAAE,KAAK;AAEhD,QAAI,SAAS,cAAc;AACzB,UAAI,MAAM,WAAW,GAAG;AACtB,qBAAa,KAAK,EAAE,MAAM,KAAK,SAAS,QAAQ,qBAAqB,CAAC;AACtE;AAAA,MACF;AAGA,UAAI,CAAC,WAAW,iBAAiB;AAC/B,kBAAU,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,EAAE;AACtC,eAAO,KAAK,OAAO;AACnB,0BAAkB;AAAA,MACpB;AACA,cAAQ,WAAW,KAAK,EAAE,OAAO,MAAM,YAAY,GAAG,KAAK,CAAC;AAC5D;AAAA,IACF;AAEA,QAAI,SAAS,WAAW;AAEtB,UAAI,MAAM,WAAW,GAAG;AACtB,qBAAa,KAAK,EAAE,MAAM,KAAK,SAAS,QAAQ,kBAAkB,CAAC;AACnE;AAAA,MACF;AACA,eAAS,KAAK,EAAE,OAAO,KAAK,CAAC;AAC7B;AAAA,IACF;AAEA,QAAI,gBAAgB,IAAI,IAAI,GAAG;AAC7B,UAAI,CAAC,SAAS;AAGZ,qBAAa,KAAK;AAAA,UAChB;AAAA,UACA,KAAK;AAAA,UACL,QAAQ,KAAK,IAAI;AAAA,QACnB,CAAC;AACD;AAAA,MACF;AACA,cAAQ,MAAM,KAAK,EAAE,MAAM,MAA8B,SAAS,OAAO,KAAK,CAAC;AAC/E,wBAAkB;AAClB;AAAA,IACF;AAEA,QAAI,iBAAiB,IAAI,IAAI,GAAG;AAC9B,wBAAkB,KAAK,EAAE,MAAM,KAAK,CAAC;AACrC;AAAA,IACF;AAEA,iBAAa,KAAK,EAAE,MAAM,KAAK,SAAS,QAAQ,uBAAuB,IAAI,KAAK,CAAC;AAAA,EACnF;AAEA,SAAO,EAAE,QAAQ,UAAU,cAAc,kBAAkB;AAC7D;;;AClGA,eAAsB,YACpB,QACA,UAA8B,CAAC,GACT;AACtB,QAAM,MAAM,IAAI,IAAI,eAAe,MAAM,EAAE,SAAS;AACpD,QAAM,EAAE,QAAQ,QAAQ,IAAI,eAAe,QAAQ,QAAQ,QAAQ,aAAa,GAAK;AACrF,QAAM,QAAQ,EAAE,QAAQ,CAAC,GAAG,UAAU,CAAC,GAAG,cAAc,CAAC,GAAG,mBAAmB,CAAC,EAAE;AAClF,QAAM,aAAa,EAAE,OAAO,GAAG,UAAU,KAAK,aAAa,MAAM;AAEjE,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,UAAU,SAAS,CAAC;AAK3D,UAAM,aAAa,IAAI,QAAQ;AAC/B,UAAM,YAAY;AAAA,MAChB,OAAO,aAAa,IAAI;AAAA,MACxB,UAAU,IAAI,OAAO;AAAA,MACrB,aAAa,cAAc,CAAC,WAAW,IAAI,KAAK,GAAG;AAAA,IACrD;AAEA,QAAI,CAAC,IAAI,IAAI;AACX,aAAO,EAAE,KAAK,QAAQ,IAAI,QAAQ,OAAO,OAAO,YAAY,GAAG,WAAW,GAAG,MAAM;AAAA,IACrF;AAEA,UAAM,MAAM,MAAM,IAAI,KAAK;AAC3B,WAAO;AAAA,MACL;AAAA,MACA,QAAQ,IAAI;AAAA,MACZ,OAAO;AAAA,MACP;AAAA,MACA,YAAY,OAAO,WAAW,KAAK,MAAM;AAAA,MACzC;AAAA,MACA,GAAG,YAAY,GAAG;AAAA,IACpB;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,KAAK,QAAQ,GAAG,OAAO,OAAO,YAAY,GAAG,WAAW,YAAY,GAAG,MAAM;AAAA,EACxF,UAAE;AACA,YAAQ;AAAA,EACV;AACF;AAEA,SAAS,WAAW,GAAW,GAAoB;AACjD,MAAI;AACF,WAAO,IAAI,IAAI,CAAC,EAAE,WAAW,IAAI,IAAI,CAAC,EAAE;AAAA,EAC1C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AChEA,eAAsB,aACpB,QACA,UAA+B,CAAC,GACT;AACvB,QAAM,MAAM,IAAI,IAAI,gBAAgB,MAAM,EAAE,SAAS;AACrD,QAAM,EAAE,QAAQ,QAAQ,IAAI,eAAe,QAAQ,QAAQ,QAAQ,aAAa,GAAK;AAErF,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,UAAU,SAAS,CAAC;AAC3D,QAAI,CAAC,IAAI,GAAI,QAAO,EAAE,KAAK,QAAQ,IAAI,QAAQ,OAAO,OAAO,SAAS,OAAO,YAAY,EAAE;AAC3F,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,WAAO;AAAA,MACL;AAAA,MACA,QAAQ,IAAI;AAAA,MACZ,OAAO;AAAA,MACP,SAAS,mBAAmB,KAAK,IAAI;AAAA,MACrC,YAAY,aAAa,IAAI;AAAA,IAC/B;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,KAAK,QAAQ,GAAG,OAAO,OAAO,SAAS,OAAO,YAAY,EAAE;AAAA,EACvE,UAAE;AACA,YAAQ;AAAA,EACV;AACF;AAEO,SAAS,aAAa,MAAsB;AACjD,QAAM,MAAM,KAAK,MAAM,UAAU,GAAG,UAAU;AAC9C,QAAM,KAAK,KAAK,MAAM,cAAc,GAAG,UAAU;AACjD,SAAO,MAAM;AACf;;;ACkBA,eAAsB,QAAQ,KAAa,UAA0B,CAAC,GAAkB;AACtF,QAAM;AAAA,IACJ,QAAQ,eAAe;AAAA,IACvB,OAAO;AAAA,IACP,UAAU;AAAA,IACV,GAAG;AAAA,EACL,IAAI;AAEJ,QAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AAIzC,MAAI,SAAS,YAAY;AACvB,WAAO,oBAAoB;AAAA,MACzB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAGA,QAAM,EAAE,MAAM,YAAY,MAAM,WAAW,IAAI,MAAM,YAAY,KAAK,YAAY;AAClF,QAAM,eAAe,cAAc,YAAY,EAAE,SAAS,WAAW,SAAS,CAAC;AAE/E,MAAI,SAAS,UAAU;AACrB,UAAMC,UAAS,MAAM,UAAU;AAAA,MAC7B,SAAS;AAAA,MACT,QAAQ,IAAI,IAAI,WAAW,QAAQ,EAAE;AAAA,MACrC,cAAc,aAAa,MAAM,UAAU;AAAA,MAC3C,QAAQ,aAAa;AAAA,IACvB,CAAC;AACD,WAAO;AAAA,MACL,GAAG;AAAA,MACH;AAAA,MACA,OAAO;AAAA,MACP,WAAW,EAAE,MAAM,UAAU,WAAW,MAAM;AAAA,MAC9C,MAAM,EAAE,QAAQ,WAAW;AAAA,MAC3B,QAAAA;AAAA,IACF;AAAA,EACF;AAGA,QAAM,YAAY,oBAAoB,YAAY;AAClD,MAAI,CAAC,UAAU,QAAQ;AACrB,UAAMA,UAAS,MAAM,UAAU;AAAA,MAC7B,SAAS;AAAA,MACT,QAAQ,IAAI,IAAI,WAAW,QAAQ,EAAE;AAAA,MACrC,cAAc,aAAa,MAAM,UAAU;AAAA,MAC3C,QAAQ,aAAa;AAAA,IACvB,CAAC;AACD,WAAO;AAAA,MACL,GAAG;AAAA,MACH;AAAA,MACA,OAAO;AAAA,MACP,WAAW,EAAE,MAAM,UAAU,WAAW,MAAM;AAAA,MAC9C,MAAM,EAAE,QAAQ,WAAW;AAAA,MAC3B,QAAAA;AAAA,IACF;AAAA,EACF;AAQA,MAAI;AACJ,MAAI;AACJ,MAAI;AACF,UAAM,SAAS,MAAM,gBAAgB,KAAK;AAAA,MACxC,GAAG;AAAA,MACH,kBAAkB,aAAa;AAAA,MAC/B,QAAQ,aAAa;AAAA,IACvB,CAAC;AACD,mBAAe,OAAO;AACtB,oBAAgB,OAAO;AAAA,EACzB,SAAS,KAAK;AACZ,QAAI,EAAE,eAAe,0BAA2B,OAAM;AACtD,UAAMA,UAAS,MAAM,UAAU;AAAA,MAC7B,SAAS;AAAA,MACT,QAAQ,IAAI,IAAI,WAAW,QAAQ,EAAE;AAAA,MACrC,cAAc,aAAa,MAAM,UAAU;AAAA,MAC3C,QAAQ,aAAa;AAAA,IACvB,CAAC;AACD,WAAO;AAAA,MACL,GAAG;AAAA,MACH;AAAA,MACA,OAAO;AAAA,MACP,WAAW;AAAA,QACT,MAAM;AAAA,QACN,WAAW;AAAA,QACX,kBAAkB,UAAU;AAAA,QAC5B,mBAAmB,IAAI;AAAA,MACzB;AAAA,MACA,MAAM,EAAE,QAAQ,WAAW;AAAA,MAC3B,QAAAA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,iBAAiB,cAAc,cAAc,EAAE,SAAS,cAAc,SAAS,CAAC;AACtF,QAAM,YAAY,sBAAsB,cAAc,cAAc;AACpE,QAAM,SAAS,MAAM,UAAU;AAAA,IAC7B,SAAS;AAAA,IACT,QAAQ,IAAI,IAAI,cAAc,QAAQ,EAAE;AAAA,IACxC,cAAc,eAAe,MAAM,UAAU;AAAA,IAC7C,QAAQ,aAAa;AAAA,EACvB,CAAC;AAED,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,OAAO;AAAA,IACP,WAAW;AAAA,MACT,MAAM;AAAA,MACN,WAAW;AAAA,MACX,kBAAkB,UAAU;AAAA,IAC9B;AAAA,IACA,MAAM,EAAE,QAAQ,YAAY,UAAU,aAAa;AAAA,IACnD;AAAA,IACA;AAAA,EACF;AACF;AAEA,eAAe,oBAAoB,MAMjB;AAChB,QAAM,EAAE,KAAK,WAAW,cAAc,cAAc,aAAa,IAAI;AACrE,QAAM,SAAS,MAAM,gBAAgB,KAAK;AAAA,IACxC,GAAG;AAAA,IACH,kBAAkB,aAAa;AAAA,IAC/B,QAAQ,aAAa;AAAA,EACvB,CAAC;AACD,QAAM,SAAS,cAAc,OAAO,cAAc,EAAE,SAAS,OAAO,MAAM,SAAS,CAAC;AACpF,QAAM,SAAS,MAAM,UAAU;AAAA,IAC7B,SAAS;AAAA,IACT,QAAQ,IAAI,IAAI,OAAO,MAAM,QAAQ,EAAE;AAAA,IACvC,cAAc,OAAO,MAAM,UAAU;AAAA,IACrC,QAAQ,aAAa;AAAA,EACvB,CAAC;AACD,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,OAAO,OAAO;AAAA,IACd,WAAW,EAAE,MAAM,YAAY,WAAW,MAAM;AAAA,IAChD,MAAM,EAAE,QAAQ,IAAI,UAAU,OAAO,aAAa;AAAA,IAClD;AAAA,EACF;AACF;AAEA,eAAe,UAAU,MAKG;AAC1B,MAAI,CAAC,KAAK,QAAS,QAAO,CAAC;AAC3B,QAAM,CAAC,QAAQ,SAAS,QAAQ,IAAI,MAAM,QAAQ,IAAI;AAAA,IACpD,YAAY,KAAK,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC;AAAA,IAChD,aAAa,KAAK,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC;AAAA,IACjD,KAAK,eACD,cAAc,KAAK,cAAc,EAAE,QAAQ,KAAK,OAAO,CAAC,IACxD,QAAQ,QAAQ,MAAS;AAAA,EAC/B,CAAC;AACD,QAAM,SAAyB,EAAE,QAAQ,QAAQ;AACjD,MAAI,SAAU,QAAO,WAAW;AAChC,SAAO;AACT;;;AC/MA,IAAM,QAAQ,oBAAI,IAAoB;AAE/B,SAAS,YAAY,SAAyB;AACnD,QAAM,SAAS,MAAM,IAAI,OAAO;AAChC,MAAI,OAAQ,QAAO;AACnB,MAAI,KAAK;AACT,MAAI,IAAI;AACR,SAAO,IAAI,QAAQ,QAAQ;AACzB,UAAM,IAAI,QAAQ,CAAC;AACnB,QAAI,MAAM,KAAK;AACb,UAAI,QAAQ,IAAI,CAAC,MAAM,KAAK;AAC1B,cAAM;AACN,aAAK;AACL;AAAA,MACF;AACA,YAAM;AACN,WAAK;AACL;AAAA,IACF;AACA,QAAI,MAAM,KAAK;AACb,YAAM;AACN,WAAK;AACL;AAAA,IACF;AACA,QAAI,oBAAoB,KAAK,CAAC,GAAG;AAC/B,YAAM,KAAK,CAAC;AAAA,IACd,OAAO;AACL,YAAM;AAAA,IACR;AACA,SAAK;AAAA,EACP;AACA,QAAM;AACN,QAAM,WAAW,IAAI,OAAO,EAAE;AAC9B,QAAM,IAAI,SAAS,QAAQ;AAC3B,SAAO;AACT;AAEO,SAAS,WAAW,OAAe,UAA6B;AACrE,aAAW,KAAK,UAAU;AACxB,QAAI,YAAY,CAAC,EAAE,KAAK,KAAK,EAAG,QAAO;AAAA,EACzC;AACA,SAAO;AACT;;;AC1BA,IAAM,iBAAiB;AAEhB,SAAS,sBAAsB,MAA6B;AACjE,QAAM,MAAuB,CAAC;AAC9B,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,OAAO,KAAK,MAAM;AAExB,aAAW,OAAO,KAAK,MAAM,YAAY;AACvC,SAAK,KAAK,MAAM,IAAI,MAAM,UAAU;AAAA,EACtC;AAEA,aAAW,QAAQ,KAAK,IAAI,OAAO;AACjC,UAAM,MAAM,KAAK,IAAI,YAAY;AACjC,SAAK,QAAQ,UAAU,QAAQ,WAAW,KAAK,MAAM;AACnD,WAAK,KAAK,MAAM,KAAK,MAAM,WAAW;AAAA,IACxC;AAAA,EACF;AAIA,MAAI,KAAK,KAAK,QAAQ;AACpB,eAAW,SAAS,KAAK,KAAK,OAAO,SAAS,cAAc,GAAG;AAC7D,YAAM,MAAM,MAAM,CAAC,KAAK,MAAM,CAAC;AAC/B,UAAI,CAAC,IAAK;AACV,YAAM,UAAU,IAAI,KAAK;AACzB,UAAI,CAAC,QAAS;AACd,UAAI;AACJ,UAAI;AACF,mBAAW,IAAI,IAAI,SAAS,IAAI,EAAE,SAAS;AAAA,MAC7C,QAAQ;AACN;AAAA,MACF;AACA,WAAK,KAAK,MAAM,UAAU,aAAa;AAAA,IACzC;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,KACP,KACA,MACA,MACA,QACM;AAEN,MAAI,KAAK,IAAI,IAAI,EAAG;AACpB,OAAK,IAAI,IAAI;AACb,MAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAC3B;;;AC8CA,IAAM,eAAe;AAQrB,eAAsB,MAAM,SAA6C;AACvE,QAAM,aAAa,QAAQ,SAAS;AACpC,QAAM,cAAc,KAAK,IAAI,GAAG,QAAQ,eAAe,CAAC;AACxD,QAAM,WAAW,KAAK,IAAI,GAAG,QAAQ,YAAY,GAAG;AACpD,QAAM,iBAAiB,QAAQ,mBAAmB;AAClD,QAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,QAAM,UAAU,QAAQ,WAAW,CAAC;AAEpC,QAAM,QAAQ,gBAAgB,QAAQ,QAAQ;AAC9C,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,MACL,OAAO,CAAC;AAAA,MACR,SAAS,CAAC;AAAA,MACV,QAAQ,CAAC,EAAE,KAAK,QAAQ,UAAU,SAAS,uCAAuC,CAAC;AAAA,MACnF,WAAW,CAAC;AAAA,MACZ,WAAW;AAAA,IACb;AAAA,EACF;AACA,QAAM,SAAS,IAAI,IAAI,KAAK,EAAE;AAE9B,QAAM,aAAa,oBAAI,IAAY,CAAC,KAAK,CAAC;AAC1C,QAAM,QAAqB,CAAC,EAAE,KAAK,OAAO,OAAO,EAAE,CAAC;AAKpD,aAAW,QAAQ,QAAQ,YAAY,CAAC,GAAG;AACzC,UAAM,aAAa,gBAAgB,IAAI;AACvC,QAAI,CAAC,cAAc,WAAW,IAAI,UAAU,EAAG;AAC/C,QAAI,IAAI,IAAI,UAAU,EAAE,WAAW,OAAQ;AAC3C,eAAW,IAAI,UAAU;AACzB,UAAM,KAAK,EAAE,KAAK,YAAY,OAAO,EAAE,CAAC;AAAA,EAC1C;AAEA,QAAM,QAAgB,CAAC;AACvB,QAAM,SAAuB,CAAC;AAC9B,QAAM,YAAsB,CAAC;AAC7B,MAAI,YAAY;AAqBhB,QAAM,YAAY,oBAAI,IAAY;AAElC,SAAO,MAAM,SAAS,GAAG;AACvB,QAAI,QAAQ,gBAAgB,QAAQ,QAAS;AAK7C,UAAM,OAAO,MAAM,OAAO,GAAG,WAAW;AAGxC,UAAM,cAAqC,KAAK,IAAI,OAAO,SAAS;AAClE,UAAI;AACF,cAAM,OAAO,MAAM,QAAQ,KAAK,KAAK,QAAQ,cAAc;AAC3D,eAAO,EAAE,MAAM,MAAe,MAAM,KAAK;AAAA,MAC3C,SAAS,KAAK;AACZ,eAAO;AAAA,UACL,MAAM;AAAA,UACN;AAAA,UACA,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,QACxD;AAAA,MACF;AAAA,IACF,CAAC;AAED,UAAM,UAAU,MAAM,QAAQ,IAAI,WAAW;AAC7C,eAAW,UAAU,SAAS;AAC5B,UAAI,OAAO,SAAS,OAAO;AACzB,cAAM,WAAW,OAAO,KAAK,WAAW,KAAK;AAC7C,YAAI,UAAU,cAAc;AAC1B,gBAAM,KAAK,EAAE,GAAG,OAAO,MAAM,QAAQ,CAAC;AACtC;AAAA,QACF;AACA,eAAO,KAAK,EAAE,KAAK,OAAO,KAAK,KAAK,SAAS,OAAO,MAAM,CAAC;AAC3D;AAAA,MACF;AAEA,UAAI,OAAO,KAAK,QAAS,WAAU,KAAK,OAAO,KAAK,GAAG;AASvD,YAAM,WAAW,gBAAgB,OAAO,KAAK,MAAM,QAAQ,KAAK,OAAO,KAAK,MAAM;AAClF,UAAI,UAAU,IAAI,QAAQ,EAAG;AAC7B,gBAAU,IAAI,QAAQ;AAEtB,YAAM,KAAK,OAAO,IAAI;AACtB,UAAI,QAAQ,QAAQ;AAClB,gBAAQ,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM,QAAQ,QAAQ,MAAM,OAAO,CAAC;AAAA,MAC7E;AACA,UAAI,MAAM,UAAU,UAAU;AAC5B,oBAAY;AACZ;AAAA,MACF;AAEA,UAAI,OAAO,KAAK,SAAS,WAAY;AAErC,YAAM,aAAa,sBAAsB,OAAO,IAAI;AACpD,iBAAW,aAAa,YAAY;AAClC,cAAM,aAAa,gBAAgB,UAAU,MAAM,OAAO,KAAK,MAAM,QAAQ;AAC7E,YAAI,CAAC,WAAY;AACjB,YAAI,WAAW,IAAI,UAAU,EAAG;AAChC,cAAM,SAAS,IAAI,IAAI,UAAU;AACjC,YAAI,OAAO,WAAW,OAAQ;AAE9B,cAAM,uBAAuB,aAAa,OAAO,UAAU,SAAS,OAAO;AAC3E,cAAM,aAAa,UAAU,WAAW;AACxC,YAAI,CAAC,wBAAwB,EAAE,kBAAkB,YAAa;AAE9D,mBAAW,IAAI,UAAU;AACzB,cAAM,KAAK,EAAE,KAAK,YAAY,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;AAAA,MAC9D;AAAA,IACF;AAEA,QAAI,UAAW;AAAA,EACjB;AAEA,SAAO;AAAA,IACL;AAAA,IACA,SAAS,CAAC,GAAG,UAAU;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,aAAa,UAAkB,SAAmB,SAA4B;AACrF,MAAI,QAAQ,SAAS,KAAK,WAAW,UAAU,OAAO,EAAG,QAAO;AAChE,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,SAAO,WAAW,UAAU,OAAO;AACrC;AAuBO,SAAS,gBACd,MACA,MACA,UAA+B,CAAC,GACjB;AACf,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,MAAM,IAAI;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,IAAI,aAAa,WAAW,IAAI,aAAa,SAAU,QAAO;AAClE,MAAI,OAAO;AACX,MAAI,CAAC,QAAQ,yBAAyB,IAAI,SAAS,SAAS,KAAK,IAAI,SAAS,SAAS,GAAG,GAAG;AAC3F,QAAI,WAAW,IAAI,SAAS,MAAM,GAAG,EAAE;AAAA,EACzC;AACA,SAAO,IAAI,SAAS;AACtB;;;ACxSO,SAAS,QACd,OACA,OACyC;AACzC,QAAM,SAAS,CAAC,CAAC,KAAK,GAAG,MAAwB,SAAS,OAAO,SAAS;AAC1E,MAAI,OAAO,MAAM,KAAK,EAAG,QAAO,EAAE,QAAQ,QAAQ,MAAM,QAAQ;AAChE,MAAI,OAAO,MAAM,UAAU,EAAG,QAAO,EAAE,QAAQ,QAAQ,MAAM,aAAa;AAC1E,SAAO,EAAE,QAAQ,QAAQ,MAAM,OAAO;AACxC;AAGO,SAAS,cACd,YACA,OACkB;AAClB,QAAM,WAA0B,CAAC;AACjC,QAAM,UAAuB,CAAC;AAE9B,aAAW,QAAQ,OAAO;AACxB,QAAI;AACF,UAAI,KAAK,SAAS,WAAW;AAC3B,cAAM,UAAU,KAAK,SAAS,UAAU;AACxC,iBAAS,KAAK;AAAA,UACZ,QAAQ,KAAK;AAAA,UACb,MAAM,KAAK;AAAA,UACX,QAAQ,QAAQ;AAAA,UAChB,UAAU,QAAQ,WAAW,SAAS,KAAK,WAAW;AAAA,UACtD,UAAU,QAAQ;AAAA,UAClB,UAAU,KAAK;AAAA,UACf,SAAS,QAAQ;AAAA,UACjB,QAAQ,QAAQ;AAAA,UAChB,OAAO,KAAK;AAAA,UACZ,SAAS,KAAK;AAAA,QAChB,CAAC;AAAA,MACH,OAAO;AACL,cAAM,UAAU,KAAK,SAAS,UAAU;AACxC,iBAAS,KAAK;AAAA,UACZ,QAAQ,KAAK;AAAA,UACb,MAAM,KAAK;AAAA,UACX,QAAQ,QAAQ;AAAA,UAChB,MAAM,QAAQ;AAAA,UACd,UACE,QAAQ,WAAW,UAAU,QAAQ,QAAQ,QAAQ,SAAS,UAC1D,KAAK,eAAe,QAAQ,IAAI,IAChC;AAAA,UACN,UAAU,QAAQ;AAAA,UAClB,UAAU,KAAK;AAAA,UACf,SAAS,QAAQ;AAAA,UACjB,QAAQ,QAAQ;AAAA,UAChB,OAAO,KAAK;AAAA,UACZ,SAAS,KAAK;AAAA,QAChB,CAAC;AAAA,MACH;AAAA,IACF,SAAS,KAAK;AACZ,cAAQ,KAAK;AAAA,QACX,QAAQ,KAAK;AAAA,QACb,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,MAC1D,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,QAAQ;AAC7B;AASO,SAAS,iBAAiB,QAA0B,OAAqC;AAC9F,QAAM,OAAO,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AACzD,QAAM,SAAkB,CAAC;AAEzB,aAAW,WAAW,OAAO,UAAU;AACrC,QAAI,QAAQ,WAAW,UAAU,QAAQ,WAAW,OAAQ;AAC5D,WAAO,KAAK;AAAA,MACV,QAAQ,QAAQ;AAAA,MAChB,UAAU,QAAQ,YAAY;AAAA,MAC9B,SAAS,QAAQ,WAAW,YAAY,QAAQ,QAAQ;AAAA,MACxD,QAAQ,QAAQ;AAAA,MAChB,KAAK,KAAK,IAAI,QAAQ,MAAM,GAAG;AAAA;AAAA;AAAA;AAAA,MAI/B,OAAO,QAAQ;AAAA,MACf,SAAS,QAAQ;AAAA,MACjB,UAAU,QAAQ;AAAA,MAClB,UAAU,QAAQ;AAAA,IACpB,CAAC;AAAA,EACH;AAEA,aAAW,SAAS,OAAO,SAAS;AAClC,WAAO,KAAK;AAAA,MACV,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS,UAAU,MAAM,MAAM,aAAa,MAAM,OAAO;AAAA,IAC3D,CAAC;AAAA,EACH;AAEA,SAAO;AACT;;;ACnHA,IAAM,cAAgC,CAAC,IAAI,EAAE;AAC7C,IAAM,mBAAqC,CAAC,GAAG,EAAE;AACjD,IAAM,aAA+B,CAAC,IAAI,GAAG;AAC7C,IAAM,kBAAoC,CAAC,IAAI,GAAG;AAGlD,IAAM,cAAgC,CAAC,KAAK,GAAG;AAC/C,IAAM,mBAAqC,CAAC,GAAG,CAAC;AAEhD,SAAS,OAAO,OAAqC;AACnD,MAAI,CAAC,MAAO,QAAO,CAAC;AACpB,SAAO,MACJ,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,EACjC,OAAO,OAAO;AACnB;AAGA,SAAS,kBAAkB,OAAwB;AACjD,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,KAAK;AACzB,WAAO,IAAI,aAAa,WAAW,IAAI,aAAa;AAAA,EACtD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,YAAY,KAAgE;AACnF,QAAM,UAAU,IAAI,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,EAAE,OAAO,OAAO;AACrE,QAAM,WAAW,QAAQ,CAAC;AAC1B,MAAI,CAAC,YAAY,CAAC,eAAe,KAAK,QAAQ,EAAG,QAAO;AACxD,QAAM,SAAS,QAAQ,MAAM,CAAC,EAAE,KAAK,CAAC,MAAM,qBAAqB,KAAK,CAAC,CAAC;AACxE,SAAO,SAAS,EAAE,UAAU,OAAO,IAAI,EAAE,SAAS;AACpD;AAWA,SAAS,WAAW,GAAW,GAAoB;AACjD,QAAM,OAAO,YAAY,CAAC;AAC1B,QAAM,QAAQ,YAAY,CAAC;AAC3B,MAAI,CAAC,QAAQ,CAAC,SAAS,KAAK,aAAa,MAAM,SAAU,QAAO;AAChE,SAAO,CAAC,KAAK,UAAU,CAAC,MAAM,UAAU,KAAK,WAAW,MAAM;AAChE;AAOA,SAAS,gBAAgB,IAA0B;AACjD,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,cAAc,GAAG,MAAM,UAAU;AAC1C,QAAI,WAAW,WAAY;AAC3B,UAAM,QAAQ,YAAY,WAAW,QAAQ;AAC7C,QAAI,CAAC,MAAO;AACZ,SAAK,IAAI,MAAM,SAAS,GAAG,MAAM,QAAQ,IAAI,MAAM,MAAM,KAAK,MAAM,QAAQ;AAAA,EAC9E;AACA,SAAO,CAAC,GAAG,IAAI;AACjB;AAGA,SAAS,cAAc,QAAgB,aAAyC;AAC9E,MAAI,WAAW,EAAG,QAAO;AACzB,MAAI,UAAU,OAAO,SAAS,IAAK,QAAO,QAAQ,MAAM;AACxD,SAAO,QAAQ,MAAM,WAAW,eAAe,iBAAiB;AAClE;AAGA,IAAM,YAAY,oBAAI,IAAI;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGD,IAAM,kBAAkB,oBAAI,IAAI,CAAC,oBAAoB,8BAA8B,CAAC;AAGpF,SAAS,cAAc,IAAkC;AACvD,SAAO,GAAG,MAAM,MAAM,OAAO,CAAC,SAAS,UAAU,IAAI,KAAK,IAAI,KAAK,EAAE,YAAY,CAAC,CAAC;AACrF;AAEA,SAAS,UAAU,OAAuB;AACxC,SAAO,MAAM,KAAK,EAAE,YAAY,EAAE,MAAM,KAAK,EAAE,KAAK,EAAE,KAAK,GAAG;AAChE;AAWA,SAAS,UAAU,MAAc,KAAsB;AACrD,QAAMC,UAAS,CAAC,UAA0B;AACxC,QAAI;AACF,aAAO,IAAI,IAAI,OAAO,wBAAwB,EAAE;AAAA,IAClD,QAAQ;AACN,aAAO,MAAM,MAAM,GAAG,EAAE,CAAC,KAAK;AAAA,IAChC;AAAA,EACF;AACA,SAAOA,QAAO,IAAI,MAAMA,QAAO,GAAG;AACpC;AAQA,SAAS,aAAa,IAAyB;AAC7C,SAAO;AAAA,IACL,GAAG,UAAU,OAAO,SAAS,GAAG,UAAU,OAAO,SAAS,KAAK,GAAG,UAAU,KAAK;AAAA,EACnF;AACF;AAEA,IAAM,eAA4B;AAAA,EAChC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAGF,OAAO;AAAA,EACP,SAAS,CAAC,mBAAmB;AAAA,EAC7B,OAAO,CAAC,gBAAgB;AAAA,EACxB,UAAU;AAAA,EACV,SAAS,CAAC,gBAAgB,kBAAkB;AAAA,EAC5C,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAChB,UAAM,WAAW,GAAG,SAAS,OAAO,OAAO,KAAK;AAChD,QAAI,SAAU,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAChD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,UAAU,YAAY;AAAA,MACtB,SAAS;AAAA,MACT,QAAQ,EAAE,MAAM,QAAQ;AAAA,IAC1B;AAAA,EACF;AACF;AAEA,IAAM,cAA0B;AAAA,EAC9B,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO,4BAA4B,YAAY,CAAC,CAAC,QAAQ,YAAY,CAAC,CAAC;AAAA,EACvE,KACE;AAAA,EAGF,OAAO;AAAA,EACP,SAAS,CAAC,qBAAqB,eAAe;AAAA,EAC9C,OAAO,CAAC,gBAAgB;AAAA,EACxB,OAAO,EAAE,OAAO,aAAa,YAAY,iBAAiB;AAAA,EAC1D,gBAAgB,EAAE,YAAY,WAAW,MAAM,UAAU;AAAA,EACzD,UAAU,GAAG,YAAY,CAAC,CAAC,SAAI,YAAY,CAAC,CAAC;AAAA,EAC7C,SAAS,CAAC,eAAe;AAAA,EACzB,UAAU,CAAC,OAAO;AAChB,UAAM,QAAQ,GAAG,SAAS,OAAO,OAAO,KAAK;AAC7C,QAAI,CAAC,MAAO,QAAO,EAAE,QAAQ,MAAM,UAAU,EAAE;AAC/C,UAAM,EAAE,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAAQ;AAAA,MAC7C,OAAO;AAAA,MACP,YAAY;AAAA,IACd,CAAC;AACD,QAAI,WAAW,OAAQ,QAAO,EAAE,QAAQ,MAAM,UAAU,MAAM,OAAO;AACrE,UAAM,YAAY,MAAM,SAAS,YAAY,CAAC,IAAI,UAAU;AAC5D,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,UAAU,MAAM;AAAA,MAChB,SAAS,YAAY,MAAM,MAAM,sBAAiB,SAAS,uBAAuB,YAAY,CAAC,CAAC,SAAI,YAAY,CAAC,CAAC;AAAA,MAClH,QAAQ,EAAE,MAAM,QAAQ;AAAA,IAC1B;AAAA,EACF;AACF;AAEA,IAAM,qBAAkC;AAAA,EACtC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAEF,OAAO;AAAA,EACP,SAAS,CAAC,uCAAuC,gBAAgB;AAAA,EACjE,OAAO,CAAC,kBAAkB;AAAA,EAC1B,UAAU;AAAA,EACV,SAAS,CAAC,sBAAsB,wBAAwB;AAAA,EACxD,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAChB,UAAM,WAAW,GAAG,KAAK,aAAa,OAAO,KAAK;AAClD,QAAI,SAAU,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAChD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,UAAU,YAAY;AAAA,MACtB,SAAS;AAAA,MACT,QAAQ,EAAE,MAAM,QAAQ,MAAM,cAAc;AAAA,IAC9C;AAAA,EACF;AACF;AAEA,IAAM,oBAAgC;AAAA,EACpC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO,6BAA6B,WAAW,CAAC,CAAC,QAAQ,WAAW,CAAC,CAAC;AAAA,EACtE,KACE;AAAA,EAEF,OAAO;AAAA,EACP,SAAS,CAAC,kBAAkB,sBAAsB;AAAA,EAClD,OAAO,CAAC,kBAAkB;AAAA,EAC1B,OAAO,EAAE,OAAO,YAAY,YAAY,gBAAgB;AAAA,EACxD,gBAAgB,EAAE,YAAY,WAAW,MAAM,UAAU;AAAA,EACzD,UAAU,GAAG,WAAW,CAAC,CAAC,SAAI,WAAW,CAAC,CAAC;AAAA,EAC3C,SAAS,CAAC,qBAAqB;AAAA,EAC/B,UAAU,CAAC,OAAO;AAChB,UAAM,QAAQ,GAAG,KAAK,aAAa,OAAO,KAAK;AAC/C,QAAI,CAAC,MAAO,QAAO,EAAE,QAAQ,MAAM,UAAU,EAAE;AAC/C,UAAM,EAAE,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAAQ;AAAA,MAC7C,OAAO;AAAA,MACP,YAAY;AAAA,IACd,CAAC;AACD,QAAI,WAAW,OAAQ,QAAO,EAAE,QAAQ,MAAM,UAAU,MAAM,OAAO;AACrE,UAAM,YAAY,MAAM,SAAS,WAAW,CAAC,IAAI,UAAU;AAC3D,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,UAAU,MAAM;AAAA,MAChB,SAAS,kBAAkB,MAAM,MAAM,sBAAiB,SAAS,uBAAuB,WAAW,CAAC,CAAC,SAAI,WAAW,CAAC,CAAC;AAAA,MACtH,QAAQ,EAAE,MAAM,QAAQ,MAAM,cAAc;AAAA,IAC9C;AAAA,EACF;AACF;AAEA,IAAM,mBAAgC;AAAA,EACpC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAGF,OAAO;AAAA,EACP,SAAS,CAAC,gBAAgB,yBAAyB;AAAA,EACnD,OAAO,CAAC,gBAAgB;AAAA,EACxB,UAAU;AAAA,EACV,SAAS,CAAC,oBAAoB;AAAA,EAC9B,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAChB,UAAM,WAAW,GAAG,KAAK,WAAW;AACpC,QAAI,SAAU,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAChD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS;AAAA,MACT,QAAQ,EAAE,MAAM,QAAQ,KAAK,YAAY;AAAA,IAC3C;AAAA,EACF;AACF;AAEA,IAAM,oBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAGF,OAAO;AAAA,EACP,SAAS,CAAC,gBAAgB,2BAA2B,YAAY;AAAA,EACjE,OAAO,CAAC,gBAAgB;AAAA,EACxB,UAAU;AAAA,EACV,SAAS,CAAC,mBAAmB;AAAA,EAC7B,UAAU,CAAC,OAAO;AAChB,UAAM,YAAY,GAAG,KAAK;AAC1B,UAAM,MAAM,WAAW,KAAK,KAAK;AACjC,QAAI,CAAC,IAAK,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAChD,QAAI,gBAAgB,KAAK,GAAG,EAAG,QAAO,EAAE,QAAQ,QAAQ,UAAU,IAAI;AACtE,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS,iBAAiB,GAAG,4FAAuF,WAAW,SAAS,GAAG;AAAA,MAC3I,QAAQ,EAAE,MAAM,QAAQ,KAAK,YAAY;AAAA,IAC3C;AAAA,EACF;AACF;AAEA,IAAM,kBAA+B;AAAA,EACnC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAEF,OAAO;AAAA,EACP,SAAS,CAAC,gBAAgB,qCAAqC;AAAA,EAC/D,OAAO,CAAC,eAAe;AAAA,EACvB,UAAU;AAAA,EACV,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAChB,UAAM,WAAW,GAAG,KAAK,UAAU,OAAO,KAAK;AAC/C,QAAI,SAAU,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAChD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SACE;AAAA,MACF,QAAQ,EAAE,MAAM,QAAQ,MAAM,WAAW;AAAA,IAC3C;AAAA,EACF;AACF;AAEA,IAAM,iBAA8B;AAAA,EAClC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAGF,OAAO;AAAA,EACP,SAAS,CAAC,KAAK;AAAA,EACf,OAAO,CAAC,iBAAiB;AAAA,EACzB,UAAU;AAAA,EACV,SAAS,CAAC,iBAAiB,kBAAkB;AAAA,EAC7C,UAAU,CAAC,OAAO;AAChB,UAAM,WAAW,GAAG,UAAU,OAAO,OAAO,KAAK;AACjD,QAAI,SAAU,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAChD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS;AAAA,MACT,QAAQ,EAAE,MAAM,QAAQ,UAAU,WAAW;AAAA,IAC/C;AAAA,EACF;AACF;AAEA,IAAM,uBAAoC;AAAA,EACxC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAEF,OAAO;AAAA,EACP,SAAS,CAAC,OAAO,iBAAiB;AAAA,EAClC,OAAO,CAAC,yBAAyB,mBAAmB,oBAAoB,eAAe;AAAA,EACvF,UAAU;AAAA,EACV,SAAS,CAAC,qBAAqB;AAAA,EAC/B,UAAU,CAAC,OAAO;AAChB,UAAM,WAAW,GAAG,UAAU,aAAa,OAAO,KAAK;AACvD,QAAI,SAAU,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAEhD,QAAI,CAAC,aAAa,EAAE,EAAG,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAC7D,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SACE;AAAA,MACF,QAAQ,EAAE,MAAM,QAAQ,UAAU,iBAAiB;AAAA,IACrD;AAAA,EACF;AACF;AAEA,IAAM,iBAA8B;AAAA,EAClC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAGF,OAAO;AAAA,EACP,SAAS,CAAC,OAAO,iBAAiB;AAAA,EAClC,OAAO,CAAC,kBAAkB;AAAA,EAC1B,UAAU;AAAA,EACV,SAAS,CAAC,kBAAkB;AAAA,EAC5B,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAChB,QAAI,GAAG,UAAU,OAAO,SAAS,GAAG;AAClC,aAAO,EAAE,QAAQ,QAAQ,UAAU,GAAG,UAAU,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI,KAAK,EAAE;AAAA,IACjF;AACA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,UAAU,CAAC;AAAA,MACX,SACE;AAAA,MACF,QAAQ,EAAE,MAAM,QAAQ,UAAU,WAAW;AAAA,IAC/C;AAAA,EACF;AACF;AAEA,IAAM,kBAA+B;AAAA,EACnC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA;AAAA;AAAA;AAAA,EAIV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAIF,OAAO;AAAA,EACP,SAAS,CAAC,OAAO,iBAAiB;AAAA,EAClC,OAAO,CAAC,kBAAkB;AAAA,EAC1B,UAAU;AAAA,EACV,SAAS,CAAC,oBAAoB,oBAAoB;AAAA,EAClD,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAChB,QAAI,GAAG,UAAU,OAAO,WAAW,EAAG,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAE5E,UAAM,eAA2D,CAAC;AAClE,eAAW,SAAS,GAAG,UAAU,QAAQ;AACvC,mBAAa,KAAK,EAAE,UAAU,YAAY,OAAO,MAAM,IAAI,MAAM,KAAK,EAAE,CAAC;AACzE,UAAI,MAAM,WAAW;AACnB,qBAAa,KAAK;AAAA,UAChB,UAAU;AAAA,UACV,OAAO,MAAM,UAAU,MAAM,KAAK;AAAA,QACpC,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,WAAW,aAAa,IAAI,CAAC,MAAM,EAAE,KAAK;AAChD,UAAM,WAAW,aAAa,OAAO,CAAC,MAAM,CAAC,kBAAkB,EAAE,KAAK,CAAC;AACvE,QAAI,SAAS,WAAW,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAE7D,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SAAS,yCAAyC,SAC/C,IAAI,CAAC,MAAM,KAAK,EAAE,QAAQ,UAAU,EAAE,KAAK,IAAI,EAC/C,KAAK,IAAI,CAAC;AAAA,MACb,QAAQ,EAAE,MAAM,QAAQ,UAAU,SAAS,CAAC,EAAG,SAAS;AAAA,IAC1D;AAAA,EACF;AACF;AAEA,IAAM,aAA0B;AAAA,EAC9B,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAKF,OAAO;AAAA,EACP,SAAS,CAAC,KAAK;AAAA,EACf,OAAO,CAAC,kBAAkB;AAAA,EAC1B,UAAU;AAAA,EACV,SAAS,CAAC,kBAAkB;AAAA,EAC5B,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAChB,QAAI,GAAG,UAAU,OAAO,WAAW,EAAG,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAE5E,UAAM,WAAW,GAAG,UAAU,OAAO,IAAI,CAAC,WAAW;AAAA,MACnD,KAAK,MAAM,IAAI;AAAA,MACf,KAAK,MAAM,KAAK,SAAS;AAAA,IAC3B,EAAE;AACF,UAAM,cAAc,SAAS,OAAO,CAAC,UAAU,CAAC,MAAM,KAAK,KAAK,CAAC;AACjE,QAAI,YAAY,WAAW,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAEhE,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SACE,YAAY,WAAW,SAAS,SAC5B,wDACA,GAAG,YAAY,MAAM,OAAO,SAAS,MAAM;AAAA,MACjD,QAAQ,EAAE,MAAM,QAAQ,UAAU,eAAe;AAAA,IACnD;AAAA,EACF;AACF;AAeA,IAAM,oBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAMF,OAAO;AAAA,EACP,SAAS,CAAC,KAAK;AAAA,EACf,OAAO,CAAC,oBAAoB,mBAAmB,gBAAgB;AAAA,EAC/D,UAAU;AAAA,EACV,SAAS,CAAC,gBAAgB,yBAAyB;AAAA,EACnD,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAChB,UAAM,YAAY,GAAG,UAAU,OAAO,OAAO,CAAC,UAAU,MAAM,KAAK,MAAM,KAAK,CAAC;AAC/E,QAAI,UAAU,WAAW,EAAG,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAKlE,UAAM,SAAS,GAAG,UAAU,OAAO,SAAS,GAAG,SAAS,OAAO,SAAS,IAAI,KAAK;AACjF,QAAI,UAAU,GAAI,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAExD,UAAM,WAAW,UAAU,IAAI,CAAC,WAAW;AAAA,MACzC,KAAK,MAAM,IAAI;AAAA,MACf,KAAK,MAAM,KAAK,SAAS;AAAA,IAC3B,EAAE;AACF,UAAM,WAAW,SAAS,OAAO,CAAC,UAAU,MAAM,IAAI,KAAK,MAAM,KAAK;AACtE,QAAI,SAAS,WAAW,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAE7D,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SACE,SAAS,WAAW,SAAS,SACzB,sEACA,GAAG,SAAS,MAAM,OAAO,SAAS,MAAM;AAAA,MAC9C,QAAQ,EAAE,MAAM,QAAQ,UAAU,eAAe;AAAA,IACnD;AAAA,EACF;AACF;AAEA,IAAM,oBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAIF,OAAO;AAAA,EACP,SAAS,CAAC,OAAO,iBAAiB;AAAA,EAClC,OAAO,CAAC,kBAAkB;AAAA,EAC1B,UAAU;AAAA,EACV,SAAS,CAAC,oBAAoB,gBAAgB;AAAA,EAC9C,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAChB,QAAI,GAAG,UAAU,OAAO,WAAW,EAAG,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAE5E,UAAM,WAAW,GAAG,UAAU,OAAO,IAAI,CAAC,WAAW;AAAA,MACnD,KAAK,MAAM,IAAI;AAAA,MACf,OAAO,MAAM,OAAO,SAAS;AAAA,MAC7B,QAAQ,MAAM,QAAQ,SAAS;AAAA,IACjC,EAAE;AACF,UAAM,aAAa,SAAS,OAAO,CAAC,UAAU,MAAM,UAAU,QAAQ,MAAM,WAAW,IAAI;AAC3F,QAAI,WAAW,WAAW,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAE/D,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SAAS,GAAG,WAAW,WAAW,SAAS,SAAS,QAAQ,GAAG,WAAW,MAAM,OAAO,SAAS,MAAM,EAAE,4BACtG,WAAW,WAAW,IAAI,KAAK,GACjC,QAAQ,WAAW,WAAW,IAAI,MAAM,EAAE;AAAA,MAC1C,QAAQ,EAAE,MAAM,QAAQ,UAAU,iBAAiB;AAAA,IACrD;AAAA,EACF;AACF;AAEA,IAAM,eAA2B;AAAA,EAC/B,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAGF,OAAO;AAAA,EACP,SAAS,CAAC,mBAAmB,SAAS;AAAA,EACtC,OAAO,CAAC,kBAAkB;AAAA,EAC1B,OAAO,EAAE,OAAO,aAAa,YAAY,iBAAiB;AAAA,EAC1D,gBAAgB,EAAE,YAAY,QAAQ,MAAM,UAAU;AAAA,EACtD,UAAU,+BAA4B,YAAY,CAAC,CAAC,QAAQ,YAAY,CAAC,CAAC;AAAA,EAC1E,SAAS,CAAC,qBAAqB;AAAA,EAC/B,UAAU,CAAC,OAAO;AAIhB,UAAM,WAAW,GAAG,UAAU,OAAO,KAAK,CAAC,UAAU,MAAM,OAAO,SAAS,MAAM,QAAQ,KAAK;AAI9F,QAAI,CAAC,SAAU,QAAO,EAAE,QAAQ,MAAM,UAAU,EAAE;AAElD,UAAM,QAAQ,KAAK,MAAO,SAAS,MAAO,QAAQ,SAAS,OAAQ,QAAS,GAAG,IAAI;AACnF,UAAM,EAAE,QAAQ,KAAK,IAAI,QAAQ,OAAO,EAAE,OAAO,aAAa,YAAY,iBAAiB,CAAC;AAC5F,QAAI,WAAW,OAAQ,QAAO,EAAE,QAAQ,MAAM,UAAU,MAAM;AAE9D,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV,SAAS,mBAAmB,SAAS,MAAO,KAAK,OAAI,SAAS,OAAQ,KAAK,sBAAiB,KAAK;AAAA,MACjG,QAAQ,EAAE,MAAM,QAAQ,UAAU,iBAAiB;AAAA,IACrD;AAAA,EACF;AACF;AAEA,IAAM,kBAA+B;AAAA,EACnC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAIF,OAAO;AAAA,EACP,SAAS,CAAC,KAAK;AAAA,EACf,OAAO,CAAC,oBAAoB,gBAAgB;AAAA,EAC5C,UAAU;AAAA,EACV,SAAS,CAAC,wBAAwB,kBAAkB;AAAA,EACpD,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAChB,UAAM,UAAU,gBAAgB,EAAE;AAClC,UAAM,WAAW,GAAG,UAAU,QAAQ,OAAO,KAAK,KAAK;AACvD,QAAI,QAAQ,SAAS,KAAK,CAAC,aAAa,EAAE,EAAG,QAAO,EAAE,QAAQ,MAAM,SAAS;AAC7E,QAAI,SAAU,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAEhD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS,sCAAsC,QAAQ,KAAK,IAAI,CAAC;AAAA,MACjE,QAAQ,EAAE,MAAM,QAAQ,UAAU,YAAY;AAAA,IAChD;AAAA,EACF;AACF;AAEA,IAAM,qBAAkC;AAAA,EACtC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAIF,OAAO;AAAA,EACP,SAAS,CAAC,OAAO,iBAAiB;AAAA,EAClC,OAAO,CAAC,oBAAoB,8BAA8B,gBAAgB;AAAA,EAC1E,UAAU;AAAA,EACV,SAAS,CAAC,qBAAqB,kBAAkB;AAAA,EACjD,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAChB,UAAM,UAAU,gBAAgB,EAAE;AAClC,UAAM,MAAM,GAAG,UAAU,QAAQ,OAAO,KAAK;AAC7C,UAAM,WAAW,GAAG,UAAU,iBAAiB,IAAI,CAAC,MAAM,EAAE,MAAM,KAAK,CAAC,EAAE,OAAO,OAAO;AACxF,UAAM,WAAW,EAAE,QAAQ,OAAO,MAAM,YAAY,UAAU,UAAU,QAAQ;AAIhF,QAAI,QAAQ,SAAS,KAAK,CAAC,IAAK,QAAO,EAAE,QAAQ,MAAM,SAAS;AAEhE,UAAM,UAAU,QAAQ;AAAA,MACtB,CAAC,QAAQ,CAAC,WAAW,KAAK,GAAG,KAAK,CAAC,SAAS,KAAK,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAAA,IAC5E;AACA,UAAM,WAAW,SAAS,OAAO,CAAC,MAAM,CAAC,QAAQ,KAAK,CAAC,QAAQ,WAAW,KAAK,CAAC,CAAC,CAAC;AAClF,QAAI,QAAQ,WAAW,KAAK,SAAS,WAAW,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAErF,UAAM,gBAA0B,CAAC;AACjC,QAAI,QAAQ,SAAS,GAAG;AACtB,oBAAc,KAAK,kCAAkC,QAAQ,KAAK,IAAI,CAAC,EAAE;AAAA,IAC3E;AACA,QAAI,SAAS,SAAS,GAAG;AACvB,oBAAc,KAAK,2BAA2B,SAAS,KAAK,IAAI,CAAC,kBAAkB;AAAA,IACrF;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SAAS,oEAAoE,cAAc,KAAK,IAAI,CAAC;AAAA,MACrG,QAAQ,EAAE,MAAM,QAAQ,UAAU,sBAAsB;AAAA,IAC1D;AAAA,EACF;AACF;AAEA,IAAM,eAA4B;AAAA,EAChC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAIF,OAAO;AAAA,EACP,SAAS,CAAC,0BAA0B,cAAc;AAAA,EAClD,OAAO,CAAC,eAAe,gBAAgB;AAAA,EACvC,UAAU;AAAA,EACV,SAAS,CAAC,6BAA6B,yBAAyB;AAAA,EAChE,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAChB,UAAM,OAAO,cAAc,EAAE;AAC7B,UAAM,eAAe,GAAG,MAAM,UAAU,SAAS,CAAC;AAClD,UAAM,WAAW;AAAA,MACf,MAAM,KAAK,IAAI,CAAC,UAAU,EAAE,KAAK,KAAK,KAAK,MAAM,KAAK,KAAK,EAAE;AAAA,MAC7D,UAAU,aAAa,IAAI,CAAC,SAAS,KAAK,GAAG;AAAA,IAC/C;AACA,QAAI,KAAK,SAAS,KAAK,aAAa,SAAS,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAElF,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SACE;AAAA,MAEF,QAAQ,EAAE,MAAM,QAAQ,KAAK,OAAO;AAAA,IACtC;AAAA,EACF;AACF;AAEA,IAAM,yBAAsC;AAAA,EAC1C,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAGF,OAAO;AAAA,EACP,SAAS,CAAC,kBAAkB,wBAAwB;AAAA,EACpD,OAAO,CAAC,eAAe,gBAAgB;AAAA,EACvC,UAAU;AAAA,EACV,SAAS,CAAC,eAAe;AAAA,EACzB,UAAU,CAAC,OAAO;AAChB,UAAM,OAAO,cAAc,EAAE;AAI7B,QAAI,KAAK,WAAW,EAAG,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAE7D,UAAM,QAAQ,KAAK,OAAO,CAAC,SAAS,gBAAgB,IAAI,KAAK,IAAI,KAAK,EAAE,YAAY,CAAC,CAAC;AACtF,UAAM,WAAW,KAAK,IAAI,CAAC,SAAS,KAAK,GAAG;AAC5C,QAAI,MAAM,SAAS,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAExD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SAAS,wBAAwB,SAAS,KAAK,IAAI,CAAC;AAAA,MACpD,QAAQ,EAAE,MAAM,QAAQ,KAAK,mBAAmB;AAAA,IAClD;AAAA,EACF;AACF;AAEA,IAAM,wBAAqC;AAAA,EACzC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAKF,OAAO;AAAA,EACP,SAAS,CAAC,mBAAmB,wBAAwB;AAAA,EACrD,OAAO,CAAC,eAAe,gBAAgB;AAAA,EACvC,UAAU;AAAA,EACV,SAAS,CAAC,eAAe;AAAA,EACzB,UAAU,CAAC,OAAO;AAChB,UAAM,WAAW,GAAG,MAAM;AAI1B,QAAI,CAAC,UAAU,UAAU,CAAC,SAAS,SAAS,SAAS,MAAM,WAAW,GAAG;AACvE,aAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAAA,IACxC;AAEA,UAAM,OAAO,cAAc,EAAE;AAC7B,UAAM,WAAW;AAAA,MACf,MAAM,KAAK,IAAI,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,SAAS,KAAK,EAAE;AAAA,MACzE,UAAU,SAAS,MAAM,IAAI,CAAC,UAAU,EAAE,KAAK,KAAK,KAAK,OAAO,KAAK,SAAS,KAAK,EAAE;AAAA,IACvF;AAEA,QAAI,KAAK,WAAW,GAAG;AACrB,aAAO;AAAA,QACL,QAAQ;AAAA,QACR;AAAA,QACA,SAAS,qBAAqB,SAAS,MAAM,MAAM;AAAA,QACnD,QAAQ,EAAE,MAAM,QAAQ,KAAK,WAAW;AAAA,MAC1C;AAAA,IACF;AAKA,UAAM,gBAA0B,CAAC;AACjC,eAAW,QAAQ,SAAS,OAAO;AACjC,YAAM,OAAO,KAAK,KAAK,CAAC,cAAc,UAAU,UAAU,MAAM,KAAK,GAAG,CAAC;AACzE,UAAI,CAAC,KAAM;AACX,UAAI,KAAK,SAAS,KAAK,SAAS,UAAU,KAAK,KAAK,MAAM,UAAU,KAAK,KAAK,GAAG;AAC/E,sBAAc;AAAA,UACZ,KAAK,KAAK,GAAG,WAAW,KAAK,KAAK,8BAA8B,KAAK,KAAK;AAAA,QAC5E;AAAA,MACF;AACA,UAAI,KAAK,QAAQ,KAAK,QAAQ,KAAK,KAAK,YAAY,MAAM,KAAK,KAAK,YAAY,GAAG;AACjF,sBAAc;AAAA,UACZ,KAAK,KAAK,GAAG,WAAW,KAAK,IAAI,8BAA8B,KAAK,IAAI;AAAA,QAC1E;AAAA,MACF;AAAA,IACF;AAEA,QAAI,cAAc,WAAW,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAClE,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SAAS,uEAAuE,cAAc,KAAK,IAAI,CAAC;AAAA,MACxG,QAAQ,EAAE,MAAM,QAAQ,KAAK,WAAW;AAAA,IAC1C;AAAA,EACF;AACF;AAEA,IAAM,mBAAgC;AAAA,EACpC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAMF,OAAO;AAAA,EACP,SAAS,CAAC,OAAO,iBAAiB;AAAA,EAClC,OAAO,CAAC,oBAAoB,QAAQ;AAAA,EACpC,UAAU;AAAA,EACV,SAAS,CAAC,oBAAoB,mBAAmB;AAAA,EACjD,UAAU,CAAC,OAAO;AAGhB,QAAI,CAAC,GAAG,OAAQ,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAEtD,UAAM,SAAS,GAAG,UAAU,OACzB,IAAI,CAAC,UAAU,GAAG,SAAS,MAAM,IAAI,MAAM,KAAK,CAAC,CAAC,EAClD,OAAO,CAAC,UAA8C,UAAU,MAAS;AAC5E,QAAI,OAAO,WAAW,EAAG,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAE/D,UAAM,WAAW,OAAO,IAAI,CAAC,WAAW;AAAA,MACtC,QAAQ,MAAM;AAAA,MACd,aAAa,MAAM,eAAe;AAAA,IACpC,EAAE;AACF,UAAM,OAAO,OAAO,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE;AAC/C,QAAI,KAAK,WAAW,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAEzD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SAAS,yCAAyC,KAC/C,IAAI,CAAC,UAAU,cAAc,MAAM,QAAQ,MAAM,WAAW,CAAC,EAC7D,KAAK,IAAI,CAAC;AAAA,MACb,QAAQ,EAAE,MAAM,QAAQ,UAAU,WAAW;AAAA,IAC/C;AAAA,EACF;AACF;AAEA,IAAM,uBAAoC;AAAA,EACxC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAWF,OAAO;AAAA,EACP,SAAS,CAAC,OAAO,iBAAiB;AAAA,EAClC,OAAO,CAAC,oBAAoB,QAAQ;AAAA,EACpC,UAAU;AAAA,EACV,SAAS,CAAC,uBAAuB,kBAAkB,oBAAoB;AAAA,EACvE,KAAK;AAAA,IACH,OAAO;AAAA,IACP,SAAS;AAAA,MACP;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX,UAAU;AAAA,EACZ;AAAA,EACA,UAAU,CAAC,OAAO;AAGhB,QAAI,CAAC,GAAG,OAAQ,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAEtD,UAAM,aAAa,GAAG,UAAU,OAC7B,IAAI,CAAC,WAAW,EAAE,OAAO,OAAO,GAAG,SAAS,MAAM,IAAI,MAAM,KAAK,CAAC,EAAE,EAAE,EACtE;AAAA,MACC,CAAC;AAAA;AAAA;AAAA;AAAA,QAIC,QAAQ,KAAK,OAAO,MAAM,KAAK,MAAM,OAAO,MAAM,KAClD,KAAK,MAAM,OAAO,UAAU,UAC5B,KAAK,MAAM,QAAQ,UAAU;AAAA;AAAA,IACjC;AACF,QAAI,WAAW,WAAW,EAAG,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAEnE,UAAM,WAAW,WAAW,IAAI,CAAC,EAAE,OAAO,MAAM,OAAO;AAAA,MACrD,KAAK,MAAM,IAAI;AAAA,MACf,UAAU,GAAG,MAAM,MAAO,KAAK,IAAI,MAAM,OAAQ,KAAK;AAAA;AAAA;AAAA,MAGtD,QAAQ,GAAG,MAAO,MAAO,CAAC,EAAG,KAAK,IAAI,MAAO,MAAO,CAAC,EAAG,MAAM;AAAA,IAChE,EAAE;AAEF,UAAM,QAAQ,SAAS,OAAO,CAAC,UAAU,MAAM,aAAa,MAAM,MAAM;AACxE,QAAI,MAAM,WAAW,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAE1D,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SAAS,yCAAyC,MAC/C,IAAI,CAAC,UAAU,KAAK,MAAM,GAAG,eAAe,MAAM,QAAQ,WAAW,MAAM,MAAM,EAAE,EACnF,KAAK,IAAI,CAAC;AAAA,MACb,QAAQ,EAAE,MAAM,QAAQ,UAAU,iBAAiB;AAAA,IACrD;AAAA,EACF;AACF;AAEA,IAAM,mBAAgC;AAAA,EACpC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAGF,OAAO;AAAA,EACP,SAAS,CAAC,0BAA0B,cAAc;AAAA,EAClD,OAAO,CAAC,eAAe,QAAQ;AAAA,EAC/B,UAAU;AAAA,EACV,SAAS,CAAC,iBAAiB,mBAAmB;AAAA,EAC9C,UAAU,CAAC,OAAO;AAChB,QAAI,CAAC,GAAG,OAAQ,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAEtD,UAAM,SAAS,cAAc,EAAE,EAC5B,IAAI,CAAC,UAAU,EAAE,MAAM,OAAO,GAAG,SAAS,KAAK,IAAI,EAAE,EAAE,EACvD;AAAA,MAAO,CAAC,SACP,QAAQ,KAAK,KAAK;AAAA,IACpB;AACF,QAAI,OAAO,WAAW,EAAG,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAE/D,UAAM,WAAW,OAAO,IAAI,CAAC,EAAE,MAAM,MAAM,OAAO;AAAA,MAChD,KAAK,KAAK;AAAA,MACV,MAAM,KAAK;AAAA,MACX,QAAQ,MAAM;AAAA,IAChB,EAAE;AACF,UAAM,OAAO,OAAO,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE;AACnD,QAAI,KAAK,WAAW,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAEzD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SAAS,0CAA0C,KAChD;AAAA,QACC,CAAC,EAAE,MAAM,MAAM,MACb,KAAK,KAAK,GAAG,eAAU,KAAK,IAAI,OAAO,cAAc,MAAM,QAAQ,MAAM,WAAW,CAAC;AAAA,MACzF,EACC,KAAK,IAAI,CAAC;AAAA,MACb,QAAQ,EAAE,MAAM,QAAQ,KAAK,OAAO;AAAA,IACtC;AAAA,EACF;AACF;AAEA,IAAM,qBAAkC;AAAA,EACtC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAKF,OAAO;AAAA,EACP,SAAS,CAAC,0BAA0B,cAAc;AAAA,EAClD,OAAO,CAAC,eAAe,QAAQ;AAAA,EAC/B,UAAU;AAAA,EACV,SAAS,CAAC,mBAAmB;AAAA,EAC7B,UAAU,CAAC,OAAO;AAChB,QAAI,CAAC,GAAG,OAAQ,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAEtD,UAAM,aAAa,cAAc,EAAE,EAChC,IAAI,CAAC,UAAU,EAAE,MAAM,OAAO,GAAG,SAAS,KAAK,IAAI,EAAE,EAAE,EACvD;AAAA,MACC,CAAC;AAAA;AAAA;AAAA;AAAA,QAIC,QAAQ,KAAK,OAAO,MAAM,KAAK,MAAM,OAAO,MAAM,KAClD,KAAK,KAAK,YAAY,KAAK,CAAC,SAAS,SAAS,KAAK;AAAA;AAAA,IACvD;AACF,QAAI,WAAW,WAAW,EAAG,QAAO,EAAE,QAAQ,MAAM,UAAU,KAAK;AAEnE,UAAM,WAAW,WAAW,IAAI,CAAC,EAAE,MAAM,MAAM,OAAO;AAAA,MACpD,MAAM,KAAK;AAAA,MACX,UAAU,KAAK,SAAS;AAAA,MACxB,SAAS,MAAM,SAAS,CAAC,GAAG,IAAI,CAAC,SAAS,GAAG,KAAK,KAAK,IAAI,KAAK,MAAM,EAAE;AAAA,IAC1E,EAAE;AAEF,UAAM,QAAQ,SAAS,OAAO,CAAC,UAAU;AACvC,YAAM,WAAW,IAAI;AAAA,QACnB,WACG,KAAK,CAAC,SAAS,KAAK,KAAK,SAAS,MAAM,IAAI,EAC5C,KAAK,YAAY,OAAO,CAAC,SAAS,SAAS,KAAK,EAChD,IAAI,CAAC,SAAS,GAAG,KAAK,KAAK,IAAI,KAAK,MAAM,EAAE;AAAA,MACjD;AACA,YAAM,SAAS,IAAI,IAAI,MAAM,MAAM;AAGnC,aAAO,SAAS,SAAS,OAAO,QAAQ,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,IAAI,IAAI,CAAC;AAAA,IACxF,CAAC;AAED,QAAI,MAAM,WAAW,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAC1D,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SAAS,yCAAyC,MAC/C;AAAA,QACC,CAAC,UACC,KAAK,MAAM,IAAI,iBAAiB,MAAM,QAAQ,mBAAmB,MAAM,OAAO,KAAK,IAAI,CAAC;AAAA,MAC5F,EACC,KAAK,IAAI,CAAC;AAAA,MACb,QAAQ,EAAE,MAAM,QAAQ,KAAK,OAAO;AAAA,IACtC;AAAA,EACF;AACF;AAEA,IAAM,iBAA8B;AAAA,EAClC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAGF,OAAO;AAAA,EACP,SAAS,CAAC,oBAAoB;AAAA,EAC9B,OAAO,CAAC,eAAe,kBAAkB,cAAc;AAAA,EACvD,UAAU;AAAA,EACV,UAAU,CAAC,OAAO;AAChB,UAAM,aAA8E;AAAA,MAClF;AAAA,QACE,MAAM;AAAA,QACN,QAAQ,IAAI,IAAI,OAAO,GAAG,KAAK,QAAQ,KAAK,CAAC;AAAA,QAC7C,QAAQ,EAAE,MAAM,QAAQ,MAAM,SAAS;AAAA,MACzC;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,QAAQ,IAAI,IAAI,OAAO,GAAG,KAAK,WAAW,KAAK,CAAC;AAAA,QAChD,QAAQ,EAAE,MAAM,QAAQ,MAAM,YAAY;AAAA,MAC5C;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,QAAQ,IAAI,IAAI,OAAO,GAAG,KAAK,QAAQ,cAAc,CAAC,CAAC;AAAA,QACvD,QAAQ,EAAE,MAAM,UAAU,MAAM,eAAe;AAAA,MACjD;AAAA,IACF;AACA,UAAM,eAAe,WAAW,OAAO,CAAC,MAAM,EAAE,OAAO,OAAO,CAAC;AAE/D,UAAM,WAAW,OAAO,YAAY,aAAa,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AAC3F,QAAI,aAAa,SAAS,EAAG,QAAO,EAAE,QAAQ,MAAM,SAAS;AAE7D,UAAM,YAAsB,CAAC;AAC7B,QAAI;AACJ,eAAW,CAAC,UAAU,QAAQ,KAAK;AAAA,MACjC,CAAC,WAAW,OAAO;AAAA,MACnB,CAAC,YAAY,QAAQ;AAAA,IACvB,GAAY;AACV,YAAM,QAAQ,aAAa,OAAO,CAAC,MAAM,EAAE,OAAO,IAAI,QAAQ,CAAC;AAC/D,YAAM,SAAS,aAAa,OAAO,CAAC,MAAM,EAAE,OAAO,IAAI,QAAQ,KAAK,CAAC,EAAE,OAAO,IAAI,QAAQ,CAAC;AAC3F,UAAI,MAAM,SAAS,KAAK,OAAO,SAAS,GAAG;AACzC,kBAAU;AAAA,UACR,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,UAAU,QAAQ,OAAO,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,UAAU,QAAQ;AAAA,QACvH;AACA,mBAAW,MAAM,CAAC,EAAG;AAAA,MACvB;AAAA,IACF;AAEA,QAAI,UAAU,WAAW,EAAG,QAAO,EAAE,QAAQ,QAAQ,SAAS;AAC9D,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA,SAAS,kCAAkC,UAAU,KAAK,IAAI,CAAC;AAAA,MAC/D;AAAA,IACF;AAAA,EACF;AACF;AAGO,IAAM,QAA6B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,aAAgC,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;;;AChxClE,IAAM,qBAAqB;;;ACGlC,SAAS,SAAS,OAA2B,WAA6C;AACxF,MAAI,UAAU,OAAW,QAAO;AAChC,SAAO,EAAE,OAAO,QAAQ,EAAE,MAAM,QAAQ,UAAU,GAAG,KAAK,MAAM;AAClE;AAEA,SAAS,iBAAiB,MAAgC;AACxD,SAAO;AAAA,IACL,OAAO,KAAK,KAAK;AAAA,IACjB,MAAM,SAAS,KAAK,IAAI,UAAU,MAAM;AAAA,IACxC,KAAK,SAAS,KAAK,IAAI,SAAS,KAAK;AAAA,IACrC,SAAS,KAAK,KAAK;AAAA;AAAA;AAAA,IAGnB,MAAM,SAAS,KAAK,IAAI,UAAU,MAAM;AAAA,EAC1C;AACF;AAEA,SAAS,kBAAkB,MAAiC;AAC1D,QAAM,KAAK,KAAK;AAChB,SAAO;AAAA,IACL,OAAO,GAAG;AAAA,IACV,MAAM,GAAG;AAAA,IACT,KAAK,GAAG;AAAA,IACR,aAAa,GAAG;AAAA,IAChB,UAAU,GAAG;AAAA,IACb,QAAQ,GAAG;AAAA,IACX,kBAAkB,GAAG;AAAA,IACrB,QAAQ,GAAG;AAAA,IACX,OAAO,GAAG;AAAA,EACZ;AACF;AAEA,SAAS,eAAe,OAAoC;AAC1D,SAAO,OAAO,UAAU,YAAY,MAAM,KAAK,IAAI,QAAQ;AAC7D;AASA,SAAS,cAAc,MAAyC;AAC9D,MAAI,OAAO,SAAS,YAAY,SAAS,KAAM,QAAO,CAAC;AACvD,QAAM,MAAO,KAA6B;AAC1C,MAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,QAAO,CAAC;AAEjC,QAAM,QAAkC,CAAC;AACzC,aAAW,SAAS,KAAK;AACvB,QAAI,OAAO,UAAU,YAAY,UAAU,KAAM;AACjD,UAAM,EAAE,KAAK,OAAO,MAAM,QAAQ,IAAI;AACtC,UAAM,SAAS,eAAe,GAAG;AACjC,QAAI,CAAC,OAAQ;AAEb,UAAM,KAAK;AAAA,MACT,KAAK;AAAA,MACL,OAAO,eAAe,KAAK;AAAA,MAC3B,MAAM,eAAe,IAAI;AAAA,MACzB,SAAS,eAAe,OAAO;AAAA,MAC/B,aAAa,WAAW,eAAe,KAAK,CAAC;AAAA,IAC/C,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,MAA4C;AACpE,QAAM,OAAO,KAAK,MAAM;AACxB,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,QAAQ,KAAK,OAAO;AAG1B,MAAI,CAAC,MAAO,QAAO,EAAE,MAAM,KAAK,KAAK;AACrC,MAAI,CAAC,MAAM,SAAS,MAAM,eAAe,QAAW;AAClD,WAAO,EAAE,MAAM,KAAK,MAAM,QAAQ,MAAM;AAAA,EAC1C;AAEA,SAAO,EAAE,MAAM,KAAK,MAAM,QAAQ,MAAM,OAAO,cAAc,MAAM,IAAI,EAAE;AAC3E;AAEA,SAAS,cAAc,MAA6B;AAClD,SAAO;AAAA,IACL,UAAU,KAAK,MAAM;AAAA,IACrB,OAAO,KAAK,MAAM;AAAA,IAClB,UAAU,iBAAiB,IAAI;AAAA,IAC/B,OAAO,KAAK,MAAM,MAAM,IAAI,CAAC,EAAE,MAAM,MAAM,MAAM,OAAO,EAAE,MAAM,MAAM,MAAM,EAAE;AAAA,EAChF;AACF;AAGO,SAAS,mBAAmB,MAAwB;AACzD,SAAO;AAAA,IACL,mBAAmB;AAAA,IACnB,WAAW,KAAK;AAAA,IAChB,MAAM;AAAA,MACJ,cAAc,KAAK,MAAM;AAAA,MACzB,UAAU,KAAK,MAAM;AAAA,MACrB,QAAQ,KAAK,MAAM;AAAA,MACnB,SAAS,KAAK,MAAM;AAAA,MACpB,WAAW,KAAK,MAAM;AAAA,MACtB,aAAa,KAAK,MAAM;AAAA,IAC1B;AAAA,IACA,WAAW;AAAA,MACT,MAAM,KAAK,UAAU;AAAA,MACrB,WAAW,KAAK,UAAU;AAAA,MAC1B,kBAAkB,KAAK,UAAU;AAAA,IACnC;AAAA,IACA,UAAU,iBAAiB,IAAI;AAAA,IAC/B,MAAM;AAAA,MACJ,aAAa,KAAK,KAAK;AAAA,MACvB,UAAU,KAAK,KAAK;AAAA,MACpB,QAAQ,KAAK,KAAK;AAAA,MAClB,WAAW,KAAK,KAAK;AAAA,MACrB,WAAW,KAAK,KAAK;AAAA,MACrB,YAAY,KAAK,KAAK;AAAA,MACtB,aAAa,KAAK,KAAK;AAAA,MACvB,UAAU,KAAK,KAAK;AAAA,MACpB,WAAW,KAAK,KAAK;AAAA,MACrB,iBAAiB,KAAK,KAAK;AAAA,MAC3B,QAAQ,KAAK,KAAK;AAAA,MAClB,UAAU,KAAK,KAAK;AAAA,IACtB;AAAA,IACA,WAAW,kBAAkB,IAAI;AAAA,IACjC,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,IAC7C,SAAS;AAAA,MACP,MAAM,KAAK,QAAQ;AAAA,MACnB,MAAM,KAAK,QAAQ;AAAA,MACnB,SAAS,KAAK,QAAQ;AAAA,MACtB,OAAO,KAAK,QAAQ;AAAA,MACpB,aAAa,KAAK,QAAQ;AAAA,MAC1B,OAAO,KAAK,QAAQ;AAAA,MACpB,UAAU,KAAK,QAAQ;AAAA,IACzB;AAAA,IACA,OAAO,cAAc,IAAI;AAAA,IACzB,GAAI,KAAK,YACL;AAAA,MACE,WAAW;AAAA,QACT,cAAc,KAAK,UAAU;AAAA,QAC7B,iBAAiB,KAAK,UAAU;AAAA,QAChC,eAAe,KAAK,UAAU;AAAA,QAC9B,cAAc,KAAK,UAAU;AAAA,QAC7B,eAAe,KAAK,UAAU;AAAA,QAC9B,cAAc,KAAK,UAAU;AAAA,QAC7B,mBAAmB,KAAK,UAAU;AAAA,MACpC;AAAA,IACF,IACA,CAAC;AAAA,IACL,QAAQ,KAAK,OAAO,IAAI,CAAC,EAAE,OAAO,OAAO,MAAM,YAAY,IAAI,OAAO;AAAA,MACpE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE;AAAA,EACJ;AACF;;;ACrKA,IAAM,gBAA0C;AAAA,EAC9C,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AACR;AAyBO,SAAS,WAA4B,QAAkB;AAC5D,SAAO,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM;AAChC,UAAM,MAAM,cAAc,EAAE,QAAQ,IAAI,cAAc,EAAE,QAAQ;AAChE,QAAI,QAAQ,EAAG,QAAO;AACtB,WAAO,EAAE,OAAO,cAAc,EAAE,MAAM;AAAA,EACxC,CAAC;AACH;;;AC1CA,IAAM,gBAAgB,IAAI,KAAK,aAAa,CAAC,IAAI,GAAG,EAAE,MAAM,YAAY,UAAU,OAAO,CAAC;AAC1F,IAAM,cAAc,IAAI,KAAK,aAAa,CAAC,IAAI,GAAG,EAAE,MAAM,UAAU,UAAU,OAAO,CAAC;AACtF,IAAM,cAAc,IAAI,KAAK,aAAa,CAAC,IAAI,GAAG,EAAE,MAAM,UAAU,UAAU,OAAO,CAAC;AAUtF,IAAM,yBAAyB,oBAAI,IAAI,CAAC,MAAM,MAAM,IAAI,CAAC;AAGzD,IAAM,2BAA2B,oBAAI,IAAI,CAAC,OAAO,OAAO,OAAO,KAAK,CAAC;AAGrE,IAAM,eAAe,oBAAI,IAAI,CAAC,MAAM,CAAC;AAc9B,IAAM,sBACX,cAAc,GAAG,IAAI,MAAM,UAC3B,cAAc,GAAG,IAAI,MAAM,UAC3B,YAAY,GAAG,IAAI,MAAM;AAGpB,IAAM,0BACX;AAKF,IAAM,cAAc;AAGb,SAAS,uBAAuB,SAA0B;AAC/D,SAAO,YAAY,KAAK,OAAO;AACjC;AAWO,SAAS,cAAc,KAAsB;AAClD,MAAI,QAAQ,YAAa,QAAO;AAChC,MAAI,CAAC,oBAAqB,QAAO,YAAY,KAAK,GAAG;AAErD,MAAI;AACJ,MAAI;AACF,gBAAY,KAAK,oBAAoB,GAAG,EAAE,CAAC,KAAK;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,UAAW,QAAO;AAEvB,MAAI;AACJ,MAAI;AACF,YAAQ,IAAI,KAAK,OAAO,SAAS;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AAKA,QAAM,YAAY,MAAM,YAAY,IAAI,YAAY;AACpD,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,yBAAyB,IAAI,QAAQ,EAAG,QAAO;AACnD,MAAI,cAAc,GAAG,QAAQ,MAAM,OAAW,QAAO;AAErD,MAAI,MAAM,QAAQ;AAChB,QAAI,aAAa,IAAI,MAAM,MAAM,EAAG,QAAO;AAC3C,QAAI,YAAY,GAAG,MAAM,MAAM,MAAM,OAAW,QAAO;AAAA,EACzD;AAEA,MAAI,MAAM,QAAQ;AAChB,QAAI,uBAAuB,IAAI,MAAM,MAAM,EAAG,QAAO;AACrD,QAAI,YAAY,GAAG,MAAM,MAAM,MAAM,OAAW,QAAO;AAAA,EACzD;AAEA,SAAO;AACT;AAYO,SAAS,mBAAmB,KAAsB;AACvD,QAAM,WAAW,IAAI,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,KAAK;AAC7D,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,yBAAyB,IAAI,OAAO,EAAG,QAAO;AAClD,MAAI,CAAC,oBAAqB,QAAO,YAAY,KAAK,OAAO;AACzD,SAAO,cAAc,GAAG,OAAO,MAAM;AACvC;;;ACpCO,SAAS,eAAe,KAAqB;AAClD,SAAO,IAAI,KAAK,EAAE,YAAY;AAChC;AA0DO,SAAS,gBAAgB,OAAe,UAAkC,CAAC,GAAe;AAC/F,QAAM,iBAAiB,oBAAI,IAAkB;AAC7C,aAAW,QAAQ,MAAO,gBAAe,IAAI,KAAK,MAAM,UAAU,IAAI;AAGtE,QAAM,OAAO,oBAAI,IAA+B;AAChD,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,SAAS,oBAAI,IAAY;AAG/B,QAAM,cAAc,IAAI,IAAI,QAAQ,gBAAgB,CAAC,CAAC;AAEtD,aAAW,UAAU,QAAQ,WAAW,CAAC,GAAG;AAC1C,UAAM,MAAM,OAAO,KAAK,EAAE,YAAY;AACtC,QAAI,IAAK,SAAQ,IAAI,GAAG;AAAA,EAC1B;AAQA,WAASC,OAAM,KAAa,eAA+B;AACzD,WAAO,QAAQ,iBAAiB,GAAG,KAAK;AAAA,EAC1C;AAUA,WAAS,SAAS,KAA6B;AAC7C,QAAI,eAAe,IAAI,GAAG,EAAG,QAAO;AACpC,WAAO,YAAY,IAAI,GAAG,IAAI,YAAY;AAAA,EAC5C;AAEA,WAAS,OAAO,OAAe,QAAgB,KAAmB;AAChE,WAAO,IAAI,KAAK;AAChB,YAAQ,IAAI,MAAM;AAClB,QAAI,MAAM,KAAK,IAAI,KAAK;AACxB,QAAI,CAAC,KAAK;AACR,YAAM,oBAAI,IAAI;AACd,WAAK,IAAI,OAAO,GAAG;AAAA,IACrB;AACA,QAAI,CAAC,IAAI,IAAI,MAAM,GAAG;AACpB,UAAI,IAAI,QAAQ,EAAE,KAAK,WAAW,eAAe,IAAI,GAAG,GAAG,QAAQ,SAAS,GAAG,EAAE,CAAC;AAAA,IACpF;AAAA,EACF;AAEA,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAM,IAAI,IAAI,KAAK,MAAM,QAAQ;AACvC,UAAM,EAAE,OAAO,WAAW,QAAQ,WAAW,IAAI,WAAW,IAAI,QAAQ;AACxE,WAAOA,OAAM,KAAK,MAAM,UAAU,SAAS,GAAG,YAAY,KAAK,MAAM,QAAQ;AAE7E,eAAW,OAAO,KAAK,MAAM,YAAY;AACvC,UAAI;AACJ,UAAI;AACF,iBAAS,IAAI,IAAI,IAAI,IAAI;AAAA,MAC3B,QAAQ;AACN;AAAA,MACF;AACA,YAAM,EAAE,MAAM,IAAI,WAAW,OAAO,QAAQ;AAC5C,YAAM,SAAS,IAAI,aAAa,cAAc,eAAe,IAAI,QAAQ;AAIzE,aAAOA,OAAM,OAAO,SAAS,GAAG,SAAS,SAAS,GAAG,QAAQ,OAAO,SAAS,CAAC;AAAA,IAChF;AAAA,EACF;AAIA,aAAW,YAAY,QAAQ,gBAAgB,CAAC,GAAG;AACjD,QAAI;AACJ,QAAI;AACF,YAAM,IAAI,IAAI,QAAQ;AAAA,IACxB,QAAQ;AACN;AAAA,IACF;AACA,UAAM,EAAE,OAAO,OAAO,IAAI,WAAW,IAAI,QAAQ;AACjD,WAAOA,OAAM,IAAI,SAAS,GAAG,KAAK,GAAG,QAAQ,IAAI,SAAS,CAAC;AAAA,EAC7D;AAEA,QAAM,gBAAgB,YAAY,CAAC,GAAG,OAAO,CAAC;AAC9C,QAAM,eAAe,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AAClE,QAAM,QAA6B,CAAC;AACpC,aAAW,SAAS,cAAc;AAChC,UAAM,KAAK,IAAI,CAAC;AAChB,eAAW,UAAU,eAAe;AAClC,YAAM,OAAO,KAAK,IAAI,KAAK,GAAG,IAAI,MAAM;AACxC,YAAM,KAAK,EAAE,MAAM,IAAI,OACnB,EAAE,KAAK,KAAK,KAAK,WAAW,KAAK,WAAW,QAAQ,KAAK,OAAO,IAChE,EAAE,KAAK,MAAM,WAAW,MAAM;AAAA,IACpC;AAAA,EACF;AACA,SAAO,EAAE,SAAS,eAAe,QAAQ,cAAc,MAAM;AAC/D;AAmBO,SAAS,0BAA0B,QAA4B;AACpE,MAAIC,SAAQ;AACZ,aAAW,SAAS,OAAO,QAAQ;AACjC,eAAW,UAAU,OAAO,SAAS;AACnC,UAAI,OAAO,MAAM,KAAK,IAAI,MAAM,GAAG,WAAW,YAAa,CAAAA;AAAA,IAC7D;AAAA,EACF;AACA,SAAOA;AACT;AAaO,SAAS,kBAAkB,OAAmC;AACnE,QAAM,SAA6B,CAAC;AACpC,QAAM,QAAQ,oBAAI,IAAkB;AACpC,aAAW,QAAQ,MAAO,OAAM,IAAI,KAAK,MAAM,UAAU,IAAI;AAE7D,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAM,KAAK,MAAM;AACvB,UAAM,oBAAoB,oBAAI,IAAY;AAC1C,eAAW,OAAO,KAAK,MAAM,YAAY;AAKvC,YAAM,MAAM,IAAI,aAAa,cAAc,IAAI;AAC/C,YAAM,SAAS,IAAI,aAAa,cAAc,eAAe,IAAI,QAAQ;AACzE,wBAAkB,IAAI,MAAM;AAC5B,UAAI,CAAC,cAAc,MAAM,GAAG;AAC1B,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN;AAAA,UACA,QAAQ;AAAA,UACR,SAAS,eAAe,GAAG;AAAA,QAC7B,CAAC;AACD;AAAA,MACF;AACA,UAAI;AACJ,UAAI;AACF,iBAAS,IAAI,IAAI,IAAI,IAAI,EAAE,SAAS;AAAA,MACtC,QAAQ;AACN;AAAA,MACF;AAGA,YAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,UAAI,CAAC,KAAM;AACX,YAAM,OAAO,KAAK,MAAM,WAAW,KAAK,CAAC,MAAM;AAC7C,YAAI;AACJ,YAAI;AACF,qBAAW,IAAI,IAAI,EAAE,IAAI,EAAE,SAAS;AAAA,QACtC,QAAQ;AACN,iBAAO;AAAA,QACT;AACA,eAAO,aAAa;AAAA,MACtB,CAAC;AACD,UAAI,CAAC,MAAM;AACT,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN;AAAA,UACA,SAAS;AAAA,UACT;AAAA,UACA,SAAS,KAAK,GAAG,iCAAiC,MAAM;AAAA,QAC1D,CAAC;AAAA,MACH;AAAA,IACF;AAIA,UAAM,oBAAoB,CAAC,GAAG,iBAAiB,EAAE,OAAO,CAAC,MAAM,MAAM,WAAW;AAChF,QAAI,kBAAkB,UAAU,KAAK,CAAC,kBAAkB,IAAI,WAAW,GAAG;AACxE,aAAO,KAAK;AAAA,QACV,MAAM;AAAA,QACN;AAAA,QACA,SAAS,mBAAmB,kBAAkB,MAAM;AAAA,MACtD,CAAC;AAAA,IACH;AAAA,EACF;AAKA,SAAO;AAAA,IACL;AAAA,IACA,CAAC,MAAM,GAAG,EAAE,IAAI,KAAS,EAAE,GAAG,KAAS,EAAE,WAAW,EAAE,KAAS,EAAE,UAAU,EAAE;AAAA,EAC/E;AACF;AAEA,SAAS,SAAY,OAAY,KAA+B;AAC9D,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAW,CAAC;AAClB,aAAW,QAAQ,OAAO;AACxB,UAAM,IAAI,IAAI,IAAI;AAClB,QAAI,KAAK,IAAI,CAAC,EAAG;AACjB,SAAK,IAAI,CAAC;AACV,QAAI,KAAK,IAAI;AAAA,EACf;AACA,SAAO;AACT;AASO,SAAS,WAAW,UAAqD;AAC9E,QAAMC,YAAW,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AACnD,MAAIA,UAAS,SAAS,KAAK,uBAAuBA,UAAS,CAAC,CAAE,GAAG;AAC/D,UAAM,SAAS,eAAeA,UAAS,CAAC,CAAE;AAC1C,UAAM,OAAOA,UAAS,MAAM,CAAC,EAAE,KAAK,GAAG;AACvC,WAAO,EAAE,OAAO,OAAO,IAAI,IAAI,KAAK,KAAK,OAAO;AAAA,EAClD;AACA,SAAO,EAAE,OAAO,YAAY,KAAK,QAAQ,YAAY;AACvD;AAEA,SAAS,YAAY,SAA6B;AAChD,QAAM,WAAW,QAAQ,SAAS,WAAW,IAAI,CAAC,WAAW,IAAI,CAAC;AAClE,QAAM,OAAO,QAAQ,OAAO,CAAC,MAAM,MAAM,WAAW,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AACvF,SAAO,CAAC,GAAG,UAAU,GAAG,IAAI;AAC9B;;;ACjXO,SAAS,aACd,QACA,MACA,YAAY,KACI;AAChB,QAAM,QAAQ,YAAY,QAAQ,SAAS;AAG3C,MAAI,CAAC,MAAO,QAAO,EAAE,SAAS,KAAK;AAEnC,QAAM,QAAQ,MAAM,WAAW,CAAC,GAAG,SAAS;AAE5C,MAAI;AACJ,MAAI,aAAa;AACjB,aAAW,QAAQ,MAAM,OAAO;AAC9B,QAAI,KAAK,QAAQ,WAAW,EAAG;AAC/B,QAAI,CAAC,eAAe,KAAK,SAAS,IAAI,EAAG;AAEzC,UAAM,SAAS,YAAY,KAAK,OAAO;AACvC,QAAI,SAAS,YAAY;AACvB,aAAO;AACP,mBAAa;AACb;AAAA,IACF;AAEA,QAAI,WAAW,cAAc,KAAK,SAAS,QAAS,QAAO;AAAA,EAC7D;AAEA,MAAI,CAAC,KAAM,QAAO,EAAE,SAAS,MAAM,OAAO,MAAM;AAChD,SAAO,EAAE,SAAS,KAAK,SAAS,SAAS,MAAM,MAAM,OAAO,MAAM;AACpE;AAWA,SAAS,YAAY,QAAgC,WAA4C;AAC/F,QAAM,SAAS,UAAU,KAAK,EAAE,YAAY;AAE5C,MAAI;AACJ,aAAW,SAAS,QAAQ;AAC1B,eAAW,SAAS,MAAM,YAAY;AACpC,UAAI,MAAM,UAAU,IAAK;AAGzB,UAAI,CAAC,OAAO,WAAW,MAAM,KAAK,EAAG;AACrC,UAAI,cAAc,UAAa,MAAM,MAAM,SAAS,UAAU,QAAQ;AACpE,oBAAY,MAAM;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAQ,aAAa;AAC3B,QAAM,WAAW,OAAO;AAAA,IAAO,CAAC,UAC9B,MAAM,WAAW,KAAK,CAAC,UAAU,MAAM,UAAU,KAAK;AAAA,EACxD;AACA,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,MAAI,SAAS,WAAW,EAAG,QAAO,SAAS,CAAC;AAE5C,SAAO;AAAA,IACL,YAAY,SAAS,QAAQ,CAAC,UAAU,MAAM,UAAU;AAAA,IACxD,OAAO,SAAS,QAAQ,CAAC,UAAU,MAAM,KAAK;AAAA,EAChD;AACF;AAUA,SAAS,eAAe,SAAiB,MAAuB;AAC9D,QAAM,WAAW,QAAQ,SAAS,GAAG;AACrC,QAAM,OAAO,WAAW,QAAQ,MAAM,GAAG,EAAE,IAAI;AAE/C,QAAM,SAAS,KACZ,MAAM,GAAG,EACT,IAAI,CAAC,YAAY,aAAa,gBAAgB,OAAO,CAAC,CAAC,EACvD,KAAK,IAAI;AAEZ,SAAO,IAAI,OAAO,IAAI,MAAM,GAAG,WAAW,MAAM,EAAE,EAAE,EAAE,KAAK,gBAAgB,IAAI,CAAC;AAClF;AASA,SAAS,gBAAgB,OAAuB;AAC9C,MAAI;AACF,WAAO,mBAAmB,KAAK;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,YAAY,SAAyB;AAC5C,SAAO,OAAO,WAAW,gBAAgB,OAAO,GAAG,MAAM;AAC3D;AAEA,SAAS,aAAa,OAAuB;AAC3C,SAAO,MAAM,QAAQ,uBAAuB,MAAM;AACpD;;;ACjIA,SAAS,eAAe,MAA4B;AAClD,SAAO,KAAK,WAAW;AACzB;AAGA,SAAS,gBAAgB,MAAyB;AAChD,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,OAAO,KAAK,MAAM,YAAY;AACvC,QAAI,IAAI,WAAY;AACpB,UAAM,MAAM,IAAI,UAAU,KAAK,EAAE,YAAY;AAC7C,QAAI,IAAK,KAAI,IAAI,GAAG;AAAA,EACtB;AACA,SAAO;AACT;AAgBA,SAAS,MAAM,MAAmB,KAAa,UAA0B;AACvE,SAAO,KAAK,iBAAiB,GAAG,KAAK,WAAW,QAAQ,EAAE;AAC5D;AAMA,SAAS,sBAAsB,MAA6C;AAC1E,QAAM,UAAU,oBAAI,IAAyB;AAC7C,QAAM,OAAO,IAAI,IAAI,KAAK,WAAW,QAAQ,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AACxE,aAAW,SAAS,KAAK,WAAW,QAAQ,CAAC,GAAG;AAC9C,QAAI;AACJ,QAAI;AACF,iBAAW,IAAI,IAAI,MAAM,GAAG,EAAE;AAAA,IAChC,QAAQ;AACN;AAAA,IACF;AACA,UAAM,EAAE,OAAO,IAAI,WAAW,QAAQ;AACtC,QAAI,WAAW,YAAa;AAO5B,QAAI,CAAC,KAAK,IAAI,OAAO,YAAY,CAAC,EAAG;AACrC,UAAM,QAAQ,MAAM,MAAM,MAAM,KAAK,QAAQ;AAC7C,UAAM,MAAM,QAAQ,IAAI,KAAK,KAAK,oBAAI,IAAY;AAClD,QAAI,IAAI,OAAO,YAAY,CAAC;AAC5B,YAAQ,IAAI,OAAO,GAAG;AAAA,EACxB;AACA,SAAO;AACT;AAEA,SAAS,OAAO,KAA4B;AAC1C,SAAO,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AACnD;AAmCA,IAAM,kBAA4B;AAAA,EAChC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,mBAAmB,wBAAwB;AAAA,EACrD,WAAW;AAAA,EACX,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,UAAU,KAAK,WAAW,QAAQ,KAAK,IAAI;AACjD,WAAO,KAAK,MACT,OAAO,CAAC,SAAS,KAAK,MAAM,WAAW,WAAW,CAAC,EACnD;AAAA,MAAI,CAAC,SACJ,MAAM;AAAA,QACJ,SAAS,KAAK,MAAM;AAAA,QACpB,SACE,iEACG,KAAK,WAAW,QAAQ,MAAM,aAAa,OAAO,aAClD,KAAK,WAAW,MAAM;AAAA,QAE3B,QAAQ,EAAE,MAAM,QAAQ,KAAK,YAAY;AAAA,QACzC,KAAK;AAAA,UACH,OAAO;AAAA,UACP,SAAS;AAAA,YACP;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,GAAG,KAAK,WAAW,QAAQ;AAAA,cACzB,CAAC,MAAM,YAAY,CAAC,oBAAoB,CAAC;AAAA,YAC3C;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF,EAAE,KAAK,IAAI;AAAA,UACX,UAAU;AAAA,QACZ;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACJ;AACF;AAqBA,SAAS,aAAa,MAAkC;AACtD,QAAM,YAAY,sBAAsB,IAAI;AAC5C,QAAM,OAAsB,CAAC;AAE7B,aAAW,QAAQ,KAAK,OAAO;AAC7B,UAAM,OAAO,gBAAgB,IAAI;AACjC,QAAI,KAAK,SAAS,EAAG;AAErB,QAAI;AACJ,QAAI;AACF,iBAAW,IAAI,IAAI,KAAK,MAAM,QAAQ,EAAE;AAAA,IAC1C,QAAQ;AACN;AAAA,IACF;AACA,UAAM,QAAQ,MAAM,MAAM,KAAK,MAAM,UAAU,QAAQ;AACvD,UAAM,YAAY,UAAU,IAAI,KAAK;AACrC,QAAI,CAAC,aAAa,UAAU,SAAS,EAAG;AAExC,SAAK,KAAK;AAAA,MACR;AAAA,MACA;AAAA,MACA,eAAe,OAAO,IAAI,IAAI,CAAC,GAAG,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC;AAAA,IAC3E,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAGA,IAAM,iBAAiB;AAAA,EACrB,OAAO;AAAA,EACP,SAAS;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AAAA,EACX,UAAU;AACZ;AAEA,IAAM,YAAY,CAAC,SACjB,eAAe,IAAI,MAAM,KAAK,WAAW,KAAK,UAAU,KAAK;AAuC/D,IAAM,4BAAsC;AAAA,EAC1C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,iBAAiB;AAAA,EAC3B,WAAW;AAAA,EACX,OAAO,CAAC,EAAE,MAAM,MAAM,MACpB,aAAa,IAAI,EACd,OAAO,CAAC,QAAQ,IAAI,cAAc,SAAS,CAAC,EAC5C;AAAA,IAAI,CAAC,EAAE,MAAM,OAAO,cAAc,MACjC,MAAM;AAAA,MACJ,SAAS,KAAK,MAAM;AAAA,MACpB,SACE,WAAW,KAAK,yBAAyB,cAAc,KAAK,IAAI,CAAC,0CAChC,cAAc,SAAS,IAAI,SAAS,IAAI,wBACnD,cAAc,SAAS,IAAI,mBAAmB,cAAc,eACpE,cAAc,SAAS,IAAI,SAAS,IAAI,4BACnD,cAAc,SAAS,IAAI,iBAAiB,aAAa;AAAA,MAE9D,QAAQ,EAAE,MAAM,QAAQ,KAAK,YAAY;AAAA,MACzC,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AACN;AAgBA,SAAS,iBAAiB,MAAyB;AACjD,QAAM,MAAM,KAAK,KAAK,QAAQ,SAAS;AACvC,SAAO,IAAI;AAAA,IACT,IACG,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,EACjC,OAAO,OAAO;AAAA,EACnB;AACF;AAsBA,IAAM,mBAA6B;AAAA,EACjC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,gBAAgB,qBAAqB;AAAA,EAC/C,WAAW,CAAC,SACV,KAAK,QAAQ,UAAU,QAAQ,CAAC,aAAa,KAAK,OAAO,QAAQ,GAAG,EAAE;AAAA,EACxE,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,YAAY,KAAK,QAAQ,OAAO,GAAG,KAAK,MAAM;AAKpD,UAAM,SAAS,KAAK,MAAM,OAAO,CAAC,SAAS,iBAAiB,IAAI,EAAE,IAAI,OAAO,CAAC;AAE9E,UAAM,SAAS,OAAO,SAClB,OAAO,OAAO,MAAM,gBAAgB,OAAO,WAAW,IAAI,KAAK,GAAG,4JAGlE;AAIJ,WAAO,MAAM;AAAA,MACX,SAAS;AAAA,MACT,UAAU,OAAO,SAAS,IAAI,UAAU;AAAA,MACxC,SAAS,kEAAkE,MAAM;AAAA,MACjF,QAAQ,EAAE,MAAM,WAAW;AAAA,MAC3B,KAAK;AAAA,QACH,OAAO;AAAA,QACP,SAAS;AAAA,UACP;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF,EAAE,KAAK,IAAI;AAAA,QACX,UAAU;AAAA,MACZ;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAcA,IAAM,kBAA4B;AAAA,EAChC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,0BAA0B,cAAc;AAAA;AAAA;AAAA;AAAA,EAIlD,WAAW,CAAC,SAAS,KAAK,YAAY;AAAA,EACtC,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,QAAQ,KAAK;AACnB,QAAI,CAAC,SAAS,MAAM,MAAO,QAAO,CAAC;AAInC,UAAM,SACJ,MAAM,WAAW,IACb,gCACA,MAAM,UAAU,MACd,uBAAuB,MAAM,MAAM,KACnC,uBAAuB,MAAM,MAAM,WAAW,MAAM,eAAe,iBAAiB;AAE5F,WAAO,MAAM;AAAA,MACX,SAAS,MAAM;AAAA,MACf,SAAS,oCAAoC,MAAM;AAAA,MACnD,QAAQ,EAAE,MAAM,WAAW;AAAA,MAC3B,KAAK;AAAA,QACH,OAAO;AAAA,QACP,SAAS;AAAA,UACP;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF,EAAE,KAAK,IAAI;AAAA,QACX,UAAU;AAAA,MACZ;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAeA,IAAM,oBAAoB,MAAM;AAEhC,IAAM,uBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,cAAc;AAAA;AAAA;AAAA;AAAA,EAIxB,WAAW,CAAC,SAAS,KAAK,WAAW,UAAa,gBAAgB,KAAK,OAAO,MAAM;AAAA,EACpF,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,QAAQ,KAAK;AACnB,UAAM,SACJ,MAAM,WAAW,IAAI,uBAAuB,uBAAuB,MAAM,MAAM;AAEjF,WAAO,MAAM;AAAA,MACX,SAAS,MAAM;AAAA,MACf,SAAS,qCAAqC,MAAM;AAAA,MACpD,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,qBAA+B;AAAA,EACnC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,cAAc;AAAA,EACxB,WAAW,CAAC,UAAU,KAAK,QAAQ,cAAc,KAAK;AAAA,EACtD,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,QAAQ,KAAK;AACnB,UAAM,MAAM,KAAK,MAAM,MAAM,aAAa,IAAI;AAE9C,WAAO,MAAM;AAAA,MACX,SAAS,MAAM;AAAA,MACf,SAAS,qBAAqB,GAAG;AAAA,MACjC,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,uBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,cAAc;AAAA,EACxB,WAAW,CAAC,UAAU,KAAK,QAAQ,aAAa,UAAU,KAAK;AAAA,EAC/D,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,QAAQ,KAAK;AAGnB,UAAM,QAAQ,MAAM,aAAa,MAAM,GAAG,CAAC;AAC3C,UAAM,OAAO,MAAM,aAAa,SAAS,MAAM;AAE/C,WAAO,MAAM;AAAA,MACX,SAAS,MAAM;AAAA,MACf,SACE,sBAAsB,MAAM,aAAa,MAAM,QAAQ,MAAM,aAAa,WAAW,IAAI,KAAK,GAAG,6BACjG,MAAM,IAAI,CAAC,MAAM,QAAQ,EAAE,IAAI,KAAK,EAAE,MAAM,GAAG,EAAE,KAAK,IAAI,IAC1D,GAAG,OAAO,IAAI,SAAS,IAAI,UAAU,EAAE;AAAA,MACzC,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,4BAAsC;AAAA,EAC1C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,gBAAgB,qBAAqB;AAAA,EAC/C,WAAW,CAAC,UAAU,KAAK,QAAQ,kBAAkB,UAAU,KAAK;AAAA,EACpE,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,QAAQ,KAAK;AACnB,UAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,MAAM,kBAAkB,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAKrE,WAAO,MAAM;AAAA,MACX,SAAS,MAAM;AAAA,MACf,SAAS,uBAAuB,MAAM,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,0DAA0D,MAAM,WAAW,IAAI,OAAO,MAAM,uBAAuB,MAAM,WAAW,IAAI,OAAO,MAAM;AAAA,MAC5N,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,uBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,cAAc;AAAA,EACxB,WAAW,CAAC,SAAS,KAAK,QAAQ,UAAU,gBAAgB;AAAA,EAC5D,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,QAAQ,KAAK;AAEnB,WAAO,MAAM;AAAA,MACX,SAAS,MAAM;AAAA,MACf,SAAS,iCAAiC,MAAM,UAAU,QAAQ;AAAA,MAClE,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,2BAAqC;AAAA,EACzC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,qBAAqB,cAAc;AAAA,EAC7C,WAAW,CAAC,UAAU,KAAK,QAAQ,YAAY,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC;AAAA,EACnF,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,QAAQ,KAAK;AACnB,UAAM,WAAW,MAAM,SAAS,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC;AAElE,WAAO,MAAM;AAAA,MACX,SAAS,MAAM;AAAA,MACf,SAAS,oCAAoC,SAAS,IAAI,CAAC,MAAM,QAAQ,EAAE,IAAI,eAAe,EAAE,KAAK,IAAI,EAAE,KAAK,IAAI,CAAC;AAAA,MACrH,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAcA,IAAM,mBAA6B;AAAA,EACjC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,gBAAgB,qBAAqB;AAAA,EAC/C,WAAW,CAAC,SAAS,KAAK,QAAQ,UAAU,QAAQ,aAAa,KAAK,OAAO,QAAQ,GAAG,EAAE;AAAA,EAC1F,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,QAAQ,KAAK;AAEnB,WAAO,KAAK,MAAM,QAAQ,CAAC,SAAS;AAClC,UAAI,CAAC,iBAAiB,IAAI,EAAE,IAAI,OAAO,EAAG,QAAO,CAAC;AAElD,UAAI;AACJ,UAAI;AACF,cAAM,MAAM,IAAI,IAAI,KAAK,MAAM,QAAQ;AACvC,eAAO,GAAG,IAAI,QAAQ,GAAG,IAAI,MAAM;AAAA,MACrC,QAAQ;AACN,eAAO,CAAC;AAAA,MACV;AAEA,YAAM,WAAW,aAAa,MAAM,QAAQ,IAAI;AAChD,UAAI,SAAS,WAAW,CAAC,SAAS,KAAM,QAAO,CAAC;AAEhD,aAAO,MAAM;AAAA,QACX,SAAS,KAAK,MAAM;AAAA,QACpB,SAAS,kFAAkF,SAAS,KAAK,IAAI,gBAAgB,SAAS,KAAK,OAAO,YAAY,SAAS,KAAK;AAAA,QAC5K,QAAQ,EAAE,MAAM,WAAW;AAAA,MAC7B,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF;AAqBA,IAAM,aAAa,oBAAI,IAAI,CAAC,UAAU,UAAU,SAAS,UAAU,WAAW,UAAU,OAAO,CAAC;AAGhG,IAAM,SAAS;AAGf,SAAS,OAAO,MAAwB;AACtC,QAAM,QAAQ,KAAK,MAAM,GAAG,MAAM;AAClC,QAAM,OAAO,KAAK,SAAS,MAAM;AACjC,SAAO,GAAG,MAAM,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,GAAG,OAAO,IAAI,SAAS,IAAI,UAAU,EAAE;AAC1F;AAEA,IAAM,iBAA2B;AAAA,EAC/B,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,qBAAqB,iBAAiB;AAAA;AAAA;AAAA;AAAA,EAIhD,WAAW,CAAC,SAAS,KAAK,cAAc,UAAa,CAAC,KAAK,UAAU,YAAY;AAAA,EACjF,OAAO,CAAC,EAAE,MAAM,MAAM,MACpB,MAAM;AAAA,IACJ,SAAS,GAAG,KAAK,MAAM;AAAA,IACvB,SACE;AAAA,IAEF,QAAQ,EAAE,MAAM,WAAW;AAAA,EAC7B,CAAC;AACL;AAEA,IAAM,oBAA8B;AAAA,EAClC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,mBAAmB;AAAA,EAC7B,WAAW,CAAC,SACV,KAAK,WAAW,YAAY,UAAU,QAAQ,CAAC,KAAK,UAAU,YAAY;AAAA,EAC5E,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,cAAc,KAAK,UAAW;AACpC,WAAO,MAAM;AAAA,MACX,SAAS,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjD,SACE;AAAA,MAEF,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,eAAyB;AAAA,EAC7B,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,mBAAmB;AAAA;AAAA;AAAA,EAG7B,WAAW,CAAC,SACV,KAAK,WAAW,YAAY,eAAe,QAC3C,KAAK,UAAU,KAAK,WAAW,KAC/B,KAAK,MAAM,SAAS;AAAA,EACtB,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,cAAc,KAAK,UAAW;AACpC,WAAO,MAAM;AAAA,MACX,SAAS,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjD,SAAS,+DAA+D,KAAK,MAAM,MAAM;AAAA,MACzF,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,yBAAmC;AAAA,EACvC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,mBAAmB;AAAA,EAC7B,WAAW,CAAC,UAAU,KAAK,WAAW,YAAY,sBAAsB,KAAK;AAAA,EAC7E,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,cAAc,KAAK,UAAW;AACpC,UAAM,QAAQ,YAAY;AAE1B,WAAO,MAAM;AAAA,MACX,SAAS,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjD,SAAS,GAAG,YAAY,kBAAkB,OAAO,KAAK;AAAA,MACtD,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,yBAAmC;AAAA,EACvC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,mBAAmB;AAAA,EAC7B,WAAW,CAAC,UAAU,KAAK,WAAW,QAAQ,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,WAAW,MAAM,GAAG,CAAC;AAAA,EACxF,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,MAAM,KAAK,UAAW,KAAK,OAAO,CAAC,UAAU,CAAC,WAAW,MAAM,GAAG,CAAC;AACzE,WAAO,MAAM;AAAA,MACX,SAAS,KAAK,UAAW,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjE,SAAS,GAAG,IAAI,MAAM,mBAAmB,IAAI,WAAW,IAAI,QAAQ,OAAO,yBAAyB,OAAO,IAAI,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAAA,MACjI,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAiBA,IAAM,qBAAqB;AAC3B,IAAM,sBAAsB;AAG5B,SAAS,cAAc,KAA8B;AACnD,SAAO,IAAI,SAAS,UAAU,IAAI,UAAU,SAAS,IAAI;AAC3D;AAEA,SAAS,mBAAmB,MAAsC;AAChE,UAAQ,KAAK,WAAW,aAAa,CAAC,GAAG;AAAA,IACvC,CAAC,QACC,IAAI,SAAS,iBACZ,cAAc,GAAG,IAAI,sBAAsB,IAAI,aAAa;AAAA,EACjE;AACF;AAcA,IAAM,wBAAkC;AAAA,EACtC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,mBAAmB;AAAA,EAC7B,WAAW,CAAC,SAAS,mBAAmB,IAAI,EAAE,SAAS;AAAA,EACvD,OAAO,CAAC,EAAE,MAAM,MAAM,MACpB,mBAAmB,IAAI,EAAE,IAAI,CAAC,QAAQ;AACpC,UAAMC,SAAQ,cAAc,GAAG;AAC/B,UAAM,OAAO,IAAI,SAAS,UAAU,mBAAmB;AACvD,UAAM,OAAiB,CAAC;AACxB,QAAIA,SAAQ,oBAAoB;AAC9B,WAAK,KAAK,GAAGA,OAAM,eAAe,OAAO,CAAC,IAAI,IAAI,8BAA8B;AAAA,IAClF;AACA,QAAI,IAAI,aAAa,qBAAqB;AACxC,WAAK;AAAA,QACH,IAAI,IAAI,aAAa,SAAW,QAAQ,CAAC,CAAC;AAAA,MAC5C;AAAA,IACF;AAEA,WAAO,MAAM;AAAA,MACX,SAAS,IAAI;AAAA,MACb,SAAS,kCAAkC,KAAK,KAAK,OAAO,CAAC;AAAA,MAC7D,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH,CAAC;AACL;AAsBA,SAAS,kBAAkB,MAAoD;AAC7E,QAAM,MAAuC,CAAC;AAE9C,aAAW,SAAS,KAAK,WAAW,QAAQ,CAAC,GAAG;AAC9C,QAAI,CAAC,MAAM,YAAa;AAExB,QAAI;AACJ,QAAI;AACJ,QAAI;AACF,cAAQ,IAAI,IAAI,MAAM,WAAW;AACjC,YAAM,IAAI,IAAI,MAAM,GAAG;AAAA,IACzB,QAAQ;AACN;AAAA,IACF;AAIA,UAAM,SAAS,MAAM,SAAS,QAAQ,UAAU,EAAE;AAClD,QAAI,WAAW,IAAK;AACpB,QAAI,IAAI,WAAW,MAAM,OAAQ;AACjC,QAAI,IAAI,SAAS,WAAW,MAAM,EAAG;AAErC,QAAI,KAAK,EAAE,KAAK,MAAM,KAAK,MAAM,MAAM,YAAY,CAAC;AAAA,EACtD;AAEA,SAAO;AACT;AAEA,IAAM,yBAAmC;AAAA,EACvC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,mBAAmB;AAAA,EAC7B,WAAW,CAAC,SAAS,kBAAkB,IAAI,EAAE,SAAS;AAAA,EACtD,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,MAAM,kBAAkB,IAAI;AAIlC,UAAM,aAAa,oBAAI,IAAsB;AAC7C,eAAW,EAAE,KAAK,KAAK,KAAK,KAAK;AAC/B,iBAAW,IAAI,MAAM,CAAC,GAAI,WAAW,IAAI,IAAI,KAAK,CAAC,GAAI,GAAG,CAAC;AAAA,IAC7D;AAEA,WAAO,CAAC,GAAG,UAAU,EAAE;AAAA,MAAI,CAAC,CAAC,MAAM,IAAI,MACrC,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,SAAS,GAAG,KAAK,MAAM,QAAQ,KAAK,WAAW,IAAI,SAAS,SAAS,uCAAuC,IAAI,IAAI,IAAI,EAAE,SAAS,QAAQ,UAAU,EAAE,CAAC,OAAO,OAAO,IAAI,CAAC,kFAAkF,KAAK,WAAW,IAAI,OAAO,MAAM;AAAA,QAC9R,QAAQ,EAAE,MAAM,WAAW;AAAA,MAC7B,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEA,IAAM,wBAAkC;AAAA,EACtC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA,EAK7B,WAAW,CAAC,SAAS,iBAAiB,IAAI,EAAE,SAAS;AAAA,EACrD,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,MAAM,iBAAiB,IAAI;AACjC,WAAO,MAAM;AAAA,MACX,SAAS,KAAK,UAAW,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjE,SAAS,GAAG,IAAI,MAAM,QAAQ,IAAI,WAAW,IAAI,mBAAmB,gBAAgB,+BAA+B,OAAO,GAAG,CAAC;AAAA,MAC9H,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,+BAAyC;AAAA,EAC7C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,mBAAmB;AAAA,EAC7B,WAAW,CAAC,SAAS,YAAY,IAAI,EAAE,OAAO;AAAA,EAC9C,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,YAAY,CAAC,GAAG,YAAY,IAAI,CAAC,EAAE,KAAK;AAC9C,WAAO,MAAM;AAAA,MACX,SAAS,KAAK,UAAW,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjE,SAAS,0BAA0B,UAAU,KAAK,OAAO,CAAC;AAAA,MAC1D,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,wBAAkC;AAAA,EACtC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,qBAAqB,iBAAiB;AAAA,EAChD,WAAW,CAAC,SAAS,YAAY,IAAI,EAAE,SAAS;AAAA,EAChD,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,MAAM,YAAY,IAAI;AAC5B,UAAM,YAAY,IAAI,OAAO,CAAC,MAAM,EAAE,WAAW,WAAW;AAC5D,UAAM,SAAS,IAAI,OAAO,CAAC,MAAM,EAAE,WAAW,QAAQ;AAEtD,UAAM,QAAkB,CAAC;AACzB,QAAI,UAAU,SAAS,GAAG;AACxB,YAAM;AAAA,QACJ,GAAG,UAAU,MAAM,wBAAwB,OAAO,UAAU,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAAA,MAClF;AAAA,IACF;AACA,QAAI,OAAO,SAAS,GAAG;AACrB,YAAM,KAAK,GAAG,OAAO,MAAM,yBAAyB,OAAO,OAAO,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,GAAG;AAAA,IAC3F;AAEA,WAAO,MAAM;AAAA,MACX,SAAS,KAAK,UAAW,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjE,SAAS,+CAA+C,MAAM,KAAK,IAAI,CAAC;AAAA,MACxE,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,sBAAgC;AAAA,EACpC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,qBAAqB,iBAAiB;AAAA,EAChD,WAAW,CAAC,SAAS,UAAU,IAAI,EAAE,SAAS;AAAA,EAC9C,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,MAAM,UAAU,IAAI;AAC1B,WAAO,MAAM;AAAA,MACX,SAAS,KAAK,UAAW,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjE,SAAS,GAAG,IAAI,MAAM,QAAQ,IAAI,WAAW,IAAI,sBAAsB,kBAAkB,mCAAmC,OAAO,GAAG,CAAC;AAAA,MACvI,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,SAAS,YAAY,MAAuC;AAC1D,QAAM,MAAM,KAAK,WAAW,YAAY;AACxC,MAAI;AACF,WAAO,MAAM,IAAI,IAAI,GAAG,EAAE,OAAO,IAAI,IAAI,KAAK,MAAM,EAAE;AAAA,EACxD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,iBAAiB,MAA6B;AACrD,QAAM,OAAO,YAAY,IAAI;AAC7B,MAAI,CAAC,KAAM,QAAO,CAAC;AAEnB,UAAQ,KAAK,WAAW,QAAQ,CAAC,GAC9B,OAAO,CAAC,UAAU;AACjB,QAAI,CAAC,WAAW,MAAM,GAAG,EAAG,QAAO;AACnC,QAAI;AACF,aAAO,IAAI,IAAI,MAAM,GAAG,EAAE,SAAS;AAAA,IACrC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,CAAC,EACA,IAAI,CAAC,UAAU,MAAM,GAAG;AAC7B;AAEA,SAAS,YAAY,MAAgC;AACnD,QAAM,YAAY,oBAAI,IAAY;AAClC,aAAW,SAAS,KAAK,WAAW,QAAQ,CAAC,GAAG;AAC9C,QAAI;AACF,gBAAU,IAAI,IAAI,IAAI,MAAM,GAAG,EAAE,SAAS,QAAQ,KAAK,EAAE,CAAC;AAAA,IAC5D,QAAQ;AACN;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAOA,IAAM,eAAe;AAErB,SAAS,YAAY,MAA6E;AAChG,QAAM,MAAgE,CAAC;AAIvE,QAAM,WAAW,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK;AAE7C,aAAW,SAAS,KAAK,WAAW,QAAQ,CAAC,GAAG;AAC9C,UAAM,QAAQ,MAAM,SAAS,KAAK;AAClC,QAAI,CAAC,MAAO;AAEZ,QAAI,CAAC,aAAa,KAAK,KAAK,GAAG;AAC7B,UAAI,KAAK,EAAE,OAAO,QAAQ,YAAY,CAAC;AACvC;AAAA,IACF;AACA,UAAM,SAAS,KAAK,MAAM,KAAK;AAC/B,QAAI,OAAO,SAAS,MAAM,KAAK,SAAS,SAAU,KAAI,KAAK,EAAE,OAAO,QAAQ,SAAS,CAAC;AAAA,EACxF;AACA,SAAO;AACT;AAEA,SAAS,UAAU,MAA6B;AAC9C,QAAM,MAAgB,CAAC;AACvB,aAAW,SAAS,KAAK,WAAW,QAAQ,CAAC,GAAG;AAC9C,UAAM,aAAa,MAAM,YAAY,KAAK,EAAE,YAAY;AACxD,QAAI,cAAc,CAAC,WAAW,IAAI,UAAU,GAAG;AAC7C,UAAI,KAAK,GAAG,MAAM,GAAG,wBAAmB,MAAM,UAAU,IAAI;AAC5D;AAAA,IACF;AACA,UAAM,WAAW,MAAM,UAAU,KAAK;AACtC,QAAI,aAAa,OAAW;AAC5B,UAAM,QAAQ,OAAO,QAAQ;AAC7B,QAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,KAAK,QAAQ,GAAG;AACrD,UAAI,KAAK,GAAG,MAAM,GAAG,sBAAiB,MAAM,QAAQ,IAAI;AAAA,IAC1D;AAAA,EACF;AACA,SAAO;AACT;AAkBA,SAAS,WAAW,KAAiC;AACnD,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,WAAO,OAAO;AACd,UAAM,OAAO,OAAO,SAAS,QAAQ,QAAQ,EAAE;AAC/C,WAAO,GAAG,OAAO,MAAM,GAAG,QAAQ,GAAG,GAAG,OAAO,MAAM;AAAA,EACvD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,WAAW,MAAsC;AACxD,QAAM,QAAQ,oBAAI,IAAkB;AACpC,aAAW,QAAQ,KAAK,OAAO;AAC7B,UAAM,MAAM,WAAW,KAAK,MAAM,QAAQ;AAC1C,QAAI,IAAK,OAAM,IAAI,KAAK,IAAI;AAAA,EAC9B;AACA,SAAO;AACT;AAGA,SAAS,YAAY,MAAqB;AACxC,MAAI,iBAAiB,IAAI,EAAE,IAAI,SAAS,EAAG,QAAO;AAClD,QAAM,SAAS,KAAK,MAAM,QAAQ,cAAc,KAAK;AACrD,SAAO,OACJ,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,MAAM,KAAK,EAAE,YAAY,CAAC,EACzC,SAAS,SAAS;AACvB;AAEA,IAAM,8BAAwC;AAAA,EAC5C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,gBAAgB,mBAAmB;AAAA;AAAA;AAAA,EAG7C,WAAW,CAAC,SACV,KAAK,QAAQ,UAAU,QACvB,aAAa,KAAK,OAAO,QAAQ,GAAG,EAAE,YACrC,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EACvC,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,QAAQ,KAAK;AACnB,UAAM,UAAoB,CAAC;AAE3B,eAAW,SAAS,KAAK,UAAW,MAAM;AACxC,UAAI;AACJ,UAAI;AACF,cAAM,MAAM,IAAI,IAAI,MAAM,GAAG;AAC7B,eAAO,GAAG,IAAI,QAAQ,GAAG,IAAI,MAAM;AAAA,MACrC,QAAQ;AACN;AAAA,MACF;AACA,UAAI,CAAC,aAAa,MAAM,QAAQ,IAAI,EAAE,QAAS,SAAQ,KAAK,MAAM,GAAG;AAAA,IACvE;AAEA,QAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAClC,WAAO,MAAM;AAAA,MACX,SAAS,KAAK,UAAW,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjE,SAAS,GAAG,QAAQ,MAAM,gBAAgB,QAAQ,WAAW,IAAI,SAAS,SAAS,kCAAkC,OAAO,OAAO,CAAC;AAAA,MACpI,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,sBAAgC;AAAA,EACpC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,qBAAqB,oBAAoB;AAAA;AAAA;AAAA;AAAA,EAInD,WAAW,CAAC,UAAU,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC1D,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,QAAQ,WAAW,IAAI;AAC7B,UAAM,cAAwB,CAAC;AAE/B,eAAW,SAAS,KAAK,UAAW,MAAM;AACxC,YAAM,MAAM,WAAW,MAAM,GAAG;AAChC,YAAM,OAAO,MAAM,MAAM,IAAI,GAAG,IAAI;AACpC,UAAI,QAAQ,YAAY,IAAI,EAAG,aAAY,KAAK,MAAM,GAAG;AAAA,IAC3D;AAEA,QAAI,YAAY,WAAW,EAAG,QAAO,CAAC;AACtC,WAAO,MAAM;AAAA,MACX,SAAS,KAAK,UAAW,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjE,SAAS,GAAG,YAAY,MAAM,gBAAgB,YAAY,WAAW,IAAI,eAAe,aAAa,iBAAiB,OAAO,WAAW,CAAC;AAAA,MACzI,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,2BAAqC;AAAA,EACzC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,qBAAqB,yBAAyB;AAAA,EACxD,WAAW,CAAC,UAAU,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC1D,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,QAAQ,WAAW,IAAI;AAC7B,UAAM,QAAkB,CAAC;AAEzB,eAAW,SAAS,KAAK,UAAW,MAAM;AACxC,YAAM,MAAM,WAAW,MAAM,GAAG;AAChC,YAAM,OAAO,MAAM,MAAM,IAAI,GAAG,IAAI;AACpC,YAAM,YAAY,MAAM,MAAM;AAC9B,UAAI,CAAC,QAAQ,CAAC,UAAW;AAEzB,YAAM,eAAe,WAAW,SAAS;AAIzC,UAAI,gBAAgB,iBAAiB,KAAK;AACxC,cAAM,KAAK,GAAG,MAAM,GAAG,WAAM,SAAS,EAAE;AAAA,MAC1C;AAAA,IACF;AAEA,QAAI,MAAM,WAAW,EAAG,QAAO,CAAC;AAChC,WAAO,MAAM;AAAA,MACX,SAAS,KAAK,UAAW,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjE,SAAS,GAAG,MAAM,MAAM,gBAAgB,MAAM,WAAW,IAAI,mCAAmC,iCAAiC,qBAAqB,OAAO,KAAK,CAAC;AAAA,MACnK,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,iBAA2B;AAAA,EAC/B,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,qBAAqB,iBAAiB;AAAA;AAAA;AAAA;AAAA,EAIhD,WAAW,CAAC,SAAS,KAAK,WAAW,YAAY,UAAU,QAAQ,KAAK,UAAU,KAAK,SAAS;AAAA,EAChG,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,SAAS,IAAI;AAAA,MACjB,KAAK,UAAW,KAAK,IAAI,CAAC,UAAU,WAAW,MAAM,GAAG,CAAC,EAAE,OAAO,OAAO;AAAA,IAC3E;AAEA,UAAM,UAAU,KAAK,MAClB,OAAO,CAAC,SAAS,CAAC,YAAY,IAAI,CAAC,EACnC,IAAI,CAAC,UAAU,EAAE,MAAM,KAAK,WAAW,KAAK,MAAM,QAAQ,EAAE,EAAE,EAC9D,OAAO,CAAC,EAAE,IAAI,MAAM,QAAQ,UAAa,CAAC,OAAO,IAAI,GAAG,CAAC,EACzD,IAAI,CAAC,EAAE,KAAK,MAAM,KAAK,MAAM,QAAQ;AAExC,QAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAClC,WAAO,MAAM;AAAA,MACX,SAAS,KAAK,UAAW,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjE,SAAS,GAAG,QAAQ,MAAM,gBAAgB,QAAQ,WAAW,IAAI,KAAK,GAAG,OAAO,QAAQ,WAAW,IAAI,MAAM,EAAE,sBAAsB,QAAQ,WAAW,IAAI,OAAO,KAAK,6BAA6B,OAAO,OAAO,CAAC,kFAAkF,QAAQ,WAAW,IAAI,OAAO,MAAM;AAAA,MAC1U,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAaA,SAAS,YAAY,MAAwC;AAC3D,SAAO,CAAC,GAAI,KAAK,gBAAgB,MAAM,OAAO,KAAK,CAAC,CAAE;AACxD;AASA,SAAS,aAAa,MAA2B;AAC/C,QAAM,WAAW,KAAK,gBAAgB,YAAY;AAClD,MAAI,aAAa,EAAG,QAAO;AAC3B,SAAO,IAAI,QAAQ,gBAAgB,aAAa,IAAI,UAAU,UAAU;AAC1E;AAEA,IAAM,0BAAoC;AAAA,EACxC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,qBAAqB,iBAAiB;AAAA,EAChD,WAAW,CAAC,SAAS,YAAY,IAAI,EAAE,KAAK,CAAC,UAAU,OAAO,MAAM,MAAM,CAAC;AAAA,EAC3E,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,OAAO,YAAY,IAAI,EAAE,OAAO,CAAC,UAAU,OAAO,MAAM,MAAM,CAAC;AAErE,WAAO,MAAM;AAAA,MACX,SAAS,KAAK,UAAW,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjE,SAAS,GAAG,KAAK,MAAM,gBAAgB,KAAK,WAAW,IAAI,WAAW,QAAQ,gBAAgB,OAAO,KAAK,IAAI,CAAC,UAAU,GAAG,MAAM,GAAG,KAAK,MAAM,WAAW,IAAI,gBAAgB,QAAQ,MAAM,MAAM,EAAE,GAAG,CAAC,CAAC,wHAAwH,aAAa,IAAI,CAAC;AAAA,MACpV,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAEA,IAAM,wBAAkC;AAAA,EACtC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,CAAC,qBAAqB,iBAAiB;AAAA,EAChD,WAAW,CAAC,SAAS,YAAY,IAAI,EAAE,KAAK,CAAC,UAAU,MAAM,cAAc,CAAC,OAAO,MAAM,MAAM,CAAC;AAAA,EAChG,OAAO,CAAC,EAAE,MAAM,MAAM,MAAM;AAC1B,UAAM,QAAQ,YAAY,IAAI,EAAE,OAAO,CAAC,UAAU,MAAM,cAAc,CAAC,OAAO,MAAM,MAAM,CAAC;AAE3F,WAAO,MAAM;AAAA,MACX,SAAS,KAAK,UAAW,YAAY,cAAc,GAAG,KAAK,MAAM;AAAA,MACjE,SAAS,GAAG,MAAM,MAAM,gBAAgB,MAAM,WAAW,IAAI,gBAAgB,cAAc,KAAK,OAAO,MAAM,IAAI,CAAC,UAAU,GAAG,MAAM,GAAG,WAAM,MAAM,QAAQ,EAAE,CAAC,CAAC,wJAAwJ,aAAa,IAAI,CAAC;AAAA,MAC1U,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC7B,CAAC;AAAA,EACH;AACF;AAGA,SAAS,OAAO,QAAyB;AACvC,SAAO,WAAW,KAAK,UAAU;AACnC;AAGA,SAAS,gBAAgB,QAAyB;AAChD,SAAO,WAAW,KAAK,UAAU;AACnC;AAEA,SAAS,WAAW,OAAwB;AAC1C,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,KAAK;AACzB,WAAO,IAAI,aAAa,WAAW,IAAI,aAAa;AAAA,EACtD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,IAAM,aAAsC;AAAA,EACjD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,YAAY,IAAkC;AAC5D,SAAO,WAAW,KAAK,CAAC,SAAS,KAAK,OAAO,EAAE;AACjD;;;ACv0CO,SAAS,SACd,MACA,QAAiC,YACf;AAClB,QAAM,SAA2B,CAAC;AAElC,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,aAAa,CAAC,KAAK,UAAU,IAAI,EAAG;AAE7C,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM;AAAA,QAClB;AAAA,QACA,OAAO,CAAC,WAAW;AAAA,UACjB,QAAQ,KAAK;AAAA,UACb,UAAU,MAAM,YAAY,KAAK;AAAA,UACjC,SAAS,MAAM;AAAA,UACf,QAAQ,MAAM;AAAA,UACd,KAAK,MAAM;AAAA,UACX,YAAY,MAAM;AAAA,UAClB,SAAS,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,UAKf,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,OAAO,SAAS,KAAK,WAAW,CAAC,EAAE,IAAI,CAAC;AAAA,QACzE;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAK;AAGZ,aAAO,KAAK;AAAA,QACV,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,SAAS,eAAe,KAAK,EAAE,aAAa,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,QAC5F,SAAS,KAAK;AAAA,MAChB,CAAC;AACD;AAAA,IACF;AAEA,QAAI,CAAC,OAAQ;AACb,QAAI,MAAM,QAAQ,MAAM,EAAG,QAAO,KAAK,GAAG,MAAM;AAAA,QAC3C,QAAO,KAAK,MAAM;AAAA,EACzB;AAEA,SAAO,eAAe,MAAM;AAC9B;AAGO,SAAS,eAAe,QAA4C;AAGzE,QAAM,SAAS,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,cAAc,EAAE,OAAO,CAAC;AAC5E,SAAO,WAAW,MAAM;AAC1B;;;ACgBO,SAAS,eAAe,KAA4B;AACzD,MAAI;AACJ,MAAI;AACF,eAAW,IAAI,IAAI,GAAG,EAAE;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO,EAAE,CAAC;AACnD,MAAI,SAAS,uBAAuB,KAAK,EAAG,QAAO,MAAM,YAAY;AACrE,SAAO;AACT;AAGA,SAAS,WAAW,MAAkD;AACpE,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,OAAO,QAAQ,CAAC,GAAG;AAC5B,UAAM,SAAS,eAAe,GAAG;AACjC,QAAI,OAAQ,KAAI,IAAI,MAAM;AAAA,EAC5B;AACA,MAAI,OAAO,WAAW;AACtB,SAAO;AACT;AAGA,SAAS,mBAAmB,OAA2C;AACrE,QAAM,QAAQ,oBAAI,IAAmD;AAErE,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAM,eAAe,KAAK,MAAM,QAAQ;AAC9C,QAAI,CAAC,OAAO,QAAQ,YAAa;AACjC,UAAM,QAAQ,MAAM,IAAI,GAAG,KAAK,EAAE,OAAO,GAAG,OAAO,oBAAI,IAAY,EAAE;AACrE,UAAM,SAAS;AACf,UAAM,OAAO,KAAK,IAAI,UAAU,KAAK,EAAE,YAAY;AACnD,QAAI,KAAM,OAAM,MAAM,IAAI,IAAI;AAC9B,UAAM,IAAI,KAAK,KAAK;AAAA,EACtB;AAEA,QAAM,aAAgC,CAAC;AACvC,aAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,KAAK,OAAO;AAC9C,UAAM,gBAAgB,CAAC,GAAG,KAAK,EAAE,KAAK;AAGtC,UAAM,iBACJ,cAAc,WAAW,IACrB,SACA,cAAc,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,EAAE,CAAC,MAAM,IAAI,MAAM,GAAG,EAAE,CAAC,CAAC;AACrE,eAAW,KAAK;AAAA,MACd;AAAA,MACA,OAAO;AAAA,MACP,iBAAiB,mBAAmB,GAAG;AAAA,MACvC;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAGA,SAAO,WAAW;AAAA,IAChB,CAAC,GAAG,MACF,OAAO,EAAE,kBAAkB,KAAK,IAAI,OAAO,EAAE,kBAAkB,KAAK,KACpE,OAAO,EAAE,eAAe,IAAI,OAAO,EAAE,eAAe,KACpD,EAAE,QAAQ,EAAE,SACZ,EAAE,IAAI,cAAc,EAAE,GAAG;AAAA,EAC7B;AACF;AASO,SAAS,iBAAiB,OAA0C;AACzE,QAAM,WAAW,IAAI;AAAA,KAClB,MAAM,YAAY,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,EAAE,OAAO,OAAO;AAAA,EAC1E;AACA,WAAS,OAAO,WAAW;AAC3B,QAAM,cAAc,WAAW,MAAM,WAAW;AAEhD,MAAI,SAAS,OAAO,KAAK,YAAY,OAAO,GAAG;AAG7C,UAAM,MAAM,oBAAI,IAAI,CAAC,GAAG,UAAU,GAAG,WAAW,CAAC;AACjD,WAAO;AAAA,MACL,SAAS,CAAC,GAAG,GAAG,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;AAAA,MACnD,QAAQ,SAAS,OAAO,IAAI,aAAa;AAAA,MACzC,cAAc,IAAI,QAAQ;AAAA,MAC1B,YAAY,CAAC;AAAA,IACf;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS,CAAC;AAAA,IACV,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,YAAY,mBAAmB,MAAM,SAAS,CAAC,CAAC;AAAA,EAClD;AACF;AAGO,SAAS,iBAAiB,MAAiC;AAChE,QAAM,YAAY,KAAK,WAAW,OAAO,CAAC,MAAM,EAAE,mBAAmB,EAAE,mBAAmB,KAAK;AAC/F,MAAI,UAAU,SAAS,EAAG,QAAO;AACjC,SAAO,UACJ,IAAI,CAAC,MAAM,EAAE,GAAG,EAChB,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC,EACjC,KAAK,GAAG;AACb;;;AC1MA,SAAS,kBAAkB;;;ACA3B,YAAYC,cAAa;AA6CzB,SAAS,aAAa,MAAqC;AACzD,QAAM,MAA0B,CAAC;AACjC,QAAM,OAAO,oBAAI,IAAY;AAE7B,OAAK,SAAS,EAAE,KAAK,CAAC,GAAG,OAAO;AAC9B,UAAM,MAAM,aAAa,KAAM,GAAG,WAAW,KAAM;AACnD,QAAI,IAAI,MAAM,GAAG,EAAE,IAAI,GAAG,YAAY,MAAM,OAAQ;AAEpD,UAAM,QAAQ,aAAa,KAAK,GAAG,UAAU;AAC7C,QAAI,CAAC,MAAO;AAEZ,UAAM,OAAO,MAAM,OAAO,aAAa,KAAK,EAAE,YAAY;AAC1D,QAAI,QAAQ,YAAa;AAEzB,UAAM,YAAY,MAAM,YAAY,IAAI,KAAK;AAC7C,UAAM,QAAQ,MAAM,QAAQ,IAAI,KAAK;AACrC,QAAI,CAAC,YAAY,CAAC,KAAM;AAIxB,UAAM,MAAM,GAAG,SAAS,YAAY,CAAC,KAAK,IAAI;AAC9C,QAAI,KAAK,IAAI,GAAG,EAAG;AACnB,SAAK,IAAI,GAAG;AAEZ,QAAI,KAAK,EAAE,UAAU,KAAK,CAAC;AAAA,EAC7B,CAAC;AAED,SAAO;AACT;AAEO,SAAS,aAAa,MAA6B;AACxD,QAAM,UAAU,KAAK,KAAK;AAC1B,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO,EAAE,MAAM,WAAW,YAAY,OAAO,MAAM,CAAC,GAAG,UAAU,CAAC,EAAE;AAAA,EACtE;AAEA,MAAI;AACJ,MAAI;AACF,QAAY,cAAK,SAAS,EAAE,SAAS,KAAK,CAAC;AAAA,EAC7C,QAAQ;AACN,WAAO,EAAE,MAAM,WAAW,YAAY,OAAO,MAAM,CAAC,GAAG,UAAU,CAAC,EAAE;AAAA,EACtE;AAIA,MAAI,EAAE,cAAc,EAAE,SAAS,GAAG;AAChC,UAAM,WAAqB,CAAC;AAC5B,UAAM,OAAO,oBAAI,IAAY;AAC7B,MAAE,eAAe,EAAE,KAAK,CAAC,GAAG,OAAO;AACjC,YAAM,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK;AAC9B,UAAI,OAAO,CAAC,KAAK,IAAI,GAAG,GAAG;AACzB,aAAK,IAAI,GAAG;AACZ,iBAAS,KAAK,GAAG;AAAA,MACnB;AAAA,IACF,CAAC;AACD,WAAO,EAAE,MAAM,SAAS,YAAY,MAAM,SAAS;AAAA,EACrD;AAEA,MAAI,EAAE,QAAQ,EAAE,SAAS,KAAK,EAAE,KAAK,EAAE,SAAS,GAAG;AACjD,UAAM,OAA0B,CAAC;AACjC,UAAM,OAAO,oBAAI,IAAY;AAC7B,MAAE,KAAK,EAAE,KAAK,CAAC,GAAG,OAAO;AACvB,YAAM,OAAO,EAAE,EAAE;AACjB,YAAM,MAAM,KAAK,KAAK,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;AACjD,UAAI,CAAC,OAAO,KAAK,IAAI,GAAG,EAAG;AAC3B,WAAK,IAAI,GAAG;AACZ,YAAM,QAAyB,EAAE,IAAI;AACrC,YAAM,UAAU,KAAK,KAAK,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;AACzD,UAAI,QAAS,OAAM,UAAU;AAC7B,YAAM,aAAa,KAAK,KAAK,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;AAC/D,UAAI,WAAY,OAAM,aAAa;AACnC,YAAM,WAAW,KAAK,KAAK,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;AAC3D,UAAI,SAAU,OAAM,WAAW;AAC/B,YAAM,aAAa,aAAa,IAAI;AACpC,UAAI,WAAW,SAAS,EAAG,OAAM,aAAa;AAC9C,WAAK,KAAK,KAAK;AAAA,IACjB,CAAC;AACD,WAAO,EAAE,MAAM,UAAU,YAAY,MAAM,KAAK;AAAA,EAClD;AAEA,SAAO,EAAE,MAAM,WAAW,YAAY,OAAO,MAAM,CAAC,GAAG,UAAU,CAAC,EAAE;AACtE;;;AD9FA,IAAM,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMf,WAAW;AAAA,EACX,SAAS;AAAA,EACT,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,eAAe;AACjB;AAsCA,eAAsB,gBACpB,SACA,UAAkC,CAAC,GACX;AACxB,QAAM,SAAS,IAAI,IAAI,OAAO,EAAE;AAChC,QAAM,UAAU,QAAQ,WAAW,SAAS;AAC5C,QAAM,cAAc,QAAQ,eAAe,SAAS;AAEpD,QAAM,cAAkC;AAAA,IACtC,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,kBAAkB;AAAA,IAClB,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,mBAAmB;AAAA,IACnB,oBAAoB;AAAA,IACpB,UAAU;AAAA,IACV,UAAU,CAAC;AAAA,EACb;AAGA,QAAM,SAAS,MAAM,YAAY,QAAQ,EAAE,QAAQ,QAAQ,OAAO,CAAC;AACnE,cAAY,cAAc,OAAO;AAIjC,QAAM,eAAe,OAAO,SAAS,IAAI,CAAC,UAAU,MAAM,KAAK;AAE/D,QAAM,aAAiC,CAAC;AACxC,aAAW,YAAY,cAAc;AACnC,eAAW,KAAK,EAAE,KAAK,UAAU,QAAQ,UAAU,kBAAkB,KAAK,CAAC;AAAA,EAC7E;AACA,aAAW,KAAK;AAAA,IACd,KAAK,IAAI,IAAI,gBAAgB,MAAM,EAAE,SAAS;AAAA,IAC9C,QAAQ;AAAA,IACR,kBAAkB,aAAa,SAAS,IAAI,IAAI,gBAAgB,MAAM,EAAE,SAAS,CAAC;AAAA,EACpF,CAAC;AACD,aAAW,KAAK;AAAA,IACd,KAAK,IAAI,IAAI,sBAAsB,MAAM,EAAE,SAAS;AAAA,IACpD,QAAQ;AAAA,IACR,kBAAkB;AAAA,EACpB,CAAC;AAGD,aAAW,aAAa,YAAY;AAClC,UAAM,MAAM,MAAM,SAAS,UAAU,KAAK,OAAO;AACjD,QAAI,CAAC,IAAI,MAAM;AAEb,UAAI,YAAY,WAAW,EAAG,aAAY,SAAS,IAAI;AACvD,UAAI,IAAI,OAAO;AACb,oBAAY,gBAAgB,GAAG,UAAU,GAAG,KAAK,IAAI,KAAK;AAC1D,oBAAY,SAAS,KAAK,0BAA0B,UAAU,GAAG,WAAM,IAAI,KAAK,EAAE;AAAA,MACpF;AACA;AAAA,IACF;AACA,UAAM,SAAS,aAAa,IAAI,IAAI;AACpC,QAAI,OAAO,SAAS,WAAW;AAC7B,UAAI,YAAY,WAAW,EAAG,aAAY,SAAS,IAAI;AACvD,kBAAY,SAAS,KAAK,wCAAwC,UAAU,GAAG,EAAE;AACjF;AAAA,IACF;AAGA,gBAAY,QAAQ;AACpB,gBAAY,SAAS,IAAI;AACzB,gBAAY,aAAa;AACzB,gBAAY,aAAa,UAAU;AACnC,gBAAY,mBAAmB,UAAU;AACzC,gBAAY,kBAAkB,gBAAgB,UAAU,GAAG;AAE3D,UAAM,YAA+B,CAAC;AACtC,UAAM,OAAO,oBAAI,IAAY;AAC7B,UAAM,YAA+B,CAAC;AACtC,QAAI,YAAY;AAEhB,cAAU,KAAK;AAAA,MACb,KAAK,UAAU;AAAA,MACf,QAAQ,IAAI;AAAA,MACZ,YAAY,IAAI;AAAA,MAChB,SAAS,IAAI;AAAA,MACb,MAAM,OAAO,SAAS,UAAU,UAAU;AAAA,MAC1C,WAAW,OAAO,SAAS,UAAU,CAAC,GAAG,OAAO,QAAQ,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA,MAI7D,UAAU,OAAO,SAAS,WAAW,OAAO,KAAK,SAAS;AAAA,MAC1D,kBAAkB,UAAU;AAAA,IAC9B,CAAC;AAED,QAAI,OAAO,SAAS,UAAU;AAC5B,kBAAY,YAAY,WAAW,MAAM,OAAO,MAAM,SAAS,UAAU,GAAG;AAAA,IAC9E,OAAO;AACL,kBAAY,UAAU;AACtB,YAAM,WAAW,OAAO,SAAS,MAAM,GAAG,WAAW;AACrD,kBAAY,oBAAoB,OAAO,SAAS;AAChD,UAAI,OAAO,SAAS,SAAS,aAAa;AACxC,oBAAY;AACZ,oBAAY,SAAS;AAAA,UACnB,qBAAqB,OAAO,SAAS,MAAM,6BAA6B,WAAW;AAAA,QACrF;AAAA,MACF;AACA,iBAAW,YAAY,UAAU;AAC/B,YAAI,UAAU,UAAU,SAAS;AAC/B,sBAAY;AACZ;AAAA,QACF;AACA,cAAM,WAAW,MAAM,SAAS,UAAU,OAAO;AACjD,YAAI,CAAC,SAAS,MAAM;AAIlB,oBAAU,KAAK;AAAA,YACb,KAAK;AAAA,YACL,QAAQ,SAAS;AAAA,YACjB,YAAY,SAAS;AAAA,YACrB,SAAS,SAAS;AAAA,YAClB,MAAM;AAAA,YACN,WAAW,CAAC;AAAA,YACZ,UAAU;AAAA,YACV,kBAAkB,aAAa,SAAS,QAAQ;AAAA,YAChD,WAAW,UAAU;AAAA,UACvB,CAAC;AACD,sBAAY,sBAAsB;AAClC,sBAAY,SAAS,KAAK,8BAA8B,QAAQ,EAAE;AAClE;AAAA,QACF;AACA,cAAM,cAAc,aAAa,SAAS,IAAI;AAC9C,kBAAU,KAAK;AAAA,UACb,KAAK;AAAA,UACL,QAAQ,SAAS;AAAA,UACjB,YAAY,SAAS;AAAA,UACrB,SAAS,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAMlB,MAAM,YAAY,SAAS,YAAY,eAAe,YAAY;AAAA,UAClE,WAAW,YAAY,SAAS,UAAU,CAAC,GAAG,YAAY,QAAQ,IAAI,CAAC;AAAA,UACvE,UAAU,YAAY,SAAS,WAAW,YAAY,KAAK,SAAS;AAAA,UACpE,kBAAkB,aAAa,SAAS,QAAQ;AAAA,UAChD,WAAW,UAAU;AAAA,QACvB,CAAC;AACD,YAAI,YAAY,SAAS,UAAU;AACjC,sBAAY,sBAAsB;AAClC,sBAAY,SAAS,KAAK,+BAA+B,QAAQ,EAAE;AACnE;AAAA,QACF;AACA,YAAI,YAAY,WAAW,MAAM,YAAY,MAAM,SAAS,QAAQ,EAAG,aAAY;AAAA,MACrF;AAAA,IACF;AAEA,gBAAY,WAAW,UAAU;AAKjC,QAAI,UAAU,WAAW,KAAK,QAAQ,kBAAkB,OAAO;AAC7D,aAAO,UAAU,SAAS,QAAQ,SAAS,WAAW;AAAA,IACxD;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,QAAQ,UAAU;AAAA,MAClB,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAGA,MAAI,QAAQ,kBAAkB,OAAO;AACnC,WAAO,UAAU,SAAS,QAAQ,SAAS,WAAW;AAAA,EACxD;AAEA,cAAY,SAAS,KAAK,mBAAmB;AAC7C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,MAAM,CAAC;AAAA,IACP,WAAW,CAAC;AAAA,IACZ;AAAA,IACA,WAAW;AAAA,EACb;AACF;AAOA,eAAe,UACb,SACA,QACA,SACA,aACwB;AACxB,QAAM,SAAS,MAAM,MAAM;AAAA,IACzB,UAAU;AAAA,IACV,OAAO,QAAQ,cAAc,SAAS;AAAA,IACtC,UAAU,QAAQ,iBAAiB,SAAS;AAAA,IAC5C,gBAAgB;AAAA,MACd,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,kBAAkB,QAAQ;AAAA,MAC1B,QAAQ,QAAQ;AAAA,IAClB;AAAA,EACF,CAAC;AACD,QAAM,OAAO,OAAO,OAAO,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE;AAC1D,cAAY,WAAW,KAAK;AAC5B,cAAY,SAAS;AAAA,IACnB,YAAY,QACR,qEACA;AAAA,EACN;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR;AAAA;AAAA;AAAA;AAAA,IAIA,WAAW,CAAC;AAAA,IACZ;AAAA,IACA,WAAW,OAAO;AAAA,EACpB;AACF;AAGA,SAAS,YACP,KACA,MACA,SACA,KACA,aACS;AACT,aAAW,SAAS,SAAS;AAC3B,QAAI,IAAI,UAAU,IAAK,QAAO;AAC9B,QAAI,KAAK,IAAI,MAAM,GAAG,EAAG;AACzB,SAAK,IAAI,MAAM,GAAG;AAIlB,QAAI,KAAK,EAAE,GAAG,OAAO,YAAY,CAAC;AAAA,EACpC;AACA,SAAO,IAAI,UAAU;AACvB;AAEA,SAAS,OAAO,MAA0B;AACxC,SAAO,MAAM,KAAK,IAAI,IAAI,IAAI,CAAC;AACjC;AAEA,SAAS,gBAAgB,KAAsB;AAC7C,MAAI;AACF,UAAM,OAAO,IAAI,IAAI,GAAG,EAAE,SAAS,YAAY;AAC/C,WAAO,SAAS,kBAAkB,SAAS;AAAA,EAC7C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,eAAe,SAAS,KAAa,SAAsD;AACzF,QAAM,EAAE,QAAQ,QAAQ,IAAI;AAAA,IAC1B,QAAQ;AAAA,IACR,QAAQ,aAAa,SAAS;AAAA,EAChC;AACA,QAAM,aAAaC,qBAAoB,QAAQ,gBAAgB;AAC/D,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B;AAAA,MACA,UAAU;AAAA,MACV,SAAS;AAAA,QACP,QAAQ;AAAA,MACV;AAAA,IACF,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO,EAAE,QAAQ,IAAI,QAAQ,YAAY,GAAG,SAAS,MAAM;AAExE,UAAM,WAAW,IAAI,QAAQ,IAAI,kBAAkB,GAAG,YAAY,KAAK;AACvE,UAAM,YAAY,IAAI,YAAY,EAAE,SAAS,KAAK,KAAK,SAAS,SAAS,MAAM;AAC/E,QAAI,WAAW;AAGb,YAAM,MAAM,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC;AAI/C,UAAI;AACF,eAAO,WAAW,IAAI,QAAQ,WAAW,GAAG,EAAE,SAAS,MAAM,GAAG,IAAI;AAAA,MACtE,QAAQ;AAEN,eAAO,WAAW,IAAI,QAAQ,IAAI,SAAS,MAAM,GAAG,IAAI;AAAA,MAC1D;AAAA,IACF;AACA,WAAO,WAAW,IAAI,QAAQ,MAAM,IAAI,KAAK,GAAG,KAAK;AAAA,EACvD,SAAS,KAAK;AAIZ,UAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC9D,WAAO,EAAE,QAAQ,GAAG,OAAO,UAAU,iBAAiB,YAAY,GAAG,SAAS,MAAM;AAAA,EACtF,UAAE;AACA,YAAQ;AACR,eAAW;AAAA,EACb;AACF;AAGA,SAAS,WAAW,QAAgB,MAAc,SAA8B;AAC9E,SAAO,EAAE,QAAQ,MAAM,YAAY,OAAO,WAAW,MAAM,MAAM,GAAG,QAAQ;AAC9E;AAEA,SAASA,qBAAoB,SAA0C;AACrE,MAAI,CAAC,QAAS,QAAO,MAAM;AAC3B,QAAM,WAAW,QAAQ,IAAI;AAC7B,UAAQ,IAAI,+BAA+B;AAC3C,SAAO,MAAM;AACX,QAAI,aAAa,OAAW,QAAO,QAAQ,IAAI;AAAA,QAC1C,SAAQ,IAAI,+BAA+B;AAAA,EAClD;AACF;;;AEpUO,IAAM,qBACX;AAGF,IAAM,qBAAqB;AAC3B,IAAM,oBAAoB,IAAI,OAAO;AACrC,IAAM,iBACJ;AAGF,IAAM,QAAQ;AAEd,eAAsB,SACpB,KACA,UAA2B,CAAC,GACH;AACzB,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,YAAY,KAAK,IAAI;AAE3B,QAAM,EAAE,QAAQ,QAAQ,IAAI;AAAA,IAC1B,QAAQ;AAAA,IACR,QAAQ,aAAa;AAAA,EACvB;AACA,QAAM,aAAaC,qBAAoB,QAAQ,gBAAgB;AAE/D,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS;AAAA,QACP,cAAc,QAAQ,aAAa;AAAA,QACnC,QAAQ,QAAQ,UAAU;AAAA,MAC5B;AAAA,IACF,CAAC;AAED,UAAM,cAAc,qBAAqB,IAAI,QAAQ,IAAI,cAAc,CAAC;AACxE,UAAM,SAAyB;AAAA,MAC7B,cAAc;AAAA,MACd,UAAU,IAAI,OAAO;AAAA,MACrB,QAAQ,IAAI;AAAA,MACZ,YAAY,IAAI;AAAA,MAChB,eAAe,CAAC;AAAA,MAChB;AAAA,MACA,YAAY,KAAK,IAAI,IAAI;AAAA,IAC3B;AACA,UAAM,aAAa,IAAI,QAAQ,IAAI,aAAa;AAChD,QAAI,WAAY,QAAO,aAAa;AAEpC,QAAI,aAAa,YAAY,iBAAiB,IAAI,MAAM,GAAG;AACzD,YAAM,WAAW,IAAI,QAAQ,IAAI,UAAU;AAC3C,UAAI,UAAU;AACZ,YAAI;AACF,gBAAM,OAAO,IAAI,IAAI,UAAU,GAAG,EAAE,SAAS;AAC7C,iBAAO,WAAW;AAClB,iBAAO,gBAAgB,CAAC,IAAI;AAC5B,iBAAO,aAAa;AAAA,QACtB,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,YAAM,IAAI,MAAM,OAAO,EAAE,MAAM,MAAM,MAAS;AAC9C,aAAO;AAAA,IACT;AAEA,QAAI,WAAW,QAAQ;AACrB,YAAM,IAAI,MAAM,OAAO,EAAE,MAAM,MAAM,MAAS;AAC9C,aAAO;AAAA,IACT;AAEA,QAAI,eAAe,CAAC,MAAM,KAAK,WAAW,GAAG;AAC3C,YAAM,IAAI,MAAM,OAAO,EAAE,MAAM,MAAM,MAAS;AAC9C,aAAO;AAAA,IACT;AAEA,UAAM,EAAE,MAAM,UAAU,IAAI,MAAM,eAAe,KAAK,QAAQ;AAC9D,WAAO,OAAO;AACd,QAAI,UAAW,QAAO,YAAY;AAClC,WAAO;AAAA,EACT,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,cAAc;AAAA,MACd,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,eAAe,CAAC;AAAA,MAChB,QAAQ,cAAc,KAAK,QAAQ,MAAM;AAAA,MACzC,YAAY,KAAK,IAAI,IAAI;AAAA,IAC3B;AAAA,EACF,UAAE;AACA,YAAQ;AACR,eAAW;AAAA,EACb;AACF;AAEA,SAAS,iBAAiB,QAAyB;AACjD,SAAO,WAAW,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW,OAAO,WAAW;AAC5F;AAEA,SAAS,qBAAqB,KAAwC;AACpE,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,OAAO,IAAI,QAAQ,GAAG;AAC5B,UAAQ,SAAS,KAAK,MAAM,IAAI,MAAM,GAAG,IAAI,GAAG,KAAK,EAAE,YAAY,KAAK;AAC1E;AAMA,eAAe,eACb,KACA,UAC+C;AAC/C,QAAM,SAAS,IAAI,MAAM,UAAU;AACnC,MAAI,CAAC,QAAQ;AACX,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,QAAI,KAAK,SAAS,UAAU;AAC1B,aAAO,EAAE,MAAM,KAAK,MAAM,GAAG,QAAQ,GAAG,WAAW,KAAK;AAAA,IAC1D;AACA,WAAO,EAAE,MAAM,WAAW,MAAM;AAAA,EAClC;AAEA,QAAM,SAAuB,CAAC;AAC9B,MAAI,WAAW;AACf,MAAI,YAAY;AAChB,aAAS;AACP,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,QAAI,KAAM;AACV,QAAI,CAAC,MAAO;AACZ,WAAO,KAAK,KAAK;AACjB,gBAAY,MAAM;AAClB,QAAI,YAAY,UAAU;AACxB,kBAAY;AACZ,YAAM,OAAO,OAAO,EAAE,MAAM,MAAM,MAAS;AAC3C;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,OAAO,OAAO,OAAO,IAAI,CAAC,MAAM,OAAO,KAAK,CAAC,CAAC,CAAC;AAC3D,QAAM,SAAS,IAAI,aAAa,WAAW,IAAI,SAAS,GAAG,QAAQ,IAAI;AACvE,SAAO,EAAE,MAAM,OAAO,SAAS,MAAM,GAAG,UAAU;AACpD;AAGA,SAAS,cAAc,KAAc,cAAgD;AACnF,MAAI,aAAa,GAAG,GAAG;AAGrB,WAAO,cAAc,UAAU,UAAU;AAAA,EAC3C;AACA,QAAM,OAAO,UAAU,GAAG;AAC1B,MAAI,MAAM;AACR,QAAI,SAAS,eAAe,SAAS,YAAa,QAAO;AACzD,QAAI,KAAK,WAAW,OAAO,KAAK,KAAK,SAAS,aAAa,KAAK,KAAK,SAAS,MAAM,GAAG;AACrF,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,UAAU,eAAe,QAAQ,IAAI,QAAQ,YAAY,IAAI,OAAO,GAAG,EAAE,YAAY;AAC3F,MAAI,QAAQ,SAAS,aAAa,KAAK,QAAQ,SAAS,KAAK,KAAK,QAAQ,SAAS,KAAK,GAAG;AACzF,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,aAAa,KAAuB;AAC3C,SAAO,eAAe,SAAS,IAAI,SAAS;AAC9C;AAEA,SAAS,UAAU,KAAkC;AACnD,MAAI,OAAO,OAAO,QAAQ,UAAU;AAClC,UAAM,SAAU,IAA2B;AAC3C,QAAI,OAAO,WAAW,SAAU,QAAO;AACvC,UAAM,QAAS,IAA4B;AAC3C,QAAI,SAAS,OAAO,UAAU,UAAU;AACtC,YAAM,YAAa,MAA6B;AAChD,UAAI,OAAO,cAAc,SAAU,QAAO;AAAA,IAC5C;AAAA,EACF;AACA,SAAO;AACT;AAOA,SAASA,qBAAoB,SAA0C;AACrE,MAAI,CAAC,QAAS,QAAO,MAAM;AAC3B,QAAM,WAAW,QAAQ,IAAI;AAC7B,UAAQ,IAAI,+BAA+B;AAC3C,SAAO,MAAM;AACX,QAAI,aAAa,OAAW,QAAO,QAAQ,IAAI;AAAA,QAC1C,SAAQ,IAAI,+BAA+B;AAAA,EAClD;AACF;;;ACxPO,SAAS,aAAa,OAAmC;AAC9D,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,MAAI,MAAM,KAAM,QAAO;AACvB,MAAI,MAAM,aAAc,QAAO;AAC/B,MAAI,MAAM,WAAW,OAAO,MAAM,WAAW,IAAK,QAAO;AACzD,MAAI,MAAM,UAAU,IAAK,QAAO;AAChC,MAAI,MAAM,UAAU,OAAO,MAAM,SAAS,KAAK;AAC7C,WAAO,MAAM,aAAa,aAAa;AAAA,EACzC;AAEA,SAAO;AACT;;;ACAA,IAAMC,YAAW;AAAA,EACf,WAAW;AAAA,EACX,cAAc;AAAA,EACd,SAAS;AAAA,EACT,eAAe;AAAA,EACf,cAAc;AAAA,EACd,eAAe,KAAK;AACtB;AAEA,IAAM,eAAe;AACrB,IAAM,sBAAsB;AAc5B,eAAsB,UAAU,KAAa,UAA4B,CAAC,GAAuB;AAC/F,QAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,QAAM,UAAU,KAAK,IAAI;AAEzB,QAAM,SAAS,SAAS,GAAG;AAC3B,MAAI,WAAW,UAAU,WAAW,SAAS;AAC3C,WAAO;AAAA,MACL;AAAA,MACA,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,eAAe,CAAC;AAAA,MAChB,QAAQ,SAAS,uBAAuB,MAAM,KAAK;AAAA,MACnD;AAAA,MACA,YAAY,KAAK,IAAI,IAAI;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,UAAU,MAAM,eAAe,KAAK,OAAO;AACjD,QAAM,eAAe,eAAe,OAAO;AAC3C,QAAM,UAAU,aAAa;AAAA,IAC3B,QAAQ,QAAQ;AAAA,IAChB,YAAY,QAAQ,cAAc,SAAS;AAAA,IAC3C,QAAQ,QAAQ;AAAA,IAChB;AAAA,IACA,MAAM,QAAQ;AAAA,EAChB,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA,UAAU,QAAQ;AAAA,IAClB,QAAQ,QAAQ;AAAA,IAChB;AAAA,IACA,QAAQ,QAAQ;AAAA,IAChB,eAAe,QAAQ;AAAA,IACvB,QAAQ,eAAe,SAAS,YAAY;AAAA,IAC5C;AAAA,IACA,YAAY,KAAK,IAAI,IAAI;AAAA,EAC3B;AACF;AAGA,eAAe,eAAe,KAAa,SAAkD;AAC3F,QAAM,UAAU,QAAQ,WAAWA,UAAS;AAC5C,QAAM,QAAQ,QAAQ,SAAS;AAC/B,MAAI,UAAU,MAAM,sBAAsB,KAAK,OAAO;AAEtD,WAAS,UAAU,GAAG,UAAU,SAAS,WAAW;AAClD,QAAI,CAAC,YAAY,OAAO,EAAG;AAC3B,QAAI,QAAQ,QAAQ,QAAS;AAC7B,UAAM,QAAQ,aAAa,SAAS,SAAS,OAAO;AACpD,QAAI,QAAQ,EAAG,OAAM,MAAM,KAAK;AAChC,cAAU,MAAM,sBAAsB,KAAK,OAAO;AAAA,EACpD;AAEA,SAAO;AACT;AAMA,eAAe,sBACb,KACA,SACuB;AACvB,QAAM,OAAO,MAAM,gBAAgB,KAAK,QAAQ,OAAO;AACvD,MAAI,aAAa,IAAI,KAAM,KAAK,UAAU,OAAO,KAAK,SAAS,KAAM;AAGnE,UAAM,MAAM,MAAM,gBAAgB,KAAK,OAAO,OAAO;AAGrD,QAAI,IAAI,WAAW,KAAK,KAAK,WAAW,KAAK,CAAC,aAAa,IAAI,EAAG,QAAO;AACzE,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGA,SAAS,aAAa,GAA0B;AAC9C,SAAO,EAAE,WAAW,OAAO,EAAE,WAAW,OAAO,EAAE,WAAW,OAAO,EAAE,WAAW;AAClF;AAMA,eAAe,gBACb,KACA,QACA,SACuB;AACvB,QAAM,eAAe,QAAQ,gBAAgBA,UAAS;AACtD,QAAM,QAAkB,CAAC;AACzB,QAAM,UAAU,oBAAI,IAAY,CAAC,GAAG,CAAC;AACrC,MAAI,UAAU;AAEd,WAAS,MAAM,GAAG,OAAO,cAAc,OAAO;AAC5C,UAAM,MAAM,MAAM,SAAS,SAAS;AAAA,MAClC;AAAA,MACA,UAAU;AAAA,MACV,WAAW,QAAQ,aAAaA,UAAS;AAAA,MACzC,kBAAkB,QAAQ;AAAA,MAC1B,WAAW,QAAQ;AAAA,MACnB,UAAU,WAAW,QAAS,QAAQ,iBAAiBA,UAAS,gBAAiB;AAAA,IACnF,CAAC;AAED,QAAI,IAAI,WAAW,GAAG;AACpB,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,eAAe;AAAA,QACf,QAAQ,IAAI;AAAA,QACZ,MAAM;AAAA,QACN;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAAO,IAAI,cAAc,CAAC;AAChC,QAAI,MAAM;AACR,YAAM,KAAK,IAAI;AACf,UAAI,QAAQ,IAAI,IAAI,GAAG;AACrB,eAAO,EAAE,QAAQ,IAAI,QAAQ,UAAU,MAAM,eAAe,OAAO,MAAM,MAAM,OAAO;AAAA,MACxF;AACA,cAAQ,IAAI,IAAI;AAChB,gBAAU;AACV;AAAA,IACF;AAEA,WAAO;AAAA,MACL,QAAQ,IAAI;AAAA,MACZ,UAAU,IAAI;AAAA,MACd,eAAe;AAAA,MACf,MAAM;AAAA,MACN,MAAM,IAAI;AAAA,MACV,YAAY,IAAI;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AAGA,SAAO,EAAE,QAAQ,GAAG,UAAU,SAAS,eAAe,OAAO,MAAM,MAAM,OAAO;AAClF;AAEA,SAAS,eAAe,GAA0B;AAChD,MAAI,EAAE,SAAS,OAAO,EAAE,UAAU,IAAK,QAAO;AAC9C,MAAI,EAAE,SAAS,OAAW,QAAO;AAGjC,MAAI,EAAE,KAAK,SAAS,oBAAqB,QAAO;AAChD,SAAO,aAAa,KAAK,EAAE,IAAI;AACjC;AAEA,SAAS,YAAY,GAA0B;AAC7C,MAAI,EAAE,WAAW,EAAG,QAAO;AAC3B,MAAI,EAAE,WAAW,IAAK,QAAO;AAC7B,MAAI,EAAE,UAAU,IAAK,QAAO;AAC5B,SAAO;AACT;AAEA,SAAS,aAAa,GAAiB,SAAiB,SAAmC;AACzF,QAAM,MAAM,QAAQ,gBAAgBA,UAAS;AAC7C,QAAM,eAAe,gBAAgB,EAAE,UAAU;AACjD,MAAI,iBAAiB,OAAW,QAAO,KAAK,IAAI,cAAc,GAAG;AACjE,QAAM,OAAO,QAAQ,iBAAiBA,UAAS;AAC/C,QAAM,MAAM,OAAO,KAAK;AACxB,QAAM,SAAS,KAAK,MAAM,KAAK,OAAO,IAAI,IAAI;AAC9C,SAAO,KAAK,IAAI,MAAM,QAAQ,GAAG;AACnC;AAGA,SAAS,gBAAgB,OAA+C;AACtE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,OAAO,KAAK;AAC5B,MAAI,OAAO,SAAS,OAAO,EAAG,QAAO,KAAK,IAAI,GAAG,UAAU,GAAI;AAC/D,QAAM,OAAO,KAAK,MAAM,KAAK;AAC7B,MAAI,CAAC,OAAO,MAAM,IAAI,EAAG,QAAO,KAAK,IAAI,GAAG,OAAO,KAAK,IAAI,CAAC;AAC7D,SAAO;AACT;AAEA,SAAS,eAAe,GAAiB,cAA2C;AAClF,MAAI,EAAE,OAAQ,QAAO,EAAE;AACvB,MAAI,EAAE,KAAM,QAAO;AACnB,MAAI,aAAc,QAAO;AACzB,MAAI,EAAE,WAAW,IAAK,QAAO;AAC7B,MAAI,EAAE,WAAW,IAAK,QAAO;AAC7B,MAAI,EAAE,UAAU,IAAK,QAAO,GAAG,EAAE,MAAM;AACvC,SAAO;AACT;AAEA,SAAS,SAAS,KAA4B;AAC5C,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE,SAAS,QAAQ,MAAM,EAAE,EAAE,YAAY;AAAA,EAC7D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,aAAa,IAA2B;AAC/C,SAAO,IAAI,QAAQ,CAACC,aAAY,WAAWA,UAAS,EAAE,CAAC;AACzD;;;AChOA,eAAsB,oBACpB,MACA,SAC6B;AAC7B,QAAM,QAAQ,oBAAI,IAA+B;AACjD,QAAM,UAAoB,CAAC;AAE3B,aAAW,OAAO,MAAM;AACtB,QAAI,MAAM,IAAI,GAAG,EAAG;AAEpB,UAAM,UAAU,QAAQ,QAAQ,IAAI,GAAG;AACvC,QAAI,YAAY,QAAW;AACzB,YAAM,IAAI,KAAK,EAAE,KAAK,KAAK,QAAQ,SAAS,KAAK,SAAS,UAAU,KAAK,YAAY,MAAM,CAAC;AAC5F;AAAA,IACF;AAEA,UAAM,UAAU,QAAQ,QAAQ,IAAI,GAAG;AACvC,QAAI,SAAS;AACX,YAAM,IAAI,KAAK,UAAU,KAAK,SAAS,YAAY,CAAC;AACpD;AAAA,IACF;AAEA,YAAQ,KAAK,GAAG;AAAA,EAClB;AAEA,QAAM,SAAS,QAAQ,MAAM,GAAG,QAAQ,SAAS;AACjD,QAAM,WAAW,QAAQ,SAAS,OAAO;AAEzC,QAAM,QAAQ,CAAC,GAAG,MAAM;AACxB,QAAM,UAAU,KAAK,IAAI,QAAQ,eAAe,GAAG,MAAM,MAAM;AAC/D,QAAM,QAAQ;AAAA,IACZ,MAAM,KAAK,EAAE,QAAQ,QAAQ,GAAG,YAAY;AAC1C,eAAS,MAAM,MAAM,IAAI,GAAG,QAAQ,QAAW,MAAM,MAAM,IAAI,GAAG;AAChE,cAAM,QAAQ,MAAM,UAAU,KAAK;AAAA,UACjC,QAAQ,QAAQ;AAAA,UAChB,WAAW,QAAQ;AAAA,UACnB,kBAAkB,QAAQ;AAAA,QAC5B,CAAC,EAAE,MAAM,MAAM,MAAS;AACxB,YAAI,MAAO,OAAM,IAAI,KAAK,UAAU,KAAK,OAAO,OAAO,CAAC;AAAA,MAC1D;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO,EAAE,OAAO,SAAS;AAC3B;AAEA,SAAS,UACP,KACA,OACA,KACmB;AACnB,SAAO;AAAA,IACL;AAAA,IACA,QAAQ,MAAM;AAAA,IACd;AAAA,IACA,UAAU,MAAM;AAAA;AAAA;AAAA,IAGhB,YAAY,MAAM,aAAa,OAAO,MAAM,cAAc,SAAS;AAAA,EACrE;AACF;;;ACnCA,SAASC,WAAU,KAA4B;AAC7C,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,GAAG;AAAA,EACnB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO;AACX,QAAM,OAAO,IAAI,SAAS,SAAS,IAAI,IAAI,SAAS,QAAQ,QAAQ,EAAE,IAAI,IAAI;AAC9E,SAAO,GAAG,IAAI,MAAM,GAAG,IAAI,GAAG,IAAI,MAAM;AAC1C;AAEA,IAAM,YAAY;AAGlB,SAAS,aAAa,MAA2C;AAC/D,SAAO,IAAI,IAAI,KAAK,OAAO,CAAC,EAAE;AAChC;AAEA,SAAS,QAAQ,MAA2B,WAAqB,SAA+B;AAC9F,SAAO;AAAA,IACL;AAAA,IACA,SAAS,CAAC,QAAQ;AAChB,YAAM,MAAMA,WAAU,GAAG;AACzB,aAAO,QAAQ,OAAO,SAAY,KAAK,IAAI,GAAG;AAAA,IAChD;AAAA,IACA,MAAM,aAAa,IAAI;AAAA,IACvB;AAAA,IACA;AAAA,EACF;AACF;AAQO,SAAS,kBAAkB,SAAmD;AACnF,QAAM,OAAO,oBAAI,IAAoB;AACrC,QAAM,YAAsB,CAAC;AAC7B,MAAI,UAAU;AAEd,aAAW,SAAS,SAAS;AAC3B,UAAM,aAAa,MAAM;AACzB,QAAI,CAAC,cAAc,WAAW,WAAW,EAAG;AAE5C,UAAM,WAAW,WAAW,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,YAAY,MAAM,SAAS;AAIrF,QAAI,CAAC,UAAU;AACb;AACA;AAAA,IACF;AAEA,UAAM,YAAYA,WAAU,SAAS,IAAI;AACzC,QAAI,CAAC,WAAW;AACd;AACA;AAAA,IACF;AAEA,UAAM,EAAE,OAAO,YAAY,IAAI,WAAW,IAAI,IAAI,SAAS,EAAE,QAAQ;AAKrE,UAAM,UAAU,CAAC,MAAM,KAAK,GAAG,WAAW,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AAE5D,eAAW,UAAU,SAAS;AAC5B,YAAM,MAAMA,WAAU,MAAM;AAC5B,UAAI,CAAC,IAAK;AAEV,YAAM,WAAW,KAAK,IAAI,GAAG;AAC7B,UAAI,aAAa,QAAW;AAC1B,aAAK,IAAI,KAAK,WAAW;AACzB;AAAA,MACF;AAGA,UAAI,aAAa,eAAe,CAAC,UAAU,SAAS,GAAG,EAAG,WAAU,KAAK,GAAG;AAAA,IAC9E;AAAA,EACF;AAEA,SAAO,QAAQ,MAAM,WAAW,OAAO;AACzC;AAaA,SAAS,UAAU,OAAsD;AACvE,QAAM,OAAO,oBAAI,IAA6B;AAE9C,aAAW,QAAQ,OAAO;AACxB,UAAM,OAAOA,WAAU,KAAK,MAAM,QAAQ;AAC1C,QAAI,SAAS,QAAQ,KAAK,IAAI,IAAI,EAAG;AAErC,UAAM,QAAQ,oBAAI,IAAY;AAC9B,UAAM,YAAY,oBAAI,IAAY;AAClC,eAAW,OAAO,KAAK,MAAM,YAAY;AACvC,YAAM,OAAOA,WAAU,IAAI,IAAI;AAC/B,UAAI,SAAS,KAAM;AACnB,UAAI,IAAI,WAAY,WAAU,IAAI,IAAI;AAAA,eAC7B,SAAS,KAAM,OAAM,IAAI,IAAI;AAAA,IACxC;AAEA,SAAK,IAAI,MAAM,EAAE,OAAO,UAAU,UAAU,SAAS,IAAI,CAAC,GAAG,SAAS,EAAE,CAAC,IAAK,KAAK,CAAC;AAAA,EACtF;AAEA,SAAO;AACT;AAwBO,SAAS,sBAAsB,OAAsC;AAC1E,QAAM,OAAO,UAAU,KAAK;AAG5B,QAAM,SAAS,oBAAI,IAAoB;AACvC,aAAW,OAAO,KAAK,KAAK,EAAG,QAAO,IAAI,KAAK,GAAG;AAClD,QAAM,OAAO,CAAC,UAA0B;AACtC,QAAI,OAAO;AACX,WAAO,OAAO,IAAI,IAAI,MAAM,KAAM,QAAO,OAAO,IAAI,IAAI;AACxD,QAAI,SAAS;AACb,WAAO,OAAO,IAAI,MAAM,MAAM,MAAM;AAClC,YAAM,OAAO,OAAO,IAAI,MAAM;AAC9B,aAAO,IAAI,QAAQ,IAAI;AACvB,eAAS;AAAA,IACX;AACA,WAAO;AAAA,EACT;AAEA,aAAW,CAAC,KAAK,IAAI,KAAK,MAAM;AAC9B,eAAW,QAAQ,KAAK,OAAO;AAG7B,UAAI,CAAC,KAAK,IAAI,IAAI,GAAG,MAAM,IAAI,GAAG,EAAG;AACrC,YAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,GAAG,GAAG,KAAK,IAAI,CAAC;AACrC,UAAI,MAAM,EAAG,QAAO,IAAI,GAAG,CAAC;AAAA,IAC9B;AAAA,EACF;AAEA,QAAM,aAAa,oBAAI,IAAsB;AAC7C,aAAW,OAAO,KAAK,KAAK,GAAG;AAC7B,UAAM,OAAO,KAAK,GAAG;AACrB,UAAM,UAAU,WAAW,IAAI,IAAI;AACnC,QAAI,QAAS,SAAQ,KAAK,GAAG;AAAA,QACxB,YAAW,IAAI,MAAM,CAAC,GAAG,CAAC;AAAA,EACjC;AAEA,QAAM,OAAO,oBAAI,IAAoB;AACrC,MAAI,UAAU;AAEd,aAAW,WAAW,WAAW,OAAO,GAAG;AAGzC,QAAI,QAAQ,SAAS,EAAG;AAExB,UAAM,WAAW,IAAI;AAAA,MACnB,QAAQ,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,GAAG,QAAQ,EAAE,OAAO,CAAC,MAAmB,QAAQ,CAAC,CAAC;AAAA,IACjF;AAIA,QAAI,SAAS,SAAS,GAAG;AACvB;AACA;AAAA,IACF;AAEA,UAAM,SAAS,CAAC,GAAG,QAAQ,EAAE,CAAC;AAC9B,QAAI,CAAC,QAAQ,SAAS,MAAM,GAAG;AAC7B;AACA;AAAA,IACF;AAEA,UAAM,EAAE,MAAM,IAAI,WAAW,IAAI,IAAI,MAAM,EAAE,QAAQ;AACrD,eAAW,UAAU,QAAS,MAAK,IAAI,QAAQ,KAAK;AAAA,EACtD;AAIA,SAAO,QAAQ,MAAM,CAAC,GAAG,OAAO;AAClC;AAWO,SAAS,sBACd,SACA,WACc;AACd,QAAM,OAAO,IAAI,IAAI,UAAU,IAAI;AACnC,QAAM,YAAY,CAAC,GAAG,QAAQ,SAAS;AACvC,QAAM,MAAM,CAAC,QAAgB;AAC3B,QAAI,CAAC,UAAU,SAAS,GAAG,EAAG,WAAU,KAAK,GAAG;AAAA,EAClD;AACA,aAAW,OAAO,UAAU,UAAW,KAAI,GAAG;AAE9C,aAAW,CAAC,KAAK,KAAK,KAAK,QAAQ,MAAM;AACvC,UAAM,UAAU,KAAK,IAAI,GAAG;AAC5B,QAAI,YAAY,UAAa,YAAY,MAAO,KAAI,GAAG;AACvD,SAAK,IAAI,KAAK,KAAK;AAAA,EACrB;AAEA,SAAO,QAAQ,MAAM,WAAW,QAAQ,UAAU,UAAU,OAAO;AACrE;AAUO,SAAS,iBAAiB,SAAuB,WAAiC;AACvF,QAAM,SAAS,oBAAI,IAAY;AAC/B,aAAW,CAAC,KAAK,KAAK,KAAK,UAAU,MAAM;AACzC,QAAI,CAAC,QAAQ,KAAK,IAAI,GAAG,EAAG,QAAO,IAAI,KAAK;AAAA,EAC9C;AACA,SAAO,OAAO;AAChB;;;ACvRA,IAAM,qBAAqB;AAO3B,IAAM,oBAAoB;AAG1B,SAAS,SAAS,KAAuB;AACvC,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AAAA,EACxD,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAUA,SAAS,QAAQ,OAA0B,SAAwC;AACjF,SAAO,MAAM,IAAI,CAAC,SAAU,QAAQ,IAAI,KAAK,YAAY,CAAC,IAAI,aAAa,IAAK;AAClF;AASA,SAAS,UAAU,OAA0B,WAAwC;AACnF,QAAM,MAAgB,CAAC;AACvB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;AAIxC,UAAM,SAAS,MAAM,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AACzC,QAAI,KAAK,UAAU,IAAI,GAAG,CAAC,IAAI,MAAM,EAAE,IAAI,IAAI,CAAC,MAAM,MAAM,CAAC,CAAE;AAAA,EACjE;AACA,SAAO,IAAI,IAAI,KAAK,GAAG,CAAC;AAC1B;AAWA,SAAS,gBAAgBC,SAA2B,GAAqB;AACvE,MAAIA,QAAO,UAAU,EAAG,QAAO,CAAC,GAAGA,OAAM;AACzC,MAAI,KAAK,EAAG,QAAO,CAACA,QAAO,CAAC,CAAE;AAE9B,QAAM,SAAmB,CAAC;AAC1B,WAAS,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;AAC7B,WAAO,KAAKA,QAAO,KAAK,MAAO,KAAKA,QAAO,SAAS,MAAO,IAAI,EAAE,CAAC,CAAE;AAAA,EACtE;AACA,SAAO,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC;AAC5B;AAEO,SAAS,kBACd,MACA,UAAyB,CAAC,GACP;AACnB,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,UAAU,IAAI,KAAK,QAAQ,WAAW,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AAE3E,QAAM,SAAS,KAAK,IAAI,CAAC,SAAS,EAAE,KAAK,OAAO,QAAQ,SAAS,GAAG,GAAG,OAAO,EAAE,EAAE;AAKlF,QAAM,mBAAmB,oBAAI,IAAyB;AACtD,aAAW,EAAE,MAAM,KAAK,QAAQ;AAC9B,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;AACxC,YAAM,MAAM,GAAG,CAAC,IAAI,MAAM,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC;AAC/C,YAAM,MAAM,iBAAiB,IAAI,GAAG,KAAK,oBAAI,IAAY;AACzD,UAAI,IAAI,MAAM,CAAC,CAAE;AACjB,uBAAiB,IAAI,KAAK,GAAG;AAAA,IAC/B;AAAA,EACF;AAaA,QAAM,YAAY,oBAAI,IAAY;AAClC,aAAW,CAAC,KAAK,QAAQ,KAAK,kBAAkB;AAC9C,UAAM,QAAQ,IAAI,QAAQ,GAAG;AAC7B,UAAM,QAAQ,OAAO,IAAI,MAAM,GAAG,KAAK,CAAC;AACxC,UAAM,SAAS,IAAI,MAAM,QAAQ,CAAC;AAElC,UAAM,iBAAiB,UAAU,KAAM,UAAU,KAAK,WAAW;AACjE,QAAI,eAAgB;AACpB,QAAI,SAAS,QAAQ,UAAW,WAAU,IAAI,GAAG;AAAA,EACnD;AAKA,QAAM,SAAS,oBAAI,IAAiD;AACpE,aAAW,EAAE,KAAK,MAAM,KAAK,QAAQ;AACnC,UAAM,UAAU,UAAU,OAAO,SAAS;AAC1C,UAAM,aAAa,SAAS,GAAG,EAAE,KAAK,CAAC,MAAM,QAAQ,IAAI,EAAE,YAAY,CAAC,CAAC,KAAK;AAC9E,UAAM,MAAM,GAAG,UAAU,IAAI,OAAO;AACpC,UAAM,QAAQ,OAAO,IAAI,GAAG,KAAK,EAAE,SAAS,MAAM,CAAC,EAAE;AACrD,UAAM,KAAK,KAAK,GAAG;AACnB,WAAO,IAAI,KAAK,KAAK;AAAA,EACvB;AAEA,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,WAAW,oBAAI,IAAyB;AAE9C,aAAW,EAAE,SAAS,MAAM,UAAU,KAAK,OAAO,OAAO,GAAG;AAO1D,UAAMA,UAAS,CAAC,GAAG,SAAS,EAAE,KAAK;AACnC,UAAM,SAASA,QAAO,SAAS,YAAYA,UAAS,gBAAgBA,SAAQ,SAAS;AACrF,eAAW,OAAO,OAAQ,MAAK,IAAI,GAAG;AAEtC,QAAIA,QAAO,UAAU,WAAW;AAC9B,YAAM,OAAO,SAAS,IAAI,OAAO;AACjC,eAAS,IAAI,SAAS;AAAA,QACpB;AAAA,QACA,OAAO,MAAM,QAAQ,KAAKA,QAAO;AAAA,QACjC,UAAU,MAAM,WAAW,KAAK,OAAO;AAAA,MACzC,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM,KAAK,OAAO,CAAC,QAAQ,KAAK,IAAI,GAAG,CAAC;AAAA,IACxC,UAAU,CAAC,GAAG,SAAS,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI;AAAA,IAC/D,OAAO,KAAK;AAAA,EACd;AACF;;;AC5KA,IAAM,aAAa,IAAI;AAUhB,SAAS,mBAAmB,aAA0C;AAC3E,MAAI,CAAC,YAAa,QAAO;AACzB,SACE,YAAY,WAAW,QAAQ,KAC/B,gBAAgB,qBAChB,gBAAgB;AAEpB;AAGA,eAAsB,WACpB,KACA,UAA6B,CAAC,GACT;AACrB,QAAM,EAAE,QAAQ,QAAQ,IAAI,eAAe,QAAQ,QAAQ,QAAQ,aAAa,GAAK;AAErF,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B;AAAA,MACA,UAAU;AAAA,MACV,SAAS,EAAE,OAAO,WAAW,aAAa,CAAC,IAAI,QAAQ,oBAAoB;AAAA,IAC7E,CAAC;AAED,UAAM,cAAc,IAAI,QAAQ,IAAI,cAAc,GAAG,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,YAAY;AAGvF,UAAM,SAAS,IAAI,WAAW,OAAO,IAAI,WAAW;AACpD,UAAM,KAAK,UAAU,mBAAmB,WAAW;AAEnD,UAAM,OAAO,SAAS,MAAM,SAAS,GAAG,IAAI;AAC5C,UAAM,QAAQ,OAAO,YAAY,IAAI,IAAI;AAEzC,WAAO,EAAE,KAAK,QAAQ,IAAI,QAAQ,IAAI,aAAa,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC,EAAG;AAAA,EACjF,QAAQ;AACN,WAAO,EAAE,KAAK,QAAQ,GAAG,IAAI,MAAM;AAAA,EACrC,UAAE;AACA,YAAQ;AAAA,EACV;AACF;AAGA,eAAe,SAAS,KAA4C;AAClE,MAAI,CAAC,IAAI,KAAM,QAAO;AAEtB,QAAM,SAAS,IAAI,KAAK,UAAU;AAClC,QAAM,SAAuB,CAAC;AAC9B,MAAI,QAAQ;AAEZ,MAAI;AACF,WAAO,QAAQ,YAAY;AACzB,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,KAAM;AACV,UAAI,OAAO;AACT,eAAO,KAAK,KAAK;AACjB,iBAAS,MAAM;AAAA,MACjB;AAAA,IACF;AAAA,EACF,QAAQ;AACN,WAAO,OAAO,SAAS,IAAI,OAAO,OAAO,MAAM,IAAI;AAAA,EACrD,UAAE;AAGA,UAAM,OAAO,OAAO,EAAE,MAAM,MAAM,MAAS;AAAA,EAC7C;AAOA,SAAO,OAAO,SAAS,IAAI,OAAO,OAAO,MAAM,EAAE,SAAS,GAAG,UAAU,IAAI;AAC7E;AAEA,IAAM,gBAAgB,OAAO,KAAK,CAAC,KAAM,IAAM,IAAM,IAAM,IAAM,IAAM,IAAM,EAAI,CAAC;AAmBlF,IAAM,WAAW;AAUjB,IAAM,qBAAqB,oBAAI,IAAI;AAAA,EACjC;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAC1E,CAAC;AAcD,SAAS,eAAe,MAAqC;AAC3D,MAAI,KAAK;AAET,SAAO,KAAK,IAAI,KAAK,QAAQ;AAC3B,QAAI,KAAK,EAAE,MAAM,IAAM,QAAO;AAE9B,UAAM,SAAS,KAAK,KAAK,CAAC;AAE1B,QAAI,WAAW,KAAM;AACnB,YAAM;AACN;AAAA,IACF;AAEA,QAAI,WAAW,OAAQ,WAAW,KAAS,UAAU,OAAQ,UAAU,KAAO;AAC5E,YAAM;AACN;AAAA,IACF;AAEA,QAAI,WAAW,OAAQ,WAAW,IAAM,QAAO;AAE/C,UAAM,SAAS,KAAK,aAAa,KAAK,CAAC;AACvC,QAAI,SAAS,EAAG,QAAO;AAEvB,QAAI,mBAAmB,IAAI,MAAM,GAAG;AAElC,UAAI,KAAK,IAAI,KAAK,OAAQ,QAAO;AACjC,aAAO,EAAE,OAAO,KAAK,aAAa,KAAK,CAAC,GAAG,QAAQ,KAAK,aAAa,KAAK,CAAC,EAAE;AAAA,IAC/E;AAEA,UAAM,IAAI;AAAA,EACZ;AAEA,SAAO;AACT;AAEA,SAAS,YAAY,MAAuC;AAC1D,MAAI,KAAK,UAAU,MAAM,KAAK,SAAS,GAAG,CAAC,EAAE,OAAO,aAAa,GAAG;AAClE,WAAO,CAAC,EAAE,OAAO,KAAK,aAAa,EAAE,GAAG,QAAQ,KAAK,aAAa,EAAE,EAAE,CAAC;AAAA,EACzE;AAEA,MAAI,KAAK,UAAU,KAAK,KAAK,aAAa,CAAC,MAAM,UAAU;AACzD,UAAM,OAAO,eAAe,IAAI;AAChC,QAAI,KAAM,QAAO,CAAC,IAAI;AAGtB,WAAO;AAAA,EACT;AAGA,MAAI,KAAK,UAAU,KAAK,KAAK,aAAa,CAAC,MAAM,KAAK,KAAK,aAAa,CAAC,MAAM,GAAG;AAChF,UAAMC,SAAQ,KAAK,aAAa,CAAC;AACjC,QAAIA,WAAU,KAAK,KAAK,SAAS,IAAI,KAAKA,OAAO,QAAO;AAExD,UAAM,QAAqB,CAAC;AAC5B,aAAS,QAAQ,GAAG,QAAQA,QAAO,SAAS;AAC1C,YAAM,KAAK,IAAI,KAAK;AAEpB,YAAM,KAAK,EAAE,OAAO,KAAK,EAAE,KAAK,KAAK,QAAQ,KAAK,KAAK,CAAC,KAAK,IAAI,CAAC;AAAA,IACpE;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;;;AC7MA,eAAsB,aACpB,QACA,UAA+B,CAAC,GACT;AACvB,QAAM,MAAM,IAAI,IAAI,gBAAgB,MAAM,EAAE,SAAS;AACrD,QAAM,EAAE,QAAQ,QAAQ,IAAI,eAAe,QAAQ,QAAQ,QAAQ,aAAa,GAAK;AAErF,MAAI;AACF,QAAI,MAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,QAAQ,QAAQ,UAAU,SAAS,CAAC;AACzE,QAAI,IAAI,WAAW,OAAO,IAAI,WAAW,KAAK;AAC5C,YAAM,MAAM,MAAM,KAAK,EAAE,QAAQ,UAAU,SAAS,CAAC;AAAA,IACvD;AAEA,UAAM,cAAc,IAAI,QAAQ,IAAI,cAAc,GAAG,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,YAAY;AAEvF,WAAO;AAAA,MACL;AAAA,MACA,QAAQ,IAAI;AAAA,MACZ,OAAO,IAAI,MAAM,WAAW,WAAW;AAAA,MACvC;AAAA,IACF;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,KAAK,QAAQ,GAAG,OAAO,MAAM;AAAA,EACxC,UAAE;AACA,YAAQ;AAAA,EACV;AACF;AAUA,SAAS,WAAW,aAA0C;AAC5D,MAAI,CAAC,YAAa,QAAO;AACzB,SACE,YAAY,WAAW,QAAQ,KAC/B,gBAAgB,qBAChB,gBAAgB;AAEpB;;;AC/BO,SAAS,oBAAoB,YAAwB,MAAuB;AACjF,MAAI,UAAmB;AACvB,aAAW,WAAW,KAAK,MAAM,GAAG,GAAG;AACrC,QAAI,YAAY,QAAQ,YAAY,OAAW,QAAO;AACtD,QAAI,OAAO,YAAY,SAAU,QAAO;AACxC,cAAW,QAAoC,OAAO;AAAA,EACxD;AACA,SAAO,WAAW;AACpB;AAGO,SAAS,YACd,YACA,OACyB;AACzB,QAAM,SAAkC,CAAC;AACzC,aAAW,QAAQ,MAAO,QAAO,IAAI,IAAI,oBAAoB,YAAY,IAAI;AAC7E,SAAO;AACT;AAQO,SAAS,kBACd,YACA,OACmB;AACnB,SAAO,MACJ,OAAO,CAAC,SAAS,KAAK,YAAY,UAAU,KAAK,IAAI,EACrD,IAAI,CAAC,UAAU;AAAA,IACd,QAAQ,KAAK;AAAA,IACb,MAAM;AAAA,IACN,OAAO,KAAK;AAAA,IACZ,OAAO,KAAK;AAAA,IACZ,SAAS,KAAK;AAAA,IACd,UAAU,YAAY,YAAY,KAAK,KAAK;AAAA,IAC5C,SAAS;AAAA,EACX,EAAE;AACN;;;ACvDO,IAAM,kBAAkB;AAQxB,IAAM,WAA8C;AAAA,EACzD,SAAS;AAAA,IACP,MAAM;AAAA,IACN,aAAa;AAAA,EACf;AAAA,EAEA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA,IAKb,SAAS;AAAA,MACP,iBAAiB,EAAE,UAAU,QAAQ;AAAA,MACrC,oBAAoB,EAAE,UAAU,QAAQ;AAAA,MACxC,eAAe,EAAE,UAAU,QAAQ;AAAA,MACnC,WAAW,EAAE,UAAU,UAAU;AAAA,MACjC,WAAW,EAAE,UAAU,UAAU;AAAA,IACnC;AAAA,EACF;AAAA,EAEA,aAAa;AAAA,IACX,MAAM;AAAA,IACN,aAAa;AAAA;AAAA;AAAA;AAAA,IAIb,SAAS;AAAA,MACP,WAAW,EAAE,SAAS,MAAM;AAAA,IAC9B;AAAA,EACF;AAAA,EAEA,WAAW;AAAA,IACT,MAAM;AAAA,IACN,aAAa;AAAA;AAAA;AAAA;AAAA,IAIb,OAAO;AAAA,MACL,uBAAuB,EAAE,UAAU,QAAQ;AAAA,MAC3C,sBAAsB,EAAE,UAAU,UAAU;AAAA,MAC5C,oBAAoB,EAAE,UAAU,QAAQ;AAAA,MACxC,oBAAoB,EAAE,UAAU,QAAQ;AAAA,MACxC,0BAA0B,EAAE,UAAU,UAAU;AAAA,IAClD;AAAA,EACF;AACF;AAGO,IAAM,gBAAmC,OAAO,KAAK,QAAQ;AAQ7D,SAAS,eAAe,OAAe,iBAA0B;AACtE,QAAM,UAAU,SAAS,IAAI;AAC7B,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,oBAAoB,IAAI,6BAAwB,cAAc,KAAK,IAAI,CAAC,EAAE;AAAA,EAC5F;AACA,SAAO;AACT;AAGA,SAAS,YAAY,MAAY,SAAgC;AAC/D,SAAO,EAAE,GAAG,QAAQ,UAAU,KAAK,KAAK,GAAG,GAAG,QAAQ,QAAQ,KAAK,EAAE,EAAE;AACzE;AAGA,SAAS,aAAa,MAAY,UAA0B;AAC1D,MAAI,KAAK,SAAS,UAAW,QAAO,EAAE,GAAG,MAAM,SAAS;AACxD,SAAO,EAAE,GAAG,MAAM,gBAAgB,EAAE,YAAY,UAAU,MAAM,SAAS,EAAE;AAC7E;AAQO,SAAS,aAAa,OAA4B,SAAuC;AAC9F,QAAM,YAAoB,CAAC;AAC3B,aAAW,QAAQ,OAAO;AACxB,UAAM,WAAW,YAAY,MAAM,OAAO;AAC1C,QAAI,SAAS,YAAY,MAAO;AAChC,cAAU,KAAK,SAAS,WAAW,aAAa,MAAM,SAAS,QAAQ,IAAI,IAAI;AAAA,EACjF;AACA,SAAO;AACT;AAOO,SAAS,gBACd,MACA,QAA6B,OACR;AACrB,SAAO,aAAa,OAAO,eAAe,IAAI,CAAC;AACjD;;;AC1GA,IAAM,mBAA8B;AAAA,EAClC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAGF,OAAO;AAAA,EACP,SAAS,CAAC,qBAAqB,2BAA2B;AAAA,EAC1D,OAAO,CAAC,kBAAkB,oBAAoB,mBAAmB,eAAe;AAAA,EAChF,SAAS,CAAC,iBAAiB,cAAc;AAAA,EACzC,WAAW,CAAC,OAAO,QAAQ,GAAG,SAAS,OAAO,OAAO,KAAK,CAAC;AAAA,EAC3D,OACE;AAGJ;AAEA,IAAM,sBAAiC;AAAA,EACrC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAIF,OAAO;AAAA,EACP,SAAS,CAAC,kBAAkB,sBAAsB;AAAA,EAClD,OAAO,CAAC,oBAAoB,kBAAkB,eAAe;AAAA,EAC7D,SAAS,CAAC,uBAAuB,oBAAoB;AAAA,EACrD,WAAW,CAAC,OAAO,QAAQ,GAAG,KAAK,aAAa,OAAO,KAAK,CAAC;AAAA,EAC7D,OACE;AAEJ;AAEA,IAAM,qBAAgC;AAAA,EACpC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAKF,OAAO;AAAA,EACP,SAAS,CAAC,oBAAoB,wBAAwB;AAAA,EACtD,OAAO,CAAC,iBAAiB,kBAAkB,oBAAoB,kBAAkB;AAAA,EACjF,WAAW,CAAC,OAAO,QAAQ,GAAG,SAAS,MAAM,OAAO,KAAK,CAAC;AAAA,EAC1D,OACE;AACJ;AAEA,IAAM,wBAAmC;AAAA,EACvC,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,UAAU;AAAA,EACV,OAAO;AAAA,EACP,KACE;AAAA,EAIF,OAAO;AAAA,EACP,SAAS,CAAC,OAAO,iBAAiB;AAAA,EAClC,OAAO,CAAC,oBAAoB,mBAAmB,kBAAkB,eAAe;AAAA,EAChF,SAAS,CAAC,kBAAkB;AAAA,EAC5B,WAAW,CAAC,OAAO,GAAG,UAAU,OAAO,SAAS;AAAA,EAChD,OACE;AAGJ;AAGO,IAAM,cAAwC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAM,mBAA2C,IAAI,IAAI,YAAY,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;;;ACvB1F,IAAM,0BAAyC;AAAA,EAC7C,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,OACE;AAAA,EAEF,KACE;AAAA,EAGF,WAAW,CAAC,SAAS,KAAK,WAAW,iBAAiB,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC1F,KAAK,CAAC,SAAS;AACb,UAAM,OAAO,IAAI,IAAI,KAAK,WAAW,QAAQ,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AACxE,UAAM,YAAY,oBAAI,IAAyB;AAE/C,eAAW,SAAS,KAAK,WAAW,QAAQ,CAAC,GAAG;AAC9C,UAAI;AACJ,UAAI;AACF,mBAAW,IAAI,IAAI,MAAM,GAAG,EAAE;AAAA,MAChC,QAAQ;AACN;AAAA,MACF;AACA,YAAM,EAAE,OAAO,IAAI,WAAW,QAAQ;AACtC,UAAI,WAAW,YAAa;AAG5B,UAAI,CAAC,KAAK,IAAI,OAAO,YAAY,CAAC,EAAG;AACrC,YAAM,QAAQ,KAAK,iBAAiB,MAAM,GAAG,KAAK,WAAW,QAAQ,EAAE;AACvE,YAAM,MAAM,UAAU,IAAI,KAAK,KAAK,oBAAI,IAAY;AACpD,UAAI,IAAI,OAAO,YAAY,CAAC;AAC5B,gBAAU,IAAI,OAAO,GAAG;AAAA,IAC1B;AAEA,UAAM,SAA8B,CAAC;AAErC,eAAW,QAAQ,KAAK,OAAO;AAC7B,YAAM,OAAO,oBAAI,IAAY;AAC7B,iBAAW,OAAO,KAAK,MAAM,YAAY;AACvC,YAAI,IAAI,WAAY;AACpB,cAAM,MAAM,IAAI,UAAU,KAAK,EAAE,YAAY;AAC7C,YAAI,IAAK,MAAK,IAAI,GAAG;AAAA,MACvB;AACA,UAAI,KAAK,SAAS,EAAG;AAErB,UAAI;AACJ,UAAI;AACF,mBAAW,IAAI,IAAI,KAAK,MAAM,QAAQ,EAAE;AAAA,MAC1C,QAAQ;AACN;AAAA,MACF;AACA,YAAM,QAAQ,KAAK,iBAAiB,KAAK,MAAM,QAAQ,KAAK,WAAW,QAAQ,EAAE;AACjF,YAAM,YAAY,UAAU,IAAI,KAAK;AACrC,UAAI,CAAC,aAAa,UAAU,SAAS,EAAG;AAExC,YAAM,WAAW,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC,EAAE,KAAK;AACjE,UAAI,SAAS,WAAW,EAAG;AAE3B,aAAO,KAAK;AAAA,QACV,SAAS,KAAK,MAAM;AAAA,QACpB,UAAU;AAAA,UACR;AAAA;AAAA;AAAA,UAGA,gBAAgB,CAAC,GAAG,IAAI,EAAE,KAAK;AAAA,UAC/B,cAAc,CAAC,GAAG,SAAS,EAAE,KAAK;AAAA,UAClC,uBAAuB;AAAA,QACzB;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AACF;AAEO,IAAM,mBAA6C,CAAC,uBAAuB;AAe3E,SAAS,sBACd,MACA,QAAkC,kBACS;AAC3C,QAAM,MAAiD,CAAC;AAExD,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,aAAa,CAAC,KAAK,UAAU,IAAI,EAAG;AAC7C,eAAW,SAAS,KAAK,IAAI,IAAI,GAAG;AAClC,UAAI,KAAK;AAAA,QACP,QAAQ,KAAK;AAAA,QACb,MAAM;AAAA,QACN,OAAO,KAAK;AAAA;AAAA;AAAA,QAGZ,OAAO,KAAK,SAAS;AAAA,QACrB,SAAS,KAAK,WAAW,CAAC;AAAA,QAC1B,UAAU,MAAM;AAAA,QAChB,SAAS;AAAA,QACT,SAAS,MAAM;AAAA,MACjB,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;;;ACtLA,YAAYC,cAAa;AAiBzB,IAAM,cAA2B,EAAE,UAAU,WAAW,MAAM,QAAQ,QAAQ,IAAI;AAClF,IAAM,cAA6B;AAAA,EACjC,EAAE,UAAU,YAAY,MAAM,OAAO,QAAQ,MAAM;AAAA,EACnD,EAAE,UAAU,eAAe,MAAM,OAAO,QAAQ,SAAS;AAAA,EACzD,EAAE,UAAU,cAAc,MAAM,QAAQ,QAAQ,OAAO;AAAA,EACvD,EAAE,UAAU,eAAe,MAAM,OAAO,QAAQ,SAAS;AAC3D;AASO,SAAS,aAAa,MAAc,SAAyC;AAClF,QAAM,IAAY,cAAK,IAAI;AAG3B,QAAM,OAAOC,aAAY,GAAG,QAAQ,OAAO;AAC3C,QAAM,SAAS,WAAW,QAAQ,OAAO;AACzC,QAAM,QAAQ,QAAQ,gBAAgB,CAAC,aAAa,GAAG,WAAW,IAAI,CAAC,WAAW;AAElF,QAAM,MAAiB,CAAC;AACxB,QAAM,OAAO,oBAAI,IAAY;AAE7B,aAAW,QAAQ,OAAO;AACxB,MAAE,KAAK,QAAQ,EAAE,KAAK,CAAC,GAAG,OAAO;AAC/B,YAAM,MAAM,EAAE,EAAE,EAAE,KAAK,KAAK,IAAI;AAChC,UAAI,QAAQ,OAAW;AACvB,YAAM,UAAU,IAAI,KAAK;AACzB,UAAI,CAAC,QAAS;AAEd,YAAM,MAAM,SAAS,GAAG,IAAI,SAAS,MAAM,MAAM,MAAM;AACvD,UAAI,CAAC,IAAK;AAEV,YAAM,YAAY,GAAG,IAAI,MAAM,IAAI,IAAI,GAAG;AAC1C,UAAI,KAAK,IAAI,SAAS,EAAG;AACzB,WAAK,IAAI,SAAS;AAClB,UAAI,KAAK,GAAG;AAAA,IACd,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAEA,SAAS,SACP,GACA,IACA,SACA,MACA,MACA,QACgB;AAChB,QAAM,WAAW,gBAAgB,OAAO;AAIxC,QAAM,YAAY,gBAAgB,SAAS,MAAM,EAAE,uBAAuB,KAAK,CAAC;AAKhF,QAAM,MAAM,aAAa;AACzB,QAAM,OAAiB,YAAY,SAAS,WAAW,MAAM,IAAI;AAEjE,QAAM,MAAe;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ,KAAK;AAAA,IACb,KAAK,KAAK,WAAW,OAAO,KAAK,WAAW,SAAS,SAAS,EAAE,EAAE,EAAE,KAAK,KAAK,CAAC,IAAI,CAAC;AAAA,EACtF;AAEA,MAAI,KAAK,WAAW,KAAK;AACvB,UAAM,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK;AAC/B,QAAI,KAAM,KAAI,aAAa;AAAA,EAC7B;AACA,MAAI,SAAU,KAAI,WAAW;AAE7B,SAAO;AACT;AAGA,SAAS,gBAAgB,SAAqC;AAC5D,QAAM,OAAO,QAAQ,QAAQ,GAAG;AAChC,MAAI,SAAS,GAAI,QAAO;AACxB,QAAM,OAAO,QAAQ,MAAM,IAAI;AAC/B,SAAO,KAAK,SAAS,IAAI,OAAO;AAClC;AAEA,SAAS,SAAS,KAAmC;AACnD,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,SAAO,IACJ,MAAM,KAAK,EACX,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,EACjC,OAAO,OAAO;AACnB;AAEA,SAAS,SAAS,KAAa,QAAiC;AAC9D,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,WAAW,GAAG,MAAM,SAAS,aAAa;AACnD;AAEA,SAAS,WAAW,KAA4B;AAC9C,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOA,SAASA,aAAY,GAAuB,SAAyB;AACnE,QAAM,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,MAAM,GAAG,KAAK;AAC5D,MAAI,UAAU;AACZ,QAAI;AACF,aAAO,IAAI,IAAI,UAAU,OAAO,EAAE,SAAS;AAAA,IAC7C,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;;;ACtDO,SAAS,sBAAmD;AACjE,SAAO,EAAE,IAAI,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,EAAE;AAC7E;;;ACrDA,IAAMC,YAAW;AAAA,EACf,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,UAAU;AAAA,EACV,UAAU;AAAA,EACV,cAAc,IAAI,OAAO;AAC3B;AAEA,eAAsB,aACpB,WACA,UAA4B,CAAC,GACH;AAC1B,QAAM,WAAW,QAAQ,YAAYA,UAAS;AAC9C,QAAM,WAAW,QAAQ,YAAYA,UAAS;AAC9C,QAAM,gBAAgB,QAAQ,kBAAkB;AAEhD,QAAM,WAAqB,CAAC;AAC5B,MAAI,YAAY;AAGhB,QAAM,WAAW,UAAU,KAAK,IAAI,CAAC,MAAM,EAAE,GAAG;AAChD,QAAM,WAAW,SAAS,MAAM,GAAG,QAAQ;AAC3C,MAAI,SAAS,SAAS,UAAU;AAC9B,gBAAY;AACZ,aAAS,KAAK,YAAY,SAAS,MAAM,0BAA0B,QAAQ,gBAAgB;AAAA,EAC7F;AAGA,QAAM,cAAgC,CAAC;AACvC,MAAI,eAAe;AACnB,MAAI,cAAc;AAClB,MAAI,WAAW;AAEf,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,MACE,aAAa,QAAQ,mBAAmBA,UAAS;AAAA,MACjD,SAAS;AAAA,MACT,QAAQ,QAAQ;AAAA,IAClB;AAAA,IACA,OAAO,YAAY;AACjB,YAAM,MAAM,MAAM,SAAS,SAAS;AAAA,QAClC,QAAQ;AAAA,QACR,WAAW,QAAQ,aAAaA,UAAS;AAAA,QACzC,kBAAkB,QAAQ;AAAA,QAC1B,WAAW,QAAQ;AAAA,QACnB,UAAU,QAAQ,gBAAgBA,UAAS;AAAA,QAC3C,QAAQ,QAAQ;AAAA,MAClB,CAAC;AACD,UAAI,IAAI,WAAW,KAAK,IAAI,UAAU,OAAO,IAAI,SAAS,QAAW;AACnE,uBAAe;AAAA,MACjB,OAAO;AACL,wBAAgB;AAChB,cAAM,OAAO,aAAa,IAAI,MAAM;AAAA,UAClC,SAAS,IAAI;AAAA,UACb,eAAe,QAAQ;AAAA,QACzB,CAAC;AACD,mBAAW,OAAO,KAAM,aAAY,KAAK,EAAE,SAAS,IAAI,CAAC;AAAA,MAC3D;AACA,kBAAY;AACZ,cAAQ,aAAa,EAAE,OAAO,QAAQ,MAAM,UAAU,OAAO,SAAS,OAAO,CAAC;AAAA,IAChF;AAAA,IACA,MAAM;AAAA,EACR;AAGA,QAAM,aAAuB,CAAC;AAC9B,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,OAAO,aAAa;AAC7B,UAAM,MAAM,IAAI,IAAI;AACpB,QAAI,KAAK,IAAI,GAAG,EAAG;AACnB,QAAI,CAAC,iBAAiB,IAAI,IAAI,SAAS,WAAY;AACnD,QAAI,WAAW,UAAU,UAAU;AACjC,kBAAY;AACZ,eAAS,KAAK,aAAa,QAAQ,sDAAsD;AACzF;AAAA,IACF;AACA,SAAK,IAAI,GAAG;AACZ,eAAW,KAAK,GAAG;AAAA,EACrB;AAGA,QAAM,SAAoC,CAAC;AAC3C,MAAI,YAAY;AAEhB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,MACE,aAAa,QAAQ,oBAAoBA,UAAS;AAAA,MAClD,SAAS,QAAQ,cAAcA,UAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAYxC,eAAe,OAAO,UAAU,MAAM;AAAA,MACtC,QAAQ,QAAQ;AAAA,IAClB;AAAA,IACA,OAAO,QAAQ;AACb,YAAM,QAAQ,MAAM,UAAU,KAAK;AAAA,QACjC,WAAW,QAAQ,aAAaA,UAAS;AAAA,QACzC,kBAAkB,QAAQ;AAAA,QAC1B,WAAW,QAAQ;AAAA,QACnB,QAAQ,QAAQ;AAAA,QAChB,OAAO,QAAQ;AAAA,MACjB,CAAC;AACD,aAAO,GAAG,IAAI;AACd,mBAAa;AACb,cAAQ,aAAa,EAAE,OAAO,SAAS,MAAM,WAAW,OAAO,WAAW,OAAO,CAAC;AAAA,IACpF;AAAA,IACA,CAAC,QAAQ,OAAO,GAAG;AAAA,EACrB;AAGA,QAAM,UAAU,oBAAoB;AACpC,aAAW,SAAS,OAAO,OAAO,MAAM,EAAG,SAAQ,MAAM,OAAO,KAAK;AAErE,QAAM,eAAe,kBAAkB,aAAa,MAAM;AAE1D,SAAO;AAAA,IACL,QAAQ,UAAU;AAAA,IAClB,SAAS,UAAU;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,EAAE,aAAa,SAAS;AAAA,EACvC;AACF;AAGA,SAAS,kBACP,aACA,QACiC;AACjC,QAAM,SAAS,oBAAI,IAAoC;AACvD,aAAW,OAAO,aAAa;AAC7B,UAAM,QAAQ,OAAO,IAAI,IAAI,GAAG;AAChC,QAAI,CAAC,MAAO;AACZ,QAAI,MAAM,YAAY,QAAQ,MAAM,YAAY,cAAc,MAAM,YAAY,WAAW;AACzF;AAAA,IACF;AACA,QAAI,OAAO,OAAO,IAAI,IAAI,OAAO;AACjC,QAAI,CAAC,MAAM;AACT,aAAO,oBAAI,IAAI;AACf,aAAO,IAAI,IAAI,SAAS,IAAI;AAAA,IAC9B;AACA,SAAK,IAAI,MAAM,KAAK,KAAK;AAAA,EAC3B;AACA,SAAO,MAAM,KAAK,OAAO,QAAQ,CAAC,EAC/B,IAAI,CAAC,CAAC,SAAS,GAAG,OAAO,EAAE,SAAS,QAAQ,MAAM,KAAK,IAAI,OAAO,CAAC,EAAE,EAAE,EACvE,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,SAAS,EAAE,OAAO,MAAM;AACrD;AAEA,SAAS,OAAO,KAAqB;AACnC,MAAI;AACF,WAAO,IAAI,IAAI,GAAG,EAAE;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAqBA,SAAS,gBACP,OACA,MACA,QACAC,SACe;AACf,QAAM,cAAc,KAAK,IAAI,GAAG,KAAK,WAAW;AAChD,QAAM,UAAU,KAAK;AACrB,SAAO,IAAI,QAAc,CAACC,aAAY;AACpC,QAAI,MAAM,WAAW,GAAG;AACtB,MAAAA,SAAQ;AACR;AAAA,IACF;AACA,UAAM,UAAU,MAAM,IAAI,CAAC,UAAU,EAAE,MAAM,MAAMD,QAAO,IAAI,EAAE,EAAE;AAClE,UAAM,aAAa,oBAAI,IAAoB;AAC3C,QAAI,WAAW;AACf,QAAI,UAAU;AAEd,UAAM,SAAS,MAAM;AACnB,UAAI,CAAC,SAAS;AACZ,kBAAU;AACV,QAAAC,SAAQ;AAAA,MACV;AAAA,IACF;AAEA,UAAM,OAAO,MAAM;AACjB,UAAI,QAAS;AACb,UAAI,KAAK,QAAQ,SAAS;AACxB,YAAI,aAAa,EAAG,QAAO;AAC3B;AAAA,MACF;AACA,aAAO,WAAW,aAAa;AAC7B,YAAI,YAAY;AAChB,iBAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,gBAAM,QAAQ,QAAQ,CAAC;AACvB,gBAAM,MAAM,MAAM,SAAS,KAAK,gBAAgB,WAAW;AAC3D,eAAK,WAAW,IAAI,MAAM,IAAI,KAAK,KAAK,KAAK;AAC3C,wBAAY;AACZ;AAAA,UACF;AAAA,QACF;AACA,YAAI,cAAc,GAAI;AACtB,cAAM,CAAC,MAAM,IAAI,QAAQ,OAAO,WAAW,CAAC;AAC5C,cAAM,OAAO,OAAQ;AACrB,mBAAW,IAAI,OAAO,WAAW,IAAI,IAAI,KAAK,KAAK,CAAC;AACpD,oBAAY;AACZ,aAAK,OAAO,OAAQ,IAAI,EACrB,MAAM,MAAM,MAAS,EACrB,QAAQ,MAAM;AACb,sBAAY;AACZ,qBAAW,IAAI,OAAO,WAAW,IAAI,IAAI,KAAK,KAAK,CAAC;AACpD,cAAI,QAAQ,WAAW,KAAK,aAAa,EAAG,QAAO;AAAA,cAC9C,MAAK;AAAA,QACZ,CAAC;AAAA,MACL;AACA,UAAI,QAAQ,WAAW,KAAK,aAAa,EAAG,QAAO;AAAA,IACrD;AAEA,SAAK;AAAA,EACP,CAAC;AACH;;;AC9SA,IAAM,iBAAiB;AAYhB,SAAS,kBAAkB,OAAmC;AACnE,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO,EAAE,IAAI,MAAM;AAEjC,QAAM,cAAc,QAAQ,MAAM,cAAc;AAChD,MAAI;AACJ,MAAI,aAAa;AACf,UAAM,UAAU,YAAY,CAAC,KAAK,IAAI,YAAY;AAElD,QAAI,WAAW,UAAU,WAAW,QAAS,QAAO,EAAE,IAAI,MAAM;AAChE,gBAAY;AAAA,EACd,OAAO;AAGL,gBAAY,WAAW,OAAO;AAAA,EAChC;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,SAAS;AAAA,EAC5B,QAAQ;AACN,WAAO,EAAE,IAAI,MAAM;AAAA,EACrB;AAEA,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,SAAU,QAAO,EAAE,IAAI,MAAM;AACpF,MAAI,CAAC,OAAO,SAAU,QAAO,EAAE,IAAI,MAAM;AAEzC,SAAO,EAAE,IAAI,MAAM,KAAK,UAAU;AACpC;;;AC6BO,SAAS,iBACd,OACA,MACiB;AACjB,QAAM,SAAS,IAAI;AAAA,IACjB,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,CAAC,CAAC;AAAA,EACjF;AAEA,QAAM,QAA2B,KAAK,IAAI,CAAC,QAAQ;AACjD,UAAM,WAA8C,CAAC;AACrD,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,WAAW,IAAI,UAAU;AAClC,YAAM,QAAQ,OAAO,IAAI,QAAQ,MAAM;AACvC,UAAI,CAAC,MAAO;AACZ,eAAS,QAAQ,MAAM,IAAI,QAAQ;AACnC,YAAM,QAAQ,MAAM,KAAK;AACzB,WAAK,IAAI,QAAQ,MAAM;AAAA,IACzB;AAGA,eAAW,QAAQ,MAAO,KAAI,CAAC,KAAK,IAAI,KAAK,EAAE,EAAG,QAAO,IAAI,KAAK,EAAE,EAAG,WAAW;AAClF,WAAO,EAAE,KAAK,IAAI,SAAS,SAAS;AAAA,EACtC,CAAC;AAED,SAAO;AAAA,IACL,OAAO,MAAM,IAAI,CAAC,UAAU;AAAA,MAC1B,QAAQ,KAAK;AAAA,MACb,MAAM,KAAK;AAAA,MACX,OAAO,KAAK;AAAA,MACZ,OAAO,KAAK;AAAA,MACZ,SAAS,KAAK;AAAA,MACd,UAAU,KAAK;AAAA,MACf,QAAQ,OAAO,IAAI,KAAK,EAAE;AAAA,IAC5B,EAAE;AAAA,IACF;AAAA,EACF;AACF;;;ACjDA,SAAS,WAAW,QAAyB;AAC3C,SAAO,UAAU,OAAO,SAAS;AACnC;AAcA,SAAS,WAAW,MAAqB;AACvC,QAAM,OAAO,KAAK,MAAM,aAAa,KAAK,EAAE,YAAY;AACxD,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,SAAS,eAAe,SAAS;AAC1C;AA4GA,SAAS,YAAY,KAA4B;AAC/C,MAAI;AACF,UAAM,EAAE,SAAS,IAAI,IAAI,IAAI,GAAG;AAChC,UAAM,QAAQ,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO,EAAE,CAAC;AACnD,QAAI,SAAS,uBAAuB,KAAK,EAAG,QAAO;AAAA,EACrD,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAmBO,SAAS,yBAAyB,OAAuC;AAC9E,QAAM,UAAU,MAAM,OAAO,CAAC,MAAM,EAAE,UAAU,iBAAiB;AACjE,MAAI,QAAQ,WAAW,EAAG,QAAO;AAIjC,SACE,GAAG,QAAQ,MAAM,mHACkC,QAAQ,CAAC,EAAG,UAAU,iBAAiB;AAI9F;AAkBO,SAAS,uBACd,QACA,eAAkC,CAAC,GAChB;AACnB,QAAM,cAAc,OAAO,QAAQ,OAAO,CAAC,MAAM,MAAM,WAAW;AAClE,MAAI,YAAY,SAAS,EAAG,QAAO,CAAC;AAEpC,QAAM,QAA2B,CAAC;AAClC,aAAW,SAAS,OAAO,QAAQ;AACjC,QAAI,aAAa,SAAS,KAAK,WAAW,OAAO,CAAC,GAAG,YAAY,CAAC,EAAG;AACrE,UAAM,UAAoB,CAAC;AAC3B,UAAM,UAAoB,CAAC;AAC3B,eAAW,UAAU,aAAa;AAChC,YAAM,OAAO,OAAO,MAAM,KAAK,IAAI,MAAM;AACzC,UAAI,QAAQ,KAAK,IAAK,SAAQ,KAAK,MAAM;AAAA,UACpC,SAAQ,KAAK,MAAM;AAAA,IAC1B;AACA,QAAI,QAAQ,SAAS,KAAK,QAAQ,SAAS,GAAG;AAC5C,YAAM,KAAK;AAAA,QACT,IAAI,YAAY,QAAQ,QAAQ,KAAK;AAAA,QACrC;AAAA,QACA,gBAAgB;AAAA,QAChB,gBAAgB;AAAA,MAClB,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAqBO,SAAS,SAAS,QAAsB,SAAiB,WAAmB;AACjF,MAAI,WAAW,QAAS,QAAO;AAC/B,MAAI,OAAO,QAAQ,YAAY,QAAS,QAAO;AAC/C,MAAI,WAAW,QAAS,QAAO,OAAO,QAAQ,YAAY,QAAQ,IAAI;AACtE,SAAO;AACT;AA+BA,eAAe,gBACb,OACA,SACe;AACf,QAAM,SAAS,IAAI;AAAA,IACjB,MAAM,IAAI,CAAC,SAAS,KAAK,MAAM,UAAU,IAAI,EAAE,OAAO,CAAC,SAAyB,QAAQ,IAAI,CAAC;AAAA,EAC/F;AACA,MAAI,OAAO,SAAS,EAAG;AAEvB,QAAM,SAAS,oBAAI,IAA2B;AAC9C,QAAM,QAAQ;AAAA,IACZ,CAAC,GAAG,MAAM,EAAE,IAAI,OAAO,SAAS;AAC9B,YAAM,QAAQ,MAAM,cAAc,MAAM,OAAO,EAAE,MAAM,MAAM,MAAS;AACtE,UAAI,MAAO,QAAO,IAAI,MAAM,KAAK;AAAA,IACnC,CAAC;AAAA,EACH;AAEA,aAAW,QAAQ,OAAO;AACxB,UAAM,OAAO,KAAK,MAAM,UAAU;AAClC,UAAM,QAAQ,OAAO,OAAO,IAAI,IAAI,IAAI;AACxC,QAAI,MAAO,MAAK,SAAS,EAAE,GAAG,KAAK,QAAQ,UAAU,MAAM;AAAA,EAC7D;AACF;AAuBA,eAAe,YACb,OACA,SAOe;AACf,QAAMC,cAAa,CAAC,QAAyB;AAC3C,QAAI;AACF,aAAO,IAAI,IAAI,GAAG,EAAE,WAAW,IAAI,IAAI,QAAQ,MAAM,EAAE;AAAA,IACzD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,QAAM,YAAY,CAAC,QACjB,gBAAgB,KAAK,GAAG,MAAM,QAAQ,iBAAiBA,YAAW,GAAG;AACvE,QAAM,SAAS,oBAAI,IAAY;AAC/B,aAAW,QAAQ,OAAO;AACxB,eAAW,SAAS,KAAK,UAAU,QAAQ;AACzC,YAAM,MAAM,MAAM,IAAI,MAAM,KAAK;AACjC,UAAI,UAAU,GAAG,EAAG,QAAO,IAAI,GAAG;AAAA,IACpC;AACA,eAAW,QAAQ,KAAK,MAAM,OAAO;AACnC,UAAI,UAAU,KAAK,IAAI,EAAG,QAAO,IAAI,KAAK,IAAI;AAAA,IAChD;AAAA,EACF;AACA,MAAI,OAAO,SAAS,EAAG;AAEvB,QAAM,QAAQ,CAAC,GAAG,MAAM;AACxB,QAAM,SAAS,oBAAI,IAAwB;AAC3C,QAAM,UAAU,KAAK,IAAI,QAAQ,eAAe,GAAG,MAAM,MAAM;AAE/D,QAAM,QAAQ;AAAA,IACZ,MAAM,KAAK,EAAE,QAAQ,QAAQ,GAAG,YAAY;AAC1C,eAAS,MAAM,MAAM,IAAI,GAAG,QAAQ,QAAW,MAAM,MAAM,IAAI,GAAG;AAChE,cAAM,QAAQ,MAAM,WAAW,KAAK;AAAA,UAClC,QAAQ,QAAQ;AAAA,UAChB,WAAW,QAAQ;AAAA,QACrB,CAAC,EAAE,MAAM,MAAM,MAAS;AACxB,YAAI,MAAO,QAAO,IAAI,KAAK,KAAK;AAAA,MAClC;AAAA,IACF,CAAC;AAAA,EACH;AAEA,aAAW,QAAQ,OAAO;AACxB,UAAM,OAAmC,CAAC;AAC1C,eAAW,SAAS,KAAK,UAAU,QAAQ;AACzC,YAAM,QAAQ,OAAO,IAAI,MAAM,IAAI,MAAM,KAAK,CAAC;AAC/C,UAAI,MAAO,MAAK,MAAM,GAAG,IAAI;AAAA,IAC/B;AACA,eAAW,QAAQ,KAAK,MAAM,OAAO;AACnC,YAAM,QAAQ,OAAO,IAAI,KAAK,IAAI;AAClC,UAAI,MAAO,MAAK,MAAM,GAAG,IAAI;AAAA,IAC/B;AAGA,SAAK,SAAS;AAAA,EAChB;AACF;AAEA,SAAS,wBAAwB,OAAkD;AACjF,QAAM,UAAU,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,SAAS,EAAE,MAAM,QAAQ,CAAC,CAAC;AAEpE,QAAM,OAAO,MAAM,OAAO,CAAC,SAAS;AAClC,UAAM,YAAY,KAAK,MAAM;AAC7B,QAAI,CAAC,UAAW,QAAO;AAEvB,UAAM,OAAO,SAAS,KAAK,MAAM,QAAQ;AACzC,QAAI;AACJ,QAAI;AACF,eAAS,SAAS,IAAI,IAAI,WAAW,KAAK,MAAM,QAAQ,EAAE,SAAS,CAAC;AAAA,IACtE,QAAQ;AACN,aAAO;AAAA,IACT;AAEA,QAAI,WAAW,KAAM,QAAO;AAC5B,WAAO,CAAC,QAAQ,IAAI,MAAM;AAAA,EAC5B,CAAC;AAED,SAAO,EAAE,MAAM,SAAS,MAAM,SAAS,KAAK,OAAO;AACrD;AAGA,eAAsB,SACpB,UACA,UAAwB,CAAC,GACF;AACvB,QAAM,aAAa,kBAAkB,QAAQ;AAC7C,MAAI,CAAC,WAAW,IAAI;AAClB,UAAM,IAAI,MAAM,IAAI,QAAQ,+BAA+B;AAAA,EAC7D;AACA,QAAM,QAAQ,WAAW;AACzB,QAAM,SAAS,IAAI,IAAI,KAAK,EAAE;AAE9B,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,WAAW,QAAQ,YAAY;AACrC,QAAM,gBAAgB,QAAQ,kBAAkB;AAKhD,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,QAAQ,gBAAgB,OAAO;AAOrC,QAAM,YAAY,QAAQ,YACtB,SACA,MAAM,gBAAgB,OAAO;AAAA,IAC3B,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,IACnB,kBAAkB,QAAQ;AAAA,IAC1B,eAAe;AAAA,EACjB,CAAC,EAAE,MAAM,MAAM,MAAS;AAE5B,QAAM,kBAAkB,WAAW,QAAQ,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG;AAS/D,QAAM,kBAAkB,kBAAkB,WAAW,QAAQ,CAAC,CAAC;AAM/D,QAAM,eAAe,QAAQ,aAAa,eAAe,SAAS,IAAI,eAAe;AACrF,QAAM,YACJ,iBAAiB,gBAAgB,eAAe,SAAS,IACrD,kBAAkB,gBAAgB,EAAE,SAAS,QAAQ,QAAQ,CAAC,IAC9D;AACN,QAAM,cAAc,WAAW,QAAQ;AAgBvC,QAAM,oBAAoB,YACtB,KAAK,IAAI,QAAQ,YAAY,UAAU,UAAU,KAAK,SAAS,CAAC,IAChE;AAKJ,QAAM,SAAS,MAAM,YAAY,QAAQ;AAAA,IACvC,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,EACrB,CAAC,EAAE,MAAM,MAAM,MAAS;AAKxB,QAAMC,WAAU,MAAM,aAAa,QAAQ;AAAA,IACzC,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,EACrB,CAAC,EAAE,MAAM,MAAM,MAAS;AAGxB,QAAM,cAAc,MAAM,MAAM;AAAA,IAC9B,UAAU;AAAA,IACV,UAAU;AAAA,IACV;AAAA,IACA,UAAU;AAAA,IACV,SAAS,QAAQ;AAAA,IACjB,SAAS,QAAQ;AAAA,IACjB,gBAAgB;AAAA,MACd,MAAM,QAAQ,SAAS,WAAW;AAAA,MAClC,QAAQ;AAAA,MACR,kBAAkB,QAAQ;AAAA,MAC1B,WAAW,QAAQ;AAAA,MACnB,QAAQ,QAAQ;AAAA,IAClB;AAAA,IACA,QAAQ,CAAC,MAAM,QACb,QAAQ,aAAa;AAAA,MACnB,OAAO;AAAA,MACP,MAAM,IAAI;AAAA,MACV,OAAO,IAAI,UAAU,IAAI;AAAA,MACzB,KAAK,KAAK,MAAM;AAAA,MAChB,QAAQ,KAAK,MAAM;AAAA,IACrB,CAAC;AAAA,EACL,CAAC;AAED,QAAM,QAAgB,YAAY;AAClC,QAAM,WAAW,YAAY,OAAO,IAAI,CAAC,MAAM,UAAU,EAAE,GAAG,WAAM,EAAE,OAAO,EAAE;AAQ/E,MAAI,YAAY,UAAU,SAAS,GAAG;AACpC,aAAS;AAAA,MACP,GAAG,YAAY,UAAU,MAAM,+CAC1B,YAAY,UAAU,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,GAC5C,YAAY,UAAU,SAAS,IAAI,MAAM,YAAY,UAAU,SAAS,CAAC,UAAU,EAAE;AAAA,IAC5F;AAAA,EACF;AAEA,QAAM,oBAAoB,yBAAyB,KAAK;AACxD,MAAI,kBAAmB,UAAS,KAAK,iBAAiB;AAKtD,QAAM,UAAU,MAAM,OAAO,CAAC,MAAM,WAAW,EAAE,MAAM,MAAM,CAAC;AAG9D,QAAM,YAAY,QAAQ,OAAO,UAAU;AAC3C,QAAM,EAAE,MAAM,WAAW,SAAS,eAAe,IAAI,wBAAwB,SAAS;AAoBtF,QAAM,mBAAsC;AAAA,IAC1C,GAAG,MACA,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,MAAM,CAAC,EACzC,IAAI,CAAC,OAAO;AAAA,MACX,IAAI,YAAY,QAAQ,SAAS,EAAE,MAAM,QAAQ,CAAC;AAAA,MAClD,KAAK,EAAE,MAAM;AAAA,MACb,QAAQ,EAAE,MAAM;AAAA,IAClB,EAAE;AAAA,IACJ,GAAG,YAAY,OAAO,IAAI,CAAC,OAAO;AAAA,MAChC,IAAI,YAAY,QAAQ,SAAS,EAAE,GAAG,CAAC;AAAA,MACvC,KAAK,EAAE;AAAA,MACP,QAAQ;AAAA,IACV,EAAE;AAAA,EACJ;AAcA,QAAM,gBAAgB,WAAW,EAAE,QAAQ,QAAQ,QAAQ,WAAW,QAAQ,UAAU,CAAC;AAMzF,QAAM,YAAY,WAAW;AAAA,IAC3B;AAAA,IACA,eAAe,QAAQ,iBAAiB;AAAA,IACxC,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,EACrB,CAAC;AAQD,QAAM,YAAwB,CAAC;AAC/B,QAAM,kBAAoC,CAAC;AAC3C,QAAM,aAA+B,CAAC;AACtC,QAAM,cAA4B,CAAC;AAGnC,QAAM,WAAW,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AAC7D,aAAW,QAAQ,WAAW;AAC5B,UAAM,UAAU,KAAK,MAAM;AAC3B,UAAM,aAAa,mBAAmB,IAAI;AAC1C,UAAM,aAAa,cAAc,YAAY,KAAK;AAClD,QAAI,QAAQ,aAAa;AACvB,kBAAY,KAAK,UAAU;AAAA,IAC7B;AACA,QAAI,QAAQ,aAAa;AACvB,sBAAgB,KAAK,EAAE,SAAS,UAAU,WAAW,SAAS,CAAC;AAAA,IACjE;AACA,QAAI,QAAQ,YAAY;AACtB,iBAAW,YAAY,kBAAkB,YAAY,WAAW,GAAG;AACjE,mBAAW,KAAK;AAAA,UACd,GAAG;AAAA,UACH,IAAI,YAAY,YAAY,SAAS,QAAQ,SAAS,OAAO,CAAC;AAAA,UAC9D;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAGA,UAAM,aAAa,oBAAI,IAAoB;AAC3C,eAAW,SAAS,WAAW,iBAAiB,YAAY,KAAK,CAAC,GAAG;AACnE,YAAM,IAAI,WAAW,IAAI,MAAM,MAAM,KAAK;AAC1C,iBAAW,IAAI,MAAM,QAAQ,IAAI,CAAC;AAClC,YAAM,OAAO,SAAS,IAAI,MAAM,MAAM;AACtC,gBAAU,KAAK;AAAA,QACb,IAAI,YAAY,OAAO,MAAM,QAAQ,SAAS,OAAO,GAAG,OAAO,CAAC,CAAC;AAAA,QACjE;AAAA,QACA,QAAQ,MAAM;AAAA,QACd,UAAU,MAAM;AAAA,QAChB,SAAS,MAAM;AAAA,QACf,KAAK,MAAM;AAAA,QACX,KAAK,MAAM,KAAK;AAAA;AAAA;AAAA;AAAA,QAIhB,OAAO,MAAM;AAAA,QACb,SAAS,MAAM;AAAA,QACf,UAAU,MAAM;AAAA,QAChB,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,IACH;AAAA,EACF;AASA,QAAM,aAAa,iBAAiB;AAAA,IAClC,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA,OAAO;AAAA,EACT,CAAC;AAKD,MAAI,WAAW,WAAW,UAAU,WAAW,WAAW,SAAS,GAAG;AACpE,UAAM,aAAa,iBAAiB,UAAU;AAC9C,aAAS;AAAA,MACP,aACI,kHAA6G,UAAU,uBACvH,oEAAoE,WAAW,WAC5E,IAAI,CAAC,MAAM,EAAE,GAAG,EAChB,KAAK,IAAI,CAAC;AAAA,IACnB;AAAA,EACF;AAQA,MAAI,CAAC,oBAAqB,UAAS,KAAK,uBAAuB;AAQ/D,QAAM,eAAe,sBAAsB,SAAS;AACpD,QAAM,WAAW,sBAAsB,iBAAiB,YAAY;AACpE,QAAM,mBAAmB,iBAAiB,iBAAiB,YAAY;AAEvE,QAAM,SAAS,gBAAgB,WAAW;AAAA,IACxC,cAAc;AAAA,IACd,SAAS,WAAW;AAAA,IACpB,gBAAgB,SAAS;AAAA,EAC3B,CAAC;AAGD,MAAI,SAAS,UAAU,SAAS,GAAG;AACjC,aAAS;AAAA,MACP,GAAG,SAAS,UAAU,MAAM,4IAEvB,SAAS,UAAU,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,GACzC,SAAS,UAAU,SAAS,IAAI,MAAM,SAAS,UAAU,SAAS,CAAC,UAAU,EAAE;AAAA,IACtF;AAAA,EACF;AAOA,QAAM,uBAAuB,0BAA0B,MAAM;AAC7D,MAAI,uBAAuB,GAAG;AAC5B,aAAS;AAAA,MACP,GAAG,oBAAoB,eAAe,yBAAyB,IAAI,KAAK,GAAG,kBACtE,yBAAyB,IAAI,OAAO,KAAK;AAAA,IAGhD;AAAA,EACF;AAMA,QAAM,QAAQ,WAAW,eAAe,uBAAuB,QAAQ,QAAQ,WAAW,IAAI,CAAC;AAG/F,QAAM,eACJ,WAAW,iBAAiB,QAAQ,aAAa,UAAU,KAAK,IAC5D,uBAAuB,MAAM,EAAE,SAAS,MAAM,SAC9C;AACN,QAAM,cAAwC,kBAAkB,SAAS,EAAE,IAAI,CAAC,WAAW;AAAA,IACzF,IAAI;AAAA,MACF;AAAA,MACA,MAAM;AAAA,MACN,SAAS,MAAM,GAAG;AAAA,MAClB,MAAM,UAAU,SAAS,MAAM,OAAO,IAAI;AAAA,MAC1C,MAAM,UAAU;AAAA,IAClB;AAAA,IACA,GAAG;AAAA,EACL,EAAE;AAYF,QAAM,gBAAgB,mBAAmB,QAAQ,OAAO,SAAS,YAAY,SAAS;AACtF,QAAM,aAAa,MAAM,aAAa,eAAe;AAAA,IACnD,kBAAkB,QAAQ;AAAA,IAC1B;AAAA,IACA,WAAW,QAAQ;AAAA,IACnB,UAAU;AAAA,IACV,QAAQ,QAAQ;AAAA,IAChB,YAAY,CAAC,MACX,QAAQ,aAAa;AAAA,MACnB,OAAO,EAAE,UAAU,SAAS,SAAS;AAAA,MACrC,MAAM,EAAE;AAAA,MACR,OAAO,EAAE;AAAA,IACX,CAAC;AAAA,EACL,CAAC;AACD,WAAS,KAAK,GAAG,WAAW,YAAY,QAAQ;AAWhD,QAAM,iBAAiB,YACnB,MAAM;AAAA,IACJ,UAAU,KAAK,IAAI,CAACC,WAAUA,OAAM,GAAG;AAAA,IACvC;AAAA,MACE,SAAS,IAAI;AAAA,QACX,YAAY,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,MAAM,UAAU,KAAK,MAAM,MAAM,CAAC;AAAA,MAC1E;AAAA,MACA,SAAS,IAAI,IAAI,OAAO,OAAO,WAAW,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC;AAAA,MACpF,WAAW;AAAA,MACX,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,kBAAkB,QAAQ;AAAA,IAC5B;AAAA,EACF,IACA;AAGJ,QAAM,cAA2B;AAAA,IAC/B;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAAD;AAAA,IACA;AAAA;AAAA;AAAA;AAAA,IAIA,gBAAgB,SAAS;AAAA,EAC3B;AAIA,QAAM,iBAAiB,oBAAI,IAAoB;AAC/C,QAAM,aAA0B,SAAS,WAAW,EAAE,IAAI,CAAC,UAAU;AACnE,UAAM,MAAM,GAAG,MAAM,MAAM,KAAS,MAAM,OAAO;AACjD,UAAM,IAAI,eAAe,IAAI,GAAG,KAAK;AACrC,mBAAe,IAAI,KAAK,IAAI,CAAC;AAC7B,UAAM,OAAO,YAAY,MAAM,MAAM;AACrC,WAAO;AAAA,MACL,IAAI,YAAY,QAAQ,MAAM,QAAQ,SAAS,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC;AAAA,MACxE,SAAS,MAAM;AAAA,MACf,QAAQ,MAAM;AAAA,MACd,UAAU,MAAM;AAAA,MAChB,SAAS,MAAM;AAAA,MACf,KAAK,MAAM;AAAA,MACX,KAAK,MAAM,KAAK;AAAA,MAChB,OAAO,MAAM;AAAA,MACb,SAAS,MAAM;AAAA,IACjB;AAAA,EACF,CAAC;AAKD,MAAI,QAAQ,YAAY;AACtB,eAAW,YAAY,sBAAsB,WAAW,GAAG;AACzD,iBAAW,KAAK;AAAA,QACd,GAAG;AAAA,QACH,IAAI,YAAY,YAAY,SAAS,QAAQ,SAAS,SAAS,OAAO,CAAC;AAAA,MACzE,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,cAA4B,CAAC;AACnC,aAAW,EAAE,SAAS,OAAO,KAAK,WAAW,cAAc;AACzD,eAAW,SAAS,QAAQ;AAC1B,kBAAY,KAAK;AAAA,QACf,IAAI,YAAY,QAAQ,SAAS,OAAO,GAAG,UAAU,MAAM,GAAG,CAAC;AAAA,QAC/D;AAAA,QACA,MAAM,MAAM;AAAA,QACZ,QAAQ,MAAM;AAAA,QACd,SAAS,MAAM;AAAA,QACf,QAAQ,MAAM;AAAA,MAChB,CAAC;AAAA,IACH;AAAA,EACF;AAKA,QAAM,cAAc,QAAQ;AAC5B,QAAM,aAAa,WAAW,YAAY;AAC1C,QAAM,YAAY,cAAc,KAAK,WAAW,iBAAiB;AACjE,MAAI,WAAW;AACb,aAAS;AAAA,MACP,2BAA2B,UAAU;AAAA,IACvC;AAAA,EACF,WAAW,aAAa,KAAK,cAAc,cAAc,GAAG;AAC1D,aAAS;AAAA,MACP,uBAAuB,UAAU,OAAO,WAAW;AAAA,IACrD;AAAA,EACF;AAGA,QAAM,cAAc,WAAW,QAAQ;AACvC,QAAM,aACJ,UAAU,OAAO,CAAC,MAAM,EAAE,aAAa,OAAO,EAAE,SAChD,WAAW,OAAO,CAAC,MAAM,EAAE,aAAa,OAAO,EAAE;AACnD,QAAM,sBAAsB,MAAM,SAAS,YAAY;AACvD,QAAM,aACJ,sBAAsB,KACtB,UAAU,SAAS,KACnB,WAAW,SAAS,KACpB,YAAY,SAAS;AAMvB,QAAM,aAAa,MAAM,WAAW;AACpC,MAAI,YAAY;AACd,aAAS;AAAA,MACP,wBAAwB,KAAK;AAAA,IAC/B;AAAA,EACF;AAIA,MAAI,UAAmB;AACvB,MAAI,cAAc,cAAc,KAAK,aAAa,KAAK,iBAAiB,SAAS,KAAK,WAAW;AAC/F,cAAU;AAAA,EACZ,WAAW,YAAY;AACrB,cAAU;AAAA,EACZ;AAEA,SAAO;AAAA,IACL,KAAK;AAAA,IACL,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,IACnC;AAAA,IACA,SAAS;AAAA,MACP,aAAa;AAAA,MACb;AAAA,MACA,WAAW,UAAU;AAAA,MACrB,YAAY,WAAW;AAAA,MACvB,kBAAkB,iBAAiB;AAAA,MACnC;AAAA,IACF;AAAA,IACA,YAAY;AAAA,MACV,SAAS,WAAW;AAAA,MACpB,QAAQ,WAAW;AAAA,MACnB,cAAc,WAAW;AAAA,MACzB,GAAI,WAAW,WAAW,SAAS,IAAI,EAAE,YAAY,WAAW,WAAW,IAAI,CAAC;AAAA,IAClF;AAAA,IACA,OAAO,MAAM,IAAI,CAAC,OAAO;AAAA,MACvB,KAAK,EAAE,MAAM;AAAA,MACb,QAAQ,EAAE,MAAM;AAAA,MAChB,QAAQ,YAAY,EAAE,MAAM,QAAQ;AAAA,IACtC,EAAE;AAAA,IACF;AAAA,IACA;AAAA,IACA,qBAAqB,EAAE,OAAO,YAAY;AAAA,IAC1C;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,GAAI,QAAQ,cAAc,EAAE,aAAa,iBAAiB,OAAO,eAAe,EAAE,IAAI,CAAC;AAAA,IACvF,GAAI,QAAQ,aAAa,EAAE,WAAW,IAAI,CAAC;AAAA,IAC3C,GAAI,QAAQ,cAAc,EAAE,YAAY,IAAI,CAAC;AAAA,IAC7C,aAAa;AAAA,MACX,cAAc,MAAM;AAAA,MACpB,cAAc,WAAW;AAAA,MACzB,aAAa,WAAW,YAAY;AAAA,MACpC,WAAW,YAAY,aAAa,WAAW;AAAA,MAC/C;AAAA;AAAA;AAAA;AAAA,MAIA,UAAU;AAAA,QACR,MAAM;AAAA,QACN,GAAI,YACA;AAAA,UACE,YAAY,UAAU;AAAA,UACtB,UAAU,UAAU,KAAK;AAAA,UACzB,UAAU,UAAU;AAAA,QACtB,IACA,CAAC;AAAA,MACP;AAAA,MACA,GAAI,eAAe,IAAI,EAAE,aAAa,IAAI,CAAC;AAAA,MAC3C,GAAI,iBAAiB,IAAI,EAAE,eAAe,IAAI,CAAC;AAAA,MAC/C,GAAI,uBAAuB,IAAI,EAAE,qBAAqB,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA,MAI3D,GAAI,SAAS,OAAO,KAAK,SAAS,UAAU,IACxC;AAAA,QACE,kBAAkB;AAAA,UAChB,OAAO,SAAS;AAAA,UAChB,GAAI,SAAS,UAAU,SAAS,IAAI,EAAE,WAAW,SAAS,UAAU,OAAO,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,UAKhF,GAAI,mBAAmB,IAAI,EAAE,UAAU,iBAAiB,IAAI,CAAC;AAAA;AAAA;AAAA,UAG7D,GAAI,SAAS,UAAU,IAAI,EAAE,SAAS,SAAS,QAAQ,IAAI,CAAC;AAAA,QAC9D;AAAA,MACF,IACA,CAAC;AAAA,MACL,GAAI,YACA;AAAA,QACE,SAAS;AAAA,UACP,OAAO,UAAU,YAAY;AAAA,UAC7B,YAAY,UAAU,YAAY;AAAA,UAClC,UAAU,eAAe;AAAA,UACzB,WAAW,eAAe,aAAa,KAAK;AAAA,UAC5C,GAAI,UAAU,YAAY,cACtB,EAAE,aAAa,UAAU,YAAY,YAAY,IACjD,CAAC;AAAA,QACP;AAAA,MACF,IACA,CAAC;AAAA,IACP;AAAA,EACF;AACF;AAOA,SAAS,eAAe,aAAuB,OAAuB;AACpE,QAAM,UAAU,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC1D,MAAI,IAAI;AACR,aAAW,OAAO,YAAa,KAAI,CAAC,QAAQ,IAAI,GAAG,EAAG,MAAK;AAC3D,SAAO;AACT;AAGA,SAAS,mBACP,QACA,SACA,OACA,WACe;AACf,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,MAAM,MAAM,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,EAAE;AAAA;AAAA;AAAA;AAAA,IAIlD,WAAW,CAAC;AAAA,IACZ,aAAa;AAAA,MACX,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,kBAAkB;AAAA,MAClB,aAAa;AAAA,MACb,iBAAiB;AAAA,MACjB,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,oBAAoB;AAAA,MACpB,UAAU,MAAM;AAAA,MAChB,UAAU,CAAC;AAAA,IACb;AAAA,IACA;AAAA,EACF;AACF;;;ACplCA,IAAM,OAAO;AAAA,EACX,OAAO;AAAA,EACP,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AACR;AAEA,SAAS,MAAM,SAAkB;AAC/B,SAAO,CAAC,MAAc,SAAkB,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,KAAK,KAAK;AACpF;AAEA,IAAM,eAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AACP;AAEA,SAAS,aAAa,SAA0B;AAC9C,SAAO,YAAY,UAAU,KAAK,QAAQ,YAAY,WAAW,KAAK,SAAS,KAAK;AACtF;AAEO,SAAS,eAAe,QAAsB,UAAyB,CAAC,GAAW;AACxF,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,IAAI,MAAM,KAAK;AACrB,QAAM,QAAkB,CAAC;AAEzB,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,GAAG,EAAE,KAAK,MAAM,QAAQ,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,GAAG,CAAC,EAAE;AACjE,QAAM;AAAA,IACJ,GAAG,EAAE,aAAa,OAAO,QAAQ,OAAO,GAAG,aAAa,OAAO,QAAQ,OAAO,EAAE,YAAY,CAAC,CAAC,OAC5F;AAAA,MACE,KAAK;AAAA,MACL,GAAG,OAAO,YAAY,YAAY,mBAAmB,OAAO,YAAY,YAAY;AAAA;AAAA;AAAA,OAIjF,OAAO,YAAY,kBAAkB,KAAK,aAAa,OAAO,OAAO;AAAA,IAC1E;AAAA,EACJ;AAOA,QAAM,WAAW,OAAO,YAAY;AACpC,MAAI,UAAU,UAAU,UAAU,SAAS,cAAc,SAAS,UAAU;AAC1E,UAAM,UAAU,SAAS,SAAS,CAAC;AAKnC,UAAM,UAAU,OAAO,YAAY;AACnC,UAAM,SAAS,UAAU,SAAS,WAAW,4BAA4B;AAEzE,UAAM;AAAA,MACJ;AAAA,QACE,KAAK;AAAA,QACL,aAAa,OAAO,OAAO,SAAS,UAAU,iBAAiB,MAAM,SAChE,SAAS,SAAS,MAAM,8BACxB,QAAQ,OAAO,IAAI,QAAQ,IAAI,IAAI,QAAQ,OAAO;AAAA,MACzD;AAAA,IACF;AACA,UAAM;AAAA,MACJ;AAAA,QACE,KAAK;AAAA,QACL;AAAA,MAEF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK,EAAE;AAGb,QAAM;AAAA,IACJ;AAAA,MACE,MAAM,GAAG,OAAO,QAAQ,aAAa,eAAe,OAAO,QAAQ,cAAc,CAAC;AAAA,MAClF;AAAA,QACE;AAAA,QACA,OAAO,QAAQ;AAAA,QACf;AAAA,QACA,OAAO,QAAQ,sBAAsB;AAAA,MACvC;AAAA,MACA,MAAM,GAAG,OAAO,QAAQ,WAAW,aAAa,OAAO,QAAQ,YAAY,CAAC;AAAA,MAC5E,MAAM,GAAG,OAAO,QAAQ,YAAY,cAAc,OAAO,QAAQ,aAAa,CAAC;AAAA,MAC/E;AAAA,QACE;AAAA,QACA,OAAO,QAAQ;AAAA,QACf;AAAA,QACA,OAAO,QAAQ,mBAAmB;AAAA,MACpC;AAAA,IACF,EAAE,KAAK,KAAK;AAAA,EACd;AACA,QAAM,KAAK,EAAE;AAGb,MAAI,OAAO,iBAAiB,SAAS,GAAG;AACtC,UAAM,KAAK,EAAE,KAAK,MAAM,mBAAmB,CAAC;AAC5C,eAAW,QAAQ,OAAO,kBAAkB;AAC1C,YAAM,KAAK,KAAK,EAAE,KAAK,KAAK,IAAI,KAAK,UAAU,eAAe,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE;AAAA,IAClF;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,MAAI,OAAO,YAAY,SAAS,GAAG;AACjC,UAAM,KAAK,EAAE,KAAK,MAAM,cAAc,CAAC;AACvC,UAAM,SAAS,QAAQ,OAAO,aAAa,CAAC,MAAM,EAAE,OAAO;AAC3D,eAAW,CAAC,SAAS,KAAK,KAAK,QAAQ;AACrC,YAAM,KAAK,KAAK,EAAE,KAAK,MAAM,OAAO,CAAC,EAAE;AACvC,iBAAW,QAAQ,OAAO;AACxB,cAAM,MACJ,KAAK,YAAY,WACb,EAAE,KAAK,KAAK,IAAI,KAAK,UAAU,KAAK,UAAU,QAAQ,GAAG,IACzD,EAAE,KAAK,QAAQ,IAAI,KAAK,OAAO,GAAG,KAAK,SAAS,IAAI,KAAK,MAAM,KAAK,EAAE,GAAG;AAC/E,cAAM,KAAK,OAAO,GAAG,IAAI,KAAK,IAAI,EAAE;AAAA,MACtC;AAAA,IACF;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,QAAM,EAAE,OAAO,YAAY,IAAI,OAAO;AACtC,MAAI,MAAM,SAAS,KAAK,YAAY,SAAS,GAAG;AAC9C,UAAM,KAAK,EAAE,KAAK,MAAM,sBAAsB,CAAC;AAC/C,eAAW,QAAQ,OAAO;AACxB,YAAM;AAAA,QACJ,KAAK,EAAE,KAAK,MAAM,KAAK,KAAK,CAAC,mBAAc,EAAE,KAAK,QAAQ,KAAK,eAAe,KAAK,IAAI,CAAC,CAAC,KACvF,EAAE,KAAK,KAAK,UAAU,KAAK,eAAe,KAAK,IAAI,CAAC,GAAG;AAAA,MAC3D;AAAA,IACF;AACA,eAAW,SAAS,aAAa;AAC/B,YAAM,KAAK,KAAK,EAAE,KAAK,QAAQ,MAAM,IAAI,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,GAAG,CAAC,EAAE;AACtE,YAAM,KAAK,OAAO,MAAM,OAAO,EAAE;AAAA,IACnC;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,MAAI,OAAO,UAAU,SAAS,GAAG;AAC/B,UAAM,KAAK,EAAE,KAAK,MAAM,YAAY,CAAC;AACrC,UAAM,KAAK,GAAG,mBAAmB,OAAO,WAAW,CAAC,CAAC;AACrD,UAAM,KAAK,EAAE;AAAA,EACf;AAMA,MAAI,OAAO,WAAW,SAAS,GAAG;AAChC,UAAM,KAAK,EAAE,KAAK,MAAM,kBAAkB,CAAC;AAC3C,UAAM;AAAA,MACJ,KAAK,EAAE,KAAK,KAAK,YAAY,OAAO,WAAW,QAAQ,KAAK,IAAI,KAAK,MAAM,SAAS,OAAO,WAAW,MAAM,GAAG,CAAC;AAAA,IAClH;AACA,UAAM,KAAK,GAAG,mBAAmB,OAAO,YAAY,CAAC,CAAC;AACtD,UAAM,KAAK,EAAE;AAAA,EACf;AAMA,MAAI,OAAO,aAAa;AACtB,UAAM,KAAK,EAAE,KAAK,MAAM,aAAa,CAAC;AACtC,UAAM,SAAS,OAAO,YAAY,MAAM;AACxC,UAAM,KAAK,EAAE,KAAK,KAAK,KAAK,MAAM,QAAQ,WAAW,IAAI,KAAK,GAAG,yBAAsB,CAAC;AACxF,eAAW,QAAQ,OAAO,YAAY,OAAO;AAC3C,YAAM,EAAE,MAAM,MAAM,MAAM,IAAI,QAAQ,IAAI,KAAK;AAC/C,YAAM,QAAQ;AAAA,QACZ,OAAO,IAAI,EAAE,KAAK,KAAK,GAAG,IAAI,OAAO,IAAI;AAAA,QACzC,OAAO,IAAI,EAAE,KAAK,QAAQ,GAAG,IAAI,OAAO,IAAI;AAAA,QAC5C,OAAO,IAAI,EAAE,KAAK,OAAO,GAAG,IAAI,OAAO,IAAI;AAAA,QAC3C,KAAK,IAAI,EAAE,KAAK,KAAK,GAAG,EAAE,MAAM,IAAI;AAAA,QACpC,UAAU,IAAI,EAAE,KAAK,KAAK,GAAG,OAAO,UAAU,IAAI;AAAA,MACpD,EAAE,OAAO,OAAO;AAChB,YAAM;AAAA,QACJ,KAAK,EAAE,KAAK,MAAM,KAAK,OAAO,OAAO,EAAE,CAAC,CAAC,IAAI,MAAM,KAAK,EAAE,KAAK,KAAK,QAAK,CAAC,CAAC,KACzE,EAAE,KAAK,KAAK,MAAM,KAAK,KAAK,GAAG;AAAA,MACnC;AAAA,IACF;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAOA,MAAI,OAAO,cAAc,OAAO,WAAW,SAAS,GAAG;AACrD,UAAM,KAAK,EAAE,KAAK,MAAM,gBAAgB,CAAC;AACzC,UAAM,KAAK,EAAE,KAAK,KAAK,sDAAsD,CAAC;AAC9E,eAAW,CAAC,QAAQ,KAAK,KAAK,QAAQ,CAAC,GAAG,OAAO,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG;AAC9E,YAAM;AAAA,QACJ,KAAK,EAAE,KAAK,MAAM,MAAM,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,MAAM,MAAM,QAAQ,MAAM,WAAW,IAAI,KAAK,GAAG,GAAG,CAAC;AAAA,MACpG;AACA,YAAM,KAAK,OAAO,MAAM,CAAC,EAAG,KAAK,EAAE;AAAA,IACrC;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,MAAI,OAAO,QAAQ,YAAY,SAAS;AACtC,UAAM,KAAK,EAAE,KAAK,OAAO,oBAAoB,CAAC;AAC9C,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,aAAW,WAAW,OAAO,YAAY,UAAU;AACjD,UAAM,KAAK,EAAE,KAAK,KAAK,SAAS,OAAO,EAAE,CAAC;AAAA,EAC5C;AACA,MAAI,OAAO,YAAY,WAAW;AAChC,UAAM,KAAK,EAAE,KAAK,KAAK,6CAA6C,CAAC;AAAA,EACvE;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,MACP,GACA,GACA,MACA,KACQ;AACR,QAAM,QAAQ,GAAG,CAAC,IAAI,IAAI,GAAG,MAAM,IAAI,KAAK,GAAG;AAC/C,SAAO,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,QAAQ,KAAK,CAAC,IAAI,EAAE,KAAK,KAAK,KAAK;AACtE;AAQA,SAAS,mBACP,QAMA,GACU;AACV,QAAM,QAAkB,CAAC;AACzB,aAAW,CAAC,SAAS,KAAK,KAAK,QAAQ,CAAC,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG;AACrE,UAAM,KAAK,KAAK,EAAE,KAAK,MAAM,OAAO,CAAC,EAAE;AACvC,eAAW,SAAS,OAAO;AACzB,YAAM,MACJ,MAAM,aAAa,UACf,EAAE,KAAK,KAAK,OAAO,IACnB,MAAM,aAAa,YACjB,EAAE,KAAK,QAAQ,OAAO,IACtB,EAAE,KAAK,KAAK,OAAO;AAC3B,YAAM,KAAK,OAAO,GAAG,IAAI,EAAE,KAAK,KAAK,MAAM,MAAM,CAAC,KAAK,eAAe,MAAM,OAAO,CAAC,EAAE;AAAA,IACxF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,QAAW,OAAY,KAA4C;AAC1E,QAAM,MAAM,oBAAI,IAAiB;AACjC,aAAW,QAAQ,OAAO;AACxB,UAAM,IAAI,IAAI,IAAI;AAClB,UAAM,MAAM,IAAI,IAAI,CAAC;AACrB,QAAI,IAAK,KAAI,KAAK,IAAI;AAAA,QACjB,KAAI,IAAI,GAAG,CAAC,IAAI,CAAC;AAAA,EACxB;AACA,SAAO;AACT;AAEA,SAAS,eAAe,GAAmB;AACzC,SAAO,EAAE,QAAQ,MAAM,EAAE;AAC3B;;;AC/QA,IAAME,QAAO;AAAA,EACX,OAAO;AAAA,EACP,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AACR;AAEA,SAASC,OAAM,SAAkB;AAC/B,SAAO,CAAC,MAAc,SAAkB,UAAU,GAAG,IAAI,GAAG,IAAI,GAAGD,MAAK,KAAK,KAAK;AACpF;AAEA,IAAME,gBAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AACP;AAEA,SAASC,cAAa,SAA0B;AAC9C,SAAO,YAAY,UAAUH,MAAK,QAAQ,YAAY,WAAWA,MAAK,SAASA,MAAK;AACtF;AAEO,SAAS,sBAAsB,SAAwB,UAAyB,CAAC,GAAW;AACjG,QAAM,IAAIC,OAAM,QAAQ,SAAS,KAAK;AACtC,QAAM,QAAkB,CAAC;AAEzB,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,GAAG,EAAED,MAAK,MAAM,QAAQ,CAAC,IAAI,EAAEA,MAAK,KAAK,QAAQ,GAAG,CAAC,IAAI,EAAEA,MAAK,KAAK,WAAW,CAAC,EAAE;AAC9F,QAAM;AAAA,IACJ,GAAG,EAAEG,cAAa,QAAQ,OAAO,GAAGD,cAAa,QAAQ,OAAO,CAAC,CAAC,OAChE;AAAA,MACEF,MAAK;AAAA,MACL,GAAG,QAAQ,OAAO,YAAY,mBAAmB,QAAQ,OAAO,YAAY;AAAA;AAAA,OAGzE,QAAQ,YAAY,kBAAkB,KAAK,aAAa,QAAQ,OAAO;AAAA,IAC5E;AAAA,EACJ;AACA,QAAM,KAAK,EAAE;AACb,QAAM;AAAA,IACJ;AAAA,MACE,OAAO,GAAG,QAAQ,OAAO,aAAa,aAAa;AAAA,MACnD,OAAO,GAAG,QAAQ,OAAO,qBAAqB,qBAAqB;AAAA,MACnE,OAAO,GAAG,QAAQ,OAAO,WAAW,WAAW;AAAA,MAC/C,OAAO,GAAG,QAAQ,OAAO,YAAY,YAAY;AAAA,MACjD,OAAO,GAAG,QAAQ,OAAO,kBAAkB,kBAAkB;AAAA,IAC/D,EAAE,KAAK,KAAK;AAAA,EACd;AACA,QAAM,KAAK,EAAE;AAGb,MAAI,QAAQ,iBAAiB,SAAS,GAAG;AACvC,UAAM,KAAK,EAAEA,MAAK,MAAM,mBAAmB,CAAC;AAC5C,eAAW,QAAQ,QAAQ,kBAAkB;AAC3C,YAAM,KAAK,KAAK,EAAEA,MAAK,KAAK,IAAI,KAAK,UAAU,eAAe,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE;AAAA,IAClF;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,MAAI,QAAQ,YAAY,SAAS,GAAG;AAClC,UAAM,KAAK,EAAEA,MAAK,MAAM,cAAc,CAAC;AACvC,eAAW,QAAQ,QAAQ,aAAa;AACtC,YAAM,MACJ,KAAK,YAAY,WACb,EAAEA,MAAK,KAAK,IAAI,KAAK,UAAU,KAAK,UAAU,QAAQ,GAAG,IACzD,EAAEA,MAAK,QAAQ,IAAI,KAAK,OAAO,GAAG,KAAK,SAAS,IAAI,KAAK,MAAM,KAAK,EAAE,GAAG;AAC/E,YAAM,KAAK,KAAK,GAAG,IAAI,KAAK,IAAI,IAAI,EAAEA,MAAK,KAAK,OAAI,KAAK,KAAK,EAAE,CAAC,EAAE;AACnE,YAAM,KAAK,OAAO,EAAEA,MAAK,KAAK,MAAM,YAAY,KAAK,OAAO,KAAK,SAAS,CAAC,EAAE,CAAC,EAAE;AAAA,IAClF;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,QAAM,EAAE,OAAO,YAAY,IAAI,QAAQ;AACvC,MAAI,MAAM,SAAS,KAAK,YAAY,SAAS,GAAG;AAC9C,UAAM,KAAK,EAAEA,MAAK,MAAM,sBAAsB,CAAC;AAC/C,eAAW,QAAQ,OAAO;AACxB,YAAM;AAAA,QACJ,KAAK,EAAEA,MAAK,MAAM,KAAK,KAAK,CAAC,mBAAc,EAAEA,MAAK,QAAQ,KAAK,eAAe,KAAK,IAAI,CAAC,CAAC,KACvF,EAAEA,MAAK,KAAK,UAAU,KAAK,eAAe,KAAK,IAAI,CAAC,GAAG;AAAA,MAC3D;AAAA,IACF;AACA,eAAW,SAAS,aAAa;AAC/B,YAAM,KAAK,KAAK,EAAEA,MAAK,QAAQ,MAAM,IAAI,CAAC,IAAI,EAAEA,MAAK,KAAK,OAAI,MAAM,KAAK,EAAE,CAAC,EAAE;AAC9E,YAAM,KAAK,OAAOI,gBAAe,MAAM,MAAM,CAAC,EAAE;AAChD,YAAM,KAAK,OAAO,EAAEJ,MAAK,KAAK,MAAM,YAAY,MAAM,OAAO,MAAM,SAAS,CAAC,EAAE,CAAC,EAAE;AAAA,IACpF;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,MAAI,QAAQ,UAAU,SAAS,GAAG;AAChC,UAAM,KAAK,EAAEA,MAAK,MAAM,YAAY,CAAC;AACrC,UAAM,KAAK,GAAG,WAAW,QAAQ,WAAW,CAAC,CAAC;AAC9C,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,MAAI,QAAQ,WAAW,SAAS,GAAG;AACjC,UAAM,KAAK,EAAEA,MAAK,MAAM,kBAAkB,CAAC;AAC3C,UAAM,KAAK,GAAG,WAAW,QAAQ,YAAY,CAAC,CAAC;AAC/C,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,MAAI,QAAQ,aAAa;AACvB,UAAM,KAAK,EAAEA,MAAK,MAAM,aAAa,CAAC;AACtC,eAAW,QAAQ,QAAQ,YAAY,OAAO;AAC5C,YAAM,EAAE,MAAM,MAAM,MAAM,IAAI,QAAQ,IAAI,KAAK;AAC/C,YAAM,QAAQ;AAAA,QACZ,OAAO,IAAI,EAAEA,MAAK,KAAK,GAAG,IAAI,OAAO,IAAI;AAAA,QACzC,OAAO,IAAI,EAAEA,MAAK,QAAQ,GAAG,IAAI,OAAO,IAAI;AAAA,QAC5C,OAAO,IAAI,EAAEA,MAAK,OAAO,GAAG,IAAI,OAAO,IAAI;AAAA,QAC3C,KAAK,IAAI,EAAEA,MAAK,KAAK,GAAG,EAAE,MAAM,IAAI;AAAA,QACpC,UAAU,IAAI,EAAEA,MAAK,KAAK,GAAG,OAAO,UAAU,IAAI;AAAA,MACpD,EAAE,OAAO,OAAO;AAChB,YAAM,KAAK,KAAK,EAAEA,MAAK,MAAM,KAAK,OAAO,OAAO,EAAE,CAAC,CAAC,IAAI,MAAM,KAAK,EAAEA,MAAK,KAAK,QAAK,CAAC,CAAC,EAAE;AAAA,IAC1F;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,MAAI,QAAQ,cAAc,QAAQ,WAAW,SAAS,GAAG;AACvD,UAAM,KAAK,EAAEA,MAAK,MAAM,gBAAgB,CAAC;AACzC,UAAM,SAAS,oBAAI,IAA8C;AACjE,eAAW,YAAY,QAAQ,YAAY;AACzC,YAAM,QAAQ,OAAO,IAAI,SAAS,MAAM;AACxC,UAAI,MAAO,OAAM,SAAS;AAAA,UACrB,QAAO,IAAI,SAAS,QAAQ,EAAE,OAAO,SAAS,OAAO,OAAO,EAAE,CAAC;AAAA,IACtE;AACA,eAAW,CAAC,QAAQ,EAAE,OAAO,MAAM,CAAC,KAAK,QAAQ;AAC/C,YAAM;AAAA,QACJ,KAAK,EAAEA,MAAK,MAAM,MAAM,CAAC,IAAI,EAAEA,MAAK,KAAK,IAAI,KAAK,QAAQ,UAAU,IAAI,KAAK,GAAG,GAAG,CAAC;AAAA,MACtF;AACA,YAAM,KAAK,OAAO,KAAK,EAAE;AAAA,IAC3B;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,MAAI,QAAQ,YAAY,SAAS;AAC/B,UAAM,KAAK,EAAEA,MAAK,OAAO,oBAAoB,CAAC;AAC9C,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,aAAW,WAAW,QAAQ,UAAU;AACtC,UAAM,KAAK,EAAEA,MAAK,KAAK,SAAS,OAAO,EAAE,CAAC;AAAA,EAC5C;AACA,MAAI,QAAQ,WAAW;AACrB,UAAM,KAAK,EAAEA,MAAK,KAAK,6CAA6C,CAAC;AAAA,EACvE;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,OAAO,GAAwC,GAAW,MAAsB;AACvF,QAAM,QAAQ,GAAG,CAAC,IAAI,IAAI,GAAG,MAAM,IAAI,KAAK,GAAG;AAC/C,SAAO,IAAI,IAAI,EAAEA,MAAK,MAAM,EAAEA,MAAK,QAAQ,KAAK,CAAC,IAAI,EAAEA,MAAK,KAAK,KAAK;AACxE;AAEA,SAAS,YAAY,OAAiB,MAAsB;AAC1D,QAAM,QAAQ,MAAM,KAAK,IAAI;AAC7B,SAAO,OAAO,IAAI,GAAG,KAAK,MAAM,IAAI,WAAW;AACjD;AAEA,SAASI,gBAAe,GAAmB;AACzC,SAAO,EAAE,QAAQ,MAAM,EAAE;AAC3B;AAOA,SAAS,WACP,QACA,GACU;AACV,QAAM,QAAkB,CAAC;AACzB,aAAW,SAAS,QAAQ;AAC1B,UAAM,MACJ,MAAM,aAAa,UACf,EAAEJ,MAAK,KAAK,OAAO,IACnB,MAAM,aAAa,YACjB,EAAEA,MAAK,QAAQ,OAAO,IACtB,EAAEA,MAAK,KAAK,OAAO;AAI3B,UAAM,QAAQ,MAAM,QAAQ,EAAEA,MAAK,KAAK,KAAK,MAAM,KAAK,GAAG,IAAI;AAC/D,UAAM,KAAK,KAAK,GAAG,IAAI,EAAEA,MAAK,MAAM,MAAM,MAAM,CAAC,GAAG,KAAK,IAAI,EAAEA,MAAK,KAAK,OAAI,MAAM,KAAK,EAAE,CAAC,EAAE;AAC7F,QAAI,MAAM,IAAK,OAAM,KAAK,OAAO,EAAEA,MAAK,KAAKI,gBAAe,MAAM,GAAG,CAAC,CAAC,EAAE;AACzE,QAAI,MAAM,KAAK;AAGb,YAAM,CAAC,MAAM,GAAG,IAAI,IAAI,MAAM,IAAI,MAAM,IAAI;AAC5C,YAAM,KAAK,OAAO,EAAEJ,MAAK,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE;AAC/C,iBAAW,QAAQ,KAAM,OAAM,KAAK,YAAY,EAAEA,MAAK,KAAK,IAAI,CAAC,EAAE;AAAA,IACrE;AACA,UAAM,KAAK,OAAO,EAAEA,MAAK,KAAK,MAAM,YAAY,MAAM,OAAO,MAAM,SAAS,CAAC,EAAE,CAAC,EAAE;AAAA,EACpF;AACA,SAAO;AACT;;;ACvKA,IAAM,gBAAgB,CAAC,OAAO,YAAY,UAAU,UAAU,gBAAgB,YAAY;AAO1F,IAAMK,eAAc,CAAC,KAAK,GAAG;AAC7B,IAAMC,oBAAmB,CAAC,GAAG,CAAC;AAE9B,IAAM,UAAkC;AAAA,EACtC,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACP;AAEA,SAAS,IAAI,OAAuB;AAClC,SAAO,MAAM,QAAQ,YAAY,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC;AACzD;AAWA,SAAS,UAAU,SAAyB;AAC1C,SAAO,QAAQ,QAAQ,cAAc,iBAAiB;AACxD;AAGA,SAAS,KAAK,OAAe,MAAM,KAAa;AAC9C,SAAO,MAAM,SAAS,MAAM,GAAG,MAAM,MAAM,GAAG,GAAG,CAAC,WAAM;AAC1D;AAWA,SAAS,WAAW,OAAoC;AACtD,MAAI;AACF,UAAM,MAAM,KAAK,UAAU,OAAO,MAAM,CAAC;AACzC,QAAI,QAAQ,OAAW,QAAO;AAC9B,WAAO,IAAI,SAAS,MAAS,GAAG,IAAI,MAAM,GAAG,GAAM,CAAC;AAAA,oBAAkB;AAAA,EACxE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASA,SAAS,QAAQ,OAA+C;AAC9D,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,KAAK;AACzB,WAAO,IAAI,aAAa,WAAW,IAAI,aAAa,WAAW,IAAI,OAAO;AAAA,EAC5E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAASC,QAAO,OAAuB;AACrC,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,EAAE;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAO,OAAuB;AACrC,MAAI;AACF,UAAM,EAAE,UAAU,OAAO,IAAI,IAAI,IAAI,KAAK;AAC1C,WAAO,GAAG,QAAQ,GAAG,MAAM;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,YAAY,QAA2B;AAC9C,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,UAAI,OAAO,SAAU,QAAO,OAAO;AACnC,UAAI,OAAO,KAAM,QAAO,OAAO;AAC/B,UAAI,OAAO,UAAW,QAAO,cAAc,OAAO,SAAS;AAC3D,aAAO;AAAA,IACT,KAAK;AACH,aAAO,cAAc,OAAO,GAAG;AAAA,IACjC,KAAK;AACH,aAAO,SAAS,OAAO,SAAS;AAAA,IAClC,KAAK;AACH,aAAO,YAAY,OAAO,KAAK;AAAA,IACjC,KAAK;AACH,aAAO,WAAW,OAAO,IAAI;AAAA,IAC/B,KAAK;AACH,aAAO;AAAA,EACX;AACF;AAQA,SAAS,WAAW,OAA8D;AAChF,aAAW,QAAQ,OAAO;AACxB,QAAI,QAAQ,KAAK,MAAM,KAAK,MAAM,IAAI;AACpC,aAAO,EAAE,OAAO,KAAK,OAAO,MAAM,YAAY,KAAK,MAAM,EAAE;AAAA,IAC7D;AAAA,EACF;AACA,SAAO;AACT;AAwCA,SAAS,OAAO,MAAqD;AACnE,SAAO,QAAQ,KAAK,QAAQ,KAAK,KAAK,SAAS,IAAI,OAAO;AAC5D;AAEA,SAAS,QAAQ,YAA+C;AAC9D,QAAM,KAA2C,WAAW,UAAU,OAAO,CAAC;AAG9E,QAAM,QAAQ,IAAI,IAAI,MAAM,KAAK,IAAI,GAAG,IAAI,QAAQ;AACpD,QAAM,WAAW,SAAS,WAAW,QAAQ,OAAO;AACpD,MAAI,CAAC,YAAY,SAAS,KAAK,MAAM,GAAI,QAAO;AAKhD,QAAM,QAAQ,WAAW,SAAS,SAAS,KAAK,CAAC;AACjD,QAAM,eAAe;AAAA,IACnB,IAAI,SAAS,GAAG,SAAS,EAAE,OAAO,GAAG,MAAM,OAAO,QAAQ,GAAG,OAAO,MAAM,IAAI;AAAA,EAChF;AACA,QAAM,aAAa,OAAO,OAAO,QAAQ,CAAC,CAAC;AAC3C,QAAM,OAAO,gBAAgB;AAE7B,SAAO;AAAA,IACL;AAAA,IACA,KAAK,QAAQ,QAAQ;AAAA,IACrB,KAAK,IAAI,KAAK,SAAS,WAAW,QAAQ,UAAU;AAAA,IACpD,MAAM,QAAQ,aAAa;AAAA,IAC3B,OAAO,WAAW,UAAU,OAAO;AAAA,IACnC;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,OAAO,OAAO,KAAK,MAAO,KAAK,QAAQ,KAAK,SAAU,GAAG,IAAI,MAAM;AAAA,EACrE;AACF;AAEA,SAAS,OAAO,YAA8B;AAC5C,QAAM,EAAE,WAAW,IAAI,SAAS,MAAM,UAAU,IAAI,IAAI;AACxD,QAAM,MAAM,WAAW,KAAK;AAC5B,SAAO;AAAA,IACL;AAAA,IACA,MAAMA,QAAO,GAAG;AAAA,IAChB,MAAM,OAAO,GAAG;AAAA,IAChB,SAAS,QAAQ,GAAG,OAAO,IAAI,KAAK;AAAA,IACpC,eAAe,QAAQ,GAAG,aAAa,KAAK,WAAW;AAAA,IACvD,eAAe,QAAQ,IAAI,KAAK;AAAA,IAChC,iBAAiB,QAAQ,KAAK,WAAW;AAAA,IACzC,UAAU,QAAQ,GAAG,QAAQ;AAAA,IAC7B,QAAQ,QAAQ,QAAQ,OAAO,GAAG,OAAO,IAAI,KAAK;AAAA,IAClD,OAAO,QAAQ,UAAU;AAAA,IACzB,MAAM,QAAQ,WAAW,MAAM,MAAM,CAAC,GAAG,IAAI;AAAA,IAC7C,YAAY,KAAK,YAAY;AAAA,IAC7B,aAAa,QAAQ,MAAM;AAAA,IAC3B,QAAQ,GAAG,QAAQ,SAAS,IAAI,MAAM;AAAA,IACtC,YAAY,GAAG,iBAAiB,IAAI,CAAC,MAAM,EAAE,KAAK;AAAA,IAClD,QAAQ,WAAW;AAAA,IACnB,WAAW,WAAW;AAAA,IACtB,QAAQ,WAAW,KAAK;AAAA,EAC1B;AACF;AAIA,SAAS,UAAU,OAA0B;AAC3C,MAAI,MAAM,UAAU,OAAW,QAAO;AACtC,QAAM,QAAQ,MAAM,MAAM,QAAQ,CAAC;AACnC,QAAM,SAAS,MAAM,eAAe,aAAa;AACjD,MAAI,MAAM,SAASF,aAAY,CAAC,KAAK,MAAM,SAASA,aAAY,CAAC,GAAG;AAClE,WAAO,GAAG,KAAK,MAAM,MAAM;AAAA,EAC7B;AACA,MAAI,MAAM,SAASC,kBAAiB,CAAC,KAAK,MAAM,SAASA,kBAAiB,CAAC,GAAG;AAC5E,WAAO,GAAG,KAAK,MAAM,MAAM;AAAA,EAC7B;AACA,SAAO,GAAG,KAAK,MAAM,MAAM;AAC7B;AAGA,SAAS,UAAU,OAA0B;AAC3C,MAAI,CAAC,MAAM,MAAO,QAAO;AACzB,MAAI,MAAM,MAAM,IAAI;AAClB,UAAM,OAAO,MAAM,MAAM,cAAc,KAAK,MAAM,MAAM,WAAW,KAAK;AACxE,WAAO,YAAY,MAAM,MAAM,MAAM,GAAG,IAAI;AAAA,EAC9C;AACA,SAAO,MAAM,MAAM,WAAW,IAC1B,+CACA,YAAY,MAAM,MAAM,MAAM;AACpC;AAGA,SAAS,SAAS,MAAY,WAA2B;AACvD,QAAM,QAAQ,KAAK;AACnB,MAAI,CAAC,OAAO;AACV,WAAO,eAAe,SAAS;AAAA,EACjC;AACA,MAAI,MAAM,SAAS,CAAC,MAAM,MAAM,IAAI;AAClC,WACE,eAAe,SAAS,2DAEjB,IAAI,UAAU,KAAK,CAAC,CAAC;AAAA,EAGhC;AACA,MAAI,CAAC,MAAM,KAAK;AACd,WACE,eAAe,SAAS,4DAEjB,IAAI,KAAK,MAAM,QAAQ,CAAC,CAAC;AAAA,EAGpC;AACA,QAAM,MAAM,MAAM,MAAM,IAAI,MAAM,GAAG,IAAI;AAGzC,SAAO,eAAe,SAAS,oBAAoB,IAAI,MAAM,GAAG,CAAC,UAAU,GAAG;AAChF;AAEA,SAAS,MAAM,MAOJ;AACT,QAAM,OAAO,KAAK,OACd,sBAAsB,IAAI,KAAK,KAAK,KAAK,CAAC,OAAO,IAAI,KAAK,KAAK,IAAI,CAAC,SACpE;AACJ,SAAO;AAAA,+BACsB,IAAI,KAAK,IAAI,CAAC,6BAA6B,KAAK,MAAM,QAAQ,KAAK,EAAE,CAAC,KAAK,IAAI,KAAK,KAAK,CAAC;AAAA,uBAClG,KAAK,IAAI;AAAA,eACjB,KAAK,IAAI,OAAO,IAAI,qBAAqB,IAAI,KAAK,MAAM,CAAC;AAAA;AAExE;AAEA,SAAS,QAAQ,MAAoB;AACnC,SAAO,KAAK,OACR,yBAAyB,IAAI,KAAK,IAAI,CAAC,6BACvC,iCAAiC,IAAI,KAAK,KAAK,MAAM,GAAG,CAAC,EAAE,YAAY,CAAC,CAAC;AAC/E;AAIA,SAAS,cAAc,MAAoB;AACzC,QAAM,QAAQ,KAAK,eAAe,SAAS,KAAK,SAAS,SAAS,KAAK;AACvE,QAAM,cAAc,KAAK,iBAAiB;AAC1C,QAAM,OAAO;AAAA,sBACO,QAAQ,IAAI,CAAC,wBAAwB,IAAI,KAAK,UAAU,SAAS,KAAK,IAAI,CAAC,aAAa,IAAI,KAAK,IAAI,CAAC,GAAG,IAAI,KAAK,SAAS,MAAM,KAAK,WAAM,KAAK,KAAK,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG,EAAE,KAAK,UAAK,CAAC,EAAE,CAAC;AAAA,QAChN,IAAI,KAAK,CAAC;AAAA,IACd,cAAc,qBAAqB,IAAI,WAAW,CAAC,SAAS,uGAAkG;AAAA;AAEhK,SAAO,MAAM;AAAA,IACX,MAAM;AAAA,IACN,OAAO;AAAA,IACP;AAAA,IACA,MACE;AAAA,IAGF,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,OAAO;AAAA,MACP,MACE;AAAA,IAIJ;AAAA,EACF,CAAC;AACH;AAEA,SAAS,iBAAiB,MAAoB;AAC5C,QAAM,QAAQ,KAAK;AACnB,QAAM,OAAO;AAAA,IACX,SAAS,MAAM,IAAI,CAAC;AAAA;AAAA,wBAEA,KAAK,UAAU,GAAG,IAAI,KAAK,QAAQ,KAAK,CAAC,uBAAuB,IAAI,KAAK,QAAQ,IAAI,CAAC,YAAY,kCAAkC;AAAA,8BAC9H,KAAK,gBAAgB,GAAG,IAAI,KAAK,cAAc,KAAK,CAAC,uBAAuB,IAAI,KAAK,cAAc,IAAI,CAAC,YAAY,kCAAkC;AAAA,wBAC5J,QAAQ,sBAAsB,IAAI,KAAK,MAAM,QAAQ,CAAC,CAAC,8BAA8B,IAAI,MAAM,IAAI,CAAC,GAAG,MAAM,QAAQ,IAAI,cAAW,MAAM,KAAK,KAAK,EAAE,YAAY,kCAAkC;AAAA,uBACrM,QAAQ,IAAI,UAAU,KAAK,CAAC,IAAI,kCAA6B;AAAA,0BAC1D,QAAQ,IAAI,UAAU,KAAK,CAAC,IAAI,kCAA6B;AAAA,sBACjE,OAAO,MAAM,IAAI,MAAM,GAAG,IAAI,kCAAkC;AAAA,yBAC7D,KAAK,SAAS,IAAI,KAAK,MAAM,IAAI,kCAAkC,GAAG,KAAK,WAAW,SAAS,IAAI,yBAAyB,IAAI,KAAK,WAAW,KAAK,IAAI,CAAC,CAAC,YAAY,EAAE;AAAA;AAAA;AAGhM,SAAO,MAAM;AAAA,IACX,MAAM;AAAA,IACN,OAAO;AAAA,IACP;AAAA,IACA,MACE;AAAA,IAEF,QAAQ;AAAA,EACV,CAAC;AACH;AAEA,SAAS,SAAS,MAAoB;AACpC,QAAM,QAAQ,KAAK,QAAQ;AAC3B,QAAM,OAAO;AAAA;AAAA,MAET,SAAS,MAAM,GAAG,CAAC;AAAA,2BACE,QAAQ,qBAAqB,IAAI,KAAK,CAAC,YAAY,EAAE,qBAAqB,IAAI,KAAK,IAAI,CAAC;AAAA;AAAA;AAGjH,SAAO,MAAM;AAAA,IACX,MAAM;AAAA,IACN,OAAO;AAAA,IACP;AAAA,IACA,MACE,sGAC6B,KAAK,cAAc,YAAY,IAAI,KAAK,WAAW,CAAC,YAAY,+CAA+C;AAAA,IAC9I,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,OAAO;AAAA,MACP,MACE;AAAA,IAIJ;AAAA,EACF,CAAC;AACH;AAEA,SAAS,gBAAgB,MAAoB;AAC3C,QAAM,QAAQ,KAAK,SAAS,SAAS,KAAK;AAC1C,QAAM,SAAS,KAAK,OAAO,gBAAgB,KAAK,MAAM,aAAa,QAAQ;AAC3E,QAAM,OAAO;AAAA,IACX,SAAS,MAAM,IAAI,CAAC;AAAA,yBACC,IAAI,KAAK,CAAC,aAAa,IAAI,KAAK,IAAI,CAAC;AAAA;AAE5D,SAAO,MAAM;AAAA,IACX,MAAM;AAAA,IACN,OAAO;AAAA,IACP;AAAA,IACA,MACE;AAAA,IAIF,QAAQ;AAAA,IACR,GAAI,SACA;AAAA,MACE,MAAM;AAAA,QACJ,OAAO;AAAA,QACP,MACE,uBAAuB,KAAK,OAAO,cAAc,KAAK;AAAA,MAE1D;AAAA,IACF,IACA,CAAC;AAAA,EACP,CAAC;AACH;AAEA,SAAS,aAAa,MAAoB;AACxC,QAAM,QAAQ,KAAK,SAAS,SAAS,KAAK;AAC1C,QAAM,OAAO;AAAA,sBACO,QAAQ,IAAI,CAAC,GAAG,IAAI,KAAK,UAAU,SAAS,KAAK,IAAI,CAAC;AAAA,oBACxD,IAAI,KAAK,CAAC;AAAA,IAC1B,KAAK,gBAAgB,MAAM,IAAI,KAAK,cAAc,KAAK,CAAC,SAAS,EAAE;AAAA,IACnE,SAAS,MAAM,IAAI,CAAC;AAAA;AAEtB,SAAO,MAAM;AAAA,IACX,MAAM;AAAA,IACN,OAAO;AAAA,IACP;AAAA,IACA,MACE;AAAA,IAGF,QAAQ;AAAA,EACV,CAAC;AACH;AAEA,SAAS,eAAe,MAAoB;AAC1C,QAAM,QAAQ,KAAK,SAAS,SAAS,KAAK;AAC1C,QAAM,SAAS,oBAAoB,KAAK,KAAK,cAAc,EAAE,IAAI,KAAK,aAAa;AACnF,QAAM,OAAO,oCAAoC,SAAS,6BAA6B,IAAI,MAAM,CAAC,MAAM,EAAE;AAAA,uBACrF,IAAI,KAAK,UAAU,SAAS,KAAK,IAAI,CAAC;AAAA,oBACzC,IAAI,KAAK,CAAC;AAAA,IAC1B,KAAK,gBAAgB,MAAM,IAAI,KAAK,cAAc,KAAK,CAAC,SAAS,EAAE;AAAA,IACnE,SAAS,MAAM,IAAI,CAAC;AAAA;AAEtB,SAAO,MAAM;AAAA,IACX,MAAM;AAAA,IACN,OAAO;AAAA,IACP;AAAA,IACA,MACE,yGACiB,SAAS,yCAAyC,iEAAiE;AAAA,IACtI,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,OAAO;AAAA,MACP,MACE;AAAA,IAGJ;AAAA,EACF,CAAC;AACH;AAEA,SAAS,gBAAgB,MAAoB;AAC3C,QAAM,QAAQ,KAAK,SAAS,SAAS,KAAK;AAC1C,QAAM,OAAO;AAAA,IACX,SAAS,MAAM,IAAI,CAAC;AAAA,uCACe,IAAI,KAAK,CAAC,OAAO,KAAK,gBAAgB,qBAAqB,IAAI,KAAK,cAAc,KAAK,CAAC,SAAS,EAAE,SAAS,IAAI,KAAK,IAAI,CAAC;AAAA,yBACxI,IAAI,KAAK,GAAG,CAAC;AACpC,SAAO,MAAM;AAAA,IACX,MAAM;AAAA,IACN,OAAO;AAAA,IACP;AAAA,IACA,MACE;AAAA,IAIF,QAAQ;AAAA,EACV,CAAC;AACH;AAYA,SAAS,cAAc,QAA+B;AACpD,QAAM,OAAO,OAAO,OAAO,CAAC,MAAM,cAAc,KAAK,CAAC,MAAM,EAAE,OAAO,WAAW,CAAC,CAAC,CAAC;AACnF,QAAM,SAAS,OAAO,SAAS,KAAK;AACpC,QAAM,OACJ,SAAS,IACL,mBAAmB,MAAM,iBAAiB,WAAW,IAAI,KAAK,GAAG,oEACjE;AACN,MAAI,KAAK,WAAW,GAAG;AACrB,WAAO,6NAAwN,IAAI;AAAA,EACrO;AACA,QAAM,OAAO,KACV,IAAI,CAAC,UAAU;AAGd,UAAM,WAAW,cAAc,QAAQ,MAAM,WAAW;AACxD,WAAO,mBAAmB,IAAI,MAAM,QAAQ,CAAC;AAAA,sBAC7B,IAAI,MAAM,MAAM,CAAC;AAAA,sBACjB,UAAU,IAAI,MAAM,OAAO,CAAC,CAAC;AAAA,sBAC7B,IAAI,MAAM,QAAQ,CAAC,GAAG,MAAM,QAAQ,SAAM,IAAI,MAAM,KAAK,CAAC,KAAK,EAAE,GAAG,WAAW,kBAAe,UAAU,IAAI,QAAQ,CAAC,CAAC,KAAK,EAAE;AAAA;AAAA,EAE/I,CAAC,EACA,KAAK,IAAI;AACZ,SAAO,yEAAyE,IAAI,QAAQ,IAAI;AAClG;AAEA,SAAS,YAAY,QAAoC;AACvD,QAAM,OACJ;AAEF,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO,yNAAoN,IAAI;AAAA,EACjO;AACA,QAAM,QAAQ,OACX,IAAI,CAAC,UAAU;AACd,UAAM,QAAQ,MAAM,aAChB,IAAI,MAAM,KAAK,IAAI,MAAM,UAAU,KACnC,IAAI,MAAM,KAAK,IAAI,MAAM,MAAM,KAAK,IAAI,KAAK,UAAU;AAC3D,WAAO,oBAAoB,MAAM,aAAa,SAAS,EAAE,KAAK,IAAI,KAAK,CAAC;AAAA,EAC1E,CAAC,EACA,KAAK,EAAE;AACV,QAAM,QAAQ,OAAO,KAAK,CAAC,UAAU,MAAM,SAAS,IAAI;AACxD,QAAM,SAAS,QAAQ,WAAW,MAAM,IAAI,IAAI;AAChD,QAAM,OACJ,WAAW,SACP,QAAQ,IAAI,OAAO,CAAC,GAAG,IAAI,MAAM,GAAG,GAAI,KAAK,EAAE,CAAC,WAChD,QAAQ,IAAI,MAAM,CAAC;AACzB,SAAO,qHAAqH,KAAK,SAAS,IAAI,GAAG,IAAI;AACvJ;AAEA,SAAS,cAAc,MAAoB;AACzC,MAAI,KAAK,UAAU,SAAS,UAAU;AACpC,WAAO;AAAA,EACT;AACA,SAAO,KAAK,UAAU,YAClB,iEAAiE,KAAK,UAAU,mBAAmB,KAAK,KAAK,UAAU,gBAAgB,MAAM,EAAE,kEAC/I;AACN;AAWA,SAAS,UAAU,OAAuB;AACxC,MAAI,OAAO,MAAM,cAAc,YAAY;AACzC,WAAO,CAAC,GAAG,IAAI,KAAK,UAAU,QAAW,EAAE,aAAa,WAAW,CAAC,EAAE,QAAQ,KAAK,CAAC,EAAE;AAAA,EACxF;AACA,SAAO,CAAC,GAAG,KAAK,EAAE;AACpB;AAqBA,SAAS,kBAAkB,SAAkB,UAA6B;AACxE,MAAI,SAAS,SAAS,EAAG,QAAO;AAChC,QAAM,OAAO,SACV,IAAI,CAAC,MAAM;AACV,UAAM,OAAO,EAAE,UAAU,QAAQ;AACjC,WAAO,MAAM,OAAO,kBAAkB,EAAE;AAAA,oBAC1B,OAAO,YAAO,EAAE,GAAG,IAAI,EAAE,MAAM,CAAC;AAAA,mBACjC,EAAE,WAAW,IAAI,EAAE,QAAQ,IAAI,wCAAwC;AAAA,QAClF,OAAO,IAAI,EAAE,KAAK,IAAI,kBAAkB,EAAE,KAAK,KAAK,IAAI,EAAE,KAAK,CAAC,MAAM;AAAA,oBAC1D,UAAU,EAAE,KAAK,CAAC;AAAA,oBAClB,EAAE,cAAc,UAAU,EAAE,WAAW,IAAI,kCAA6B;AAAA;AAAA,EAExF,CAAC,EACA,KAAK,IAAI;AACZ,SAAO,4CAA4C,SAAS,MAAM;AAAA;AAAA;AAAA,SAG3D,IAAI;AAAA;AAAA;AAGb;AA6BA,SAAS,aAAa,MAAwB;AAC5C,QAAM,MAAM,KAAK,QAAQ,GAAG;AAC5B,QAAM,WAAW,QAAQ,KAAK,OAAO,KAAK,MAAM,GAAG,GAAG;AACtD,QAAM,SAAS,QAAQ,KAAK,KAAK,KAAK,MAAM,GAAG;AAC/C,QAAME,YAAW,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AACnD,MAAI,WAAW,GAAI,QAAOA;AAC1B,SAAOA,UAAS,WAAW,IACvB,CAAC,MAAM,IACP,CAAC,GAAGA,UAAS,MAAM,GAAG,EAAE,GAAG,GAAGA,UAAS,GAAG,EAAE,CAAC,GAAG,MAAM,EAAE;AAC9D;AASA,SAAS,eAAe,OAA4B;AAClD,QAAM,OAAkB,EAAE,UAAU,CAAC,GAAG,UAAU,oBAAI,IAAI,GAAG,OAAO,EAAE;AACtE,QAAM,QAAQ,CAAC,MAAM,UAAU;AAC7B,QAAI,OAAO;AACX,SAAK;AACL,eAAW,WAAW,aAAa,IAAI,GAAG;AACxC,YAAM,QAAmB,KAAK,SAAS,IAAI,OAAO,KAAK;AAAA,QACrD,UAAU,CAAC,OAAO;AAAA,QAClB,UAAU,oBAAI,IAAI;AAAA,QAClB,OAAO;AAAA,MACT;AACA,WAAK,SAAS,IAAI,SAAS,KAAK;AAChC,YAAM;AACN,aAAO;AAAA,IACT;AAIA,SAAK,UAAU;AAAA,EACjB,CAAC;AACD,SAAO,EAAE,GAAG,MAAM,UAAU,aAAa,KAAK,QAAQ,EAAE;AAC1D;AAGA,SAAS,aAAa,UAA0D;AAC9E,SAAO,IAAI,IAAI,CAAC,GAAG,QAAQ,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC;AAC3E;AAEA,SAAS,UAAU,MAA4B;AAC7C,MAAI,UAAU;AAGd,SAAO,QAAQ,UAAU,UAAa,QAAQ,SAAS,SAAS,GAAG;AACjE,UAAM,OAAO,CAAC,GAAG,QAAQ,SAAS,OAAO,CAAC,EAAE,CAAC;AAC7C,cAAU,EAAE,GAAG,MAAM,UAAU,CAAC,GAAG,QAAQ,UAAU,GAAG,KAAK,QAAQ,EAAE;AAAA,EACzE;AACA,SAAO,EAAE,GAAG,SAAS,UAAU,aAAa,QAAQ,QAAQ,EAAE;AAChE;AAgBA,SAAS,UAAU,MAAiB,MAAe,YAAY,IAAY;AACzE,QAAM,QAAQ,CAAC,GAAG,KAAK,SAAS,OAAO,CAAC,EAAE,IAAI,CAAC,UAAU;AACvD,UAAM,QAAQ,IAAI,MAAM,SAAS,IAAI,CAAC,YAAY,IAAI,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC;AACzE,QAAI,MAAM,SAAS,SAAS,GAAG;AAC7B,aAAO,MAAM,UAAU,SACnB,0BAA0B,KAAK,iBAC/B,mCAAmC,MAAM,KAAK,KAAK,KAAK;AAAA,IAC9D;AAIA,UAAM,OACJ,MAAM,UAAU,SACZ,6BACA,6BAA6B,MAAM,KAAK;AAC9C,WAAO,eAAe,OAAO,UAAU,EAAE,+BAA+B,KAAK,0BAA0B,MAAM,KAAK,UAAU,IAAI;AAAA,EAClI,UAAU,OAAO,IAAI,CAAC;AAAA,EACtB,CAAC;AACD,SAAO,MAAM,YAAY,WAAW,SAAS,MAAM,EAAE,IAAI,MAAM,KAAK,IAAI,CAAC;AAC3E;AAQA,IAAM,gBAAgB;AAGtB,SAAS,SAAS,KAOP;AACT,QAAM,MACJ,IAAI,YAAY,IAAI,SAAS,IAAI,YAAY,cAAc,IAAI,SAAS,KAAK;AAC/E,MAAI,IAAK,QAAO,GAAG,GAAG,GAAG,IAAI,UAAU,MAAM,KAAK,IAAI,SAAS,GAAG,CAAC,KAAK,EAAE;AAC1E,SAAO,OAAO,IAAI,OAAO,GAAG,GAAG,IAAI,OAAO,WAAM,KAAK,IAAI,MAAM,GAAG,CAAC,KAAK,EAAE;AAC5E;AASA,SAAS,eAAe,YAAgC;AACtD,QAAM,QAAQ,WAAW;AACzB,MAAI,CAAC,OAAO;AACV,UAAM,MACJ,WAAW,UAAU,SAAS,WAC1B,uFACA;AACN,WAAO,4FAAuF,GAAG;AAAA,EACnG;AAEA,QAAM,QAAQ;AAAA,IACZ,GAAG,MAAM,cAAc,IAAI,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,SAAS,CAAC,EAAE,EAAE;AAAA,IACpE,GAAG,MAAM,cAAc,IAAI,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,SAAS,CAAC,EAAE,EAAE;AAAA,IACpE,GAAG,MAAM,aAAa,IAAI,CAAC,OAAO,EAAE,MAAM,UAAK,MAAM,SAAS,CAAC,EAAE,EAAE;AAAA,IACnE,GAAG,MAAM,aAAa,IAAI,CAAC,OAAO,EAAE,MAAM,UAAK,MAAM,SAAS,CAAC,EAAE,EAAE;AAAA,IACnE,GAAI,MAAM,eAAe,CAAC,EAAE,MAAM,KAAK,MAAM,2CAAsC,CAAC,IAAI,CAAC;AAAA,IACzF,GAAI,MAAM,kBACN,CAAC,EAAE,MAAM,KAAK,MAAM,6CAAwC,CAAC,IAC7D,CAAC;AAAA,IACL,GAAI,MAAM,oBAAoB,IAC1B,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,iBAAiB,oCAAoC,CAAC,IACnF,CAAC;AAAA,EACP;AAEA,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,MACV;AAAA,IACC,CAAC,MACC,cAAc,EAAE,SAAS,MAAM,UAAU,SAAS,wBAAwB,EAAE,IAAI,UAAU,IAAI,EAAE,IAAI,CAAC;AAAA,EACzG,EACC,KAAK,IAAI;AACZ,SAAO;AAAA,oBACW,IAAI;AAAA;AAExB;AAEA,SAAS,YACP,YACA,QACA,OACA,UACQ;AACR,QAAM,OAAO,OAAO,UAAU;AAC9B,QAAM,WAAW;AAAA,IACf,iBAAiB,IAAI;AAAA,IACrB,cAAc,IAAI;AAAA,IAClB,SAAS,IAAI;AAAA,IACb,gBAAgB,IAAI;AAAA,IACpB,aAAa,IAAI;AAAA,IACjB,eAAe,IAAI;AAAA,IACnB,gBAAgB,IAAI;AAAA,EACtB,EAAE,KAAK,IAAI;AACX,SAAO,kCAAkC,KAAK;AAAA;AAAA,UAEtC,IAAI,KAAK,IAAI,CAAC;AAAA,qBACH,IAAI,KAAK,GAAG,CAAC;AAAA,sBACZ,KAAK,MAAM,SAAM,IAAI,cAAc,IAAI,CAAC,CAAC,GAAG,KAAK,SAAS,SAAM,IAAI,KAAK,MAAM,CAAC,KAAK,EAAE;AAAA;AAAA,0BAEnF,QAAQ;AAAA,IAC9B,cAAc,MAAM,CAAC;AAAA,IACrB;AAAA,IACA,SAAS,KAAK,CAAC,QAAQ,IAAI,UAAU,KAAK;AAAA,IAC1C;AAAA,EACF,CAAC;AAAA,IACC,eAAe,UAAU,CAAC;AAAA,IAC1B,YAAY,KAAK,MAAM,CAAC;AAAA;AAE5B;AAIA,IAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2IP,SAAS,cAAc,QAAsB,UAAgC,CAAC,GAAW;AAC9F,QAAM,QAAQ,QAAQ,SAAS,yBAAoBD,QAAO,OAAO,GAAG,CAAC;AACrE,QAAM,cAAc,OAAO,eAAe,CAAC;AAC3C,QAAM,eAAe,oBAAI,IAA2B;AACpD,aAAW,SAAS,CAAC,GAAG,OAAO,WAAW,GAAG,OAAO,UAAU,GAAG;AAC/D,UAAM,SAAS,aAAa,IAAI,MAAM,OAAO;AAC7C,QAAI,OAAQ,QAAO,KAAK,KAAK;AAAA,QACxB,cAAa,IAAI,MAAM,SAAS,CAAC,KAAK,CAAC;AAAA,EAC9C;AAIA,QAAM,QACJ,OAAO,gBAAgB,SACnB,+LACA;AAKN,QAAM,OAAkB,YAAY,IAAI,CAAC,YAAY,UAAU;AAC7D,UAAM,OAAO,OAAO,WAAW,KAAK,QAAQ;AAC5C,WAAO;AAAA,MACL;AAAA,MACA,QAAQ,WAAW,IAAI,EAAE;AAAA,MACzB;AAAA,MACA,UAAU,WAAW,UAAU,QAAQ;AAAA,MACvC,OAAO,OAAO,UAAU,EAAE,SAAS,SAAS;AAAA,MAC5C,aAAa,OAAO,UAAU,EAAE,eAAe,SAAS;AAAA,IAC1D;AAAA,EACF,CAAC;AACD,QAAM,UAAU,oBAAI,IAAuB;AAC3C,cAAY,QAAQ,CAAC,YAAY,UAAU;AACzC,UAAM,QAAQ,WAAW,OAAO,WAAW,KAAK,QAAQ,CAAC,EAAE;AAC3D,YAAQ,IAAI,OAAO,CAAC,GAAI,QAAQ,IAAI,KAAK,KAAK,CAAC,GAAI,KAAK,KAAK,CAAE,CAAC;AAAA,EAClE,CAAC;AAED,QAAM,OACJ,YAAY,WAAW,IACnB,yCAAyC,KAAK,mBAC9C,YACG;AAAA,IAAI,CAAC,YAAY,UAChB;AAAA,MACE;AAAA,MACA,aAAa,IAAI,WAAW,KAAK,QAAQ,KAAK,CAAC;AAAA,MAC/C;AAAA,MACA,QAAQ,IAAI,WAAW,OAAO,WAAW,KAAK,QAAQ,CAAC,EAAE,KAAK,KAAK,CAAC;AAAA,IACtE;AAAA,EACF,EACC,KAAK,IAAI;AAElB,QAAM,MACJ,YAAY,SAAS,IACjB;AAAA,uBACe,YAAY,MAAM;AAAA,IACrC;AAAA,IACA,eAAe,YAAY,IAAI,CAAC,eAAe,OAAO,WAAW,KAAK,QAAQ,CAAC,CAAC;AAAA,IAChF,YAAY,UAAU;AAAA,IACtB;AAAA,EACF,CAAC;AAAA,UAEK;AAEN,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAOA,IAAI,KAAK,CAAC;AAAA,SACV,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,QAKN,IAAI,KAAK,CAAC;AAAA,+CAC6B,IAAI,OAAO,GAAG,CAAC;AAAA,IAC1D,GAAG;AAAA;AAAA,EAEL,IAAI;AAAA,sBACgB,IAAI,GAAG,YAAY,MAAM,QAAQ,YAAY,WAAW,IAAI,KAAK,GAAG,iBAAc,OAAO,UAAU,iBAAc,OAAO,OAAO,cAAW,CAAC;AAAA;AAAA;AAAA;AAAA;AAKjK;;;AC5jCO,IAAM,eAAe;AAuFrB,SAAS,UAAU,QAAqC;AAC7D,SAAO;AAAA,IACL,KAAK,OAAO;AAAA,IACZ,YAAY,OAAO;AAAA,IACnB,SAAS,OAAO;AAAA,IAChB,SAAS,OAAO,QAAQ;AAAA,IACxB,QAAQ;AAAA,MACN,aAAa,OAAO,QAAQ;AAAA,MAC5B,qBAAqB,OAAO,QAAQ;AAAA,MACpC,WAAW,OAAO,QAAQ;AAAA,MAC1B,YAAY,OAAO,QAAQ;AAAA,MAC3B,kBAAkB,OAAO,QAAQ;AAAA,MACjC,cAAc,OAAO,YAAY;AAAA,MACjC,cAAc,OAAO,YAAY;AAAA,MACjC,aAAa,OAAO,YAAY;AAAA,IAClC;AAAA,IACA,kBAAkB,OAAO;AAAA,IACzB,aAAa,YAAY,MAAM;AAAA,IAC/B,cAAc;AAAA,MACZ,OAAO,OAAO,oBAAoB;AAAA,MAClC,aAAa,kBAAkB,MAAM;AAAA,IACvC;AAAA,IACA,WAAW,aAAa,OAAO,SAAS;AAAA,IACxC,YAAY,aAAa,OAAO,UAAU;AAAA,IAC1C,GAAI,OAAO,cAAc,EAAE,aAAa,EAAE,OAAO,OAAO,YAAY,MAAM,EAAE,IAAI,CAAC;AAAA,IACjF,GAAI,OAAO,aAAa,EAAE,YAAY,OAAO,WAAW,IAAI,CAAC;AAAA,IAC7D,WAAW,OAAO,YAAY;AAAA,IAC9B,UAAU,OAAO,YAAY;AAAA,EAC/B;AACF;AAEA,SAAS,YAAY,QAAoC;AACvD,QAAM,SAAS,oBAAI,IAAwB;AAC3C,aAAW,QAAQ,OAAO,aAAa;AACrC,UAAM,MAAM,GAAG,KAAK,IAAI,KAAS,KAAK,OAAO;AAC7C,UAAM,WAAW,OAAO,IAAI,GAAG;AAC/B,QAAI,UAAU;AACZ,eAAS,SAAS;AAClB,UAAI,SAAS,MAAM,SAAS,aAAc,UAAS,MAAM,KAAK,KAAK,OAAO;AAAA,UACrE,UAAS,aAAa;AAAA,IAC7B,OAAO;AACL,aAAO,IAAI,KAAK;AAAA,QACd,MAAM,KAAK;AAAA,QACX,SAAS,KAAK;AAAA,QACd,QAAQ,KAAK;AAAA,QACb,QAAQ,KAAK;AAAA,QACb,OAAO;AAAA,QACP,OAAO,CAAC,KAAK,OAAO;AAAA,QACpB,WAAW;AAAA,MACb,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO,CAAC,GAAG,OAAO,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAC9F;AAQA,SAAS,aAAa,QAA0D;AAC9E,QAAM,SAAS,oBAAI,IAAuB;AAC1C,aAAW,SAAS,QAAQ;AAC1B,UAAM,WAAW,OAAO,IAAI,MAAM,MAAM;AACxC,QAAI,UAAU;AACZ,eAAS,SAAS;AAClB,UAAI,SAAS,MAAM,SAAS,aAAc,UAAS,MAAM,KAAK,MAAM,OAAO;AAAA,UACtE,UAAS,aAAa;AAAA,IAC7B,OAAO;AACL,aAAO,IAAI,MAAM,QAAQ;AAAA,QACvB,QAAQ,MAAM;AAAA,QACd,UAAU,MAAM;AAAA,QAChB,OAAO,MAAM;AAAA,QACb,KAAK,MAAM;AAAA,QACX,KAAK,MAAM;AAAA,QACX,QAAQ,MAAM;AAAA,QACd,OAAO;AAAA,QACP,OAAO,CAAC,MAAM,OAAO;AAAA,QACrB,WAAW;AAAA,MACb,CAAC;AAAA,IACH;AAAA,EACF;AACA,QAAM,UAAoC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,EAAE;AAC1E,SAAO,CAAC,GAAG,OAAO,OAAO,CAAC,EAAE;AAAA,IAC1B,CAAC,GAAG,MAAM,QAAQ,EAAE,QAAQ,IAAI,QAAQ,EAAE,QAAQ,KAAK,EAAE,OAAO,cAAc,EAAE,MAAM;AAAA,EACxF;AACF;AAEA,SAAS,kBAAkB,QAA2C;AACpE,QAAM,SAAS,oBAAI,IAAwC;AAC3D,aAAW,SAAS,OAAO,oBAAoB,aAAa;AAC1D,UAAM,WAAW,OAAO,IAAI,MAAM,IAAI;AACtC,QAAI,UAAU;AACZ,eAAS,SAAS;AAClB,UAAI,SAAS,MAAM,SAAS,aAAc,UAAS,MAAM,KAAK,MAAM,GAAG;AAAA,UAClE,UAAS,aAAa;AAAA,IAC7B,OAAO;AACL,aAAO,IAAI,MAAM,MAAM;AAAA,QACrB,MAAM,MAAM;AAAA,QACZ,OAAO;AAAA,QACP,QAAQ,MAAM;AAAA,QACd,OAAO,CAAC,MAAM,GAAG;AAAA,QACjB,WAAW;AAAA,MACb,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO,CAAC,GAAG,OAAO,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAC9F;;;AC1LA,IAAM,SAAS,CAAC,UAAK,UAAK,UAAK,UAAK,UAAK,UAAK,UAAK,UAAK,UAAK,QAAG;AAChE,IAAM,YAAY;AAElB,IAAME,QAAO;AAAA,EACX,OAAO;AAAA,EACP,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AACR;AAEA,IAAM,cAAsD;AAAA,EAC1D,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AACT;AAEA,IAAM,eAAuD;AAAA,EAC3D,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AACT;AAEA,IAAM,kBAA0D;AAAA,EAC9D,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AACT;AAEO,IAAM,SAAN,MAAa;AAAA,EACD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,WAAW;AAAA,EACX,cAAc;AAAA,EACd,cAAgE,CAAC;AAAA,EAEzE,YAAY,UAAyB,CAAC,GAAG;AACvC,SAAK,SAAS,QAAQ,UAAU,QAAQ;AACxC,SAAK,QAAQ,KAAK,OAAO,UAAU;AACnC,SAAK,QAAQ,QAAQ,SAAS,KAAK;AACnC,SAAK,OAAO,QAAQ,QAAQ;AAC5B,SAAK,oBAAoB,QAAQ,qBAAqB;AACtD,SAAK,aAAa,QAAQ,cAAc;AACxC,SAAK,MAAM,QAAQ,OAAO,KAAK;AAAA,EACjC;AAAA,EAEQ,MAAM,MAAc,MAAsB;AAChD,WAAO,KAAK,QAAQ,GAAG,IAAI,GAAG,IAAI,GAAGA,MAAK,KAAK,KAAK;AAAA,EACtD;AAAA;AAAA,EAGA,KAAK,SAAuB;AAC1B,QAAI,KAAK,SAAS,QAAS;AAC3B,SAAK,UAAU;AACf,SAAK,OAAO,MAAM,GAAG,KAAK,MAAMA,MAAK,KAAK,OAAO,CAAC;AAAA,CAAI;AACtD,QAAI,KAAK,SAAS,aAAa,KAAK,KAAM,MAAK,OAAO;AAAA,EACxD;AAAA;AAAA,EAGA,aAAa,CAAC,UAA+B;AAC3C,QAAI,KAAK,SAAS,QAAS;AAC3B,SAAK,OAAO;AAEZ,QAAI,KAAK,SAAS,WAAW;AAC3B,WAAK,YAAY,KAAK;AACtB;AAAA,IACF;AAGA,QAAI,KAAK,OAAO;AACd,WAAK,YAAY;AACjB,WAAK,OAAO;AAAA,IACd,OAAO;AACL,WAAK,eAAe,KAAK;AAAA,IAC3B;AAAA,EACF;AAAA;AAAA,EAGA,OAAa;AACX,QAAI,KAAK,OAAO;AACd,oBAAc,KAAK,KAAK;AACxB,WAAK,QAAQ;AAAA,IACf;AACA,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAIQ,cAAoB;AAC1B,QAAI,KAAK,SAAS,KAAK,qBAAqB,EAAG;AAC/C,SAAK,QAAQ,YAAY,MAAM;AAC7B,WAAK,SAAS,KAAK,QAAQ,KAAK,OAAO;AACvC,WAAK,OAAO;AAAA,IACd,GAAG,KAAK,iBAAiB;AAEzB,IAAC,KAAK,MAAiC,QAAQ;AAAA,EACjD;AAAA,EAEQ,SAAe;AACrB,QAAI,CAAC,KAAK,KAAM;AAChB,UAAM,OAAO,KAAK,YAAY,KAAK,IAAI;AACvC,QAAI,KAAK,OAAO;AACd,WAAK,OAAO,MAAM,YAAY,IAAI,EAAE;AACpC,WAAK,WAAW;AAAA,IAClB,OAAO;AACL,WAAK,OAAO,MAAM,GAAG,IAAI;AAAA,CAAI;AAAA,IAC/B;AAAA,EACF;AAAA,EAEQ,YAAY,GAA0B;AAC5C,UAAM,UAAU,KAAK,MAAMA,MAAK,MAAM,OAAO,KAAK,KAAK,CAAE;AACzD,UAAM,QAAQ,KAAK,MAAMA,MAAK,MAAM,YAAY,EAAE,KAAK,CAAC;AACxD,UAAM,SAAS,GAAG,EAAE,IAAI,IAAI,EAAE,KAAK;AACnC,UAAM,MAAM,KAAK,IAAI,EAAE,MAAM,EAAE,KAAK;AACpC,UAAM,UAAU,EAAE,MAAM,KAAK,MAAMA,MAAK,KAAK,KAAK,SAAS,EAAE,GAAG,CAAC,IAAI;AACrE,WAAO,CAAC,SAAS,OAAO,KAAK,QAAQ,OAAO,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AAAA,EACzE;AAAA,EAEQ,IAAI,MAAc,OAAuB;AAC/C,QAAI,SAAS,EAAG,QAAO;AACvB,UAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,OAAO,KAAK,CAAC;AACnD,UAAM,SAAS,KAAK,MAAM,QAAQ,SAAS;AAC3C,UAAM,MAAM,SAAI,OAAO,MAAM,IAAI,SAAI,OAAO,YAAY,MAAM;AAC9D,WAAO,KAAK,MAAMA,MAAK,KAAK,IAAI,GAAG,GAAG;AAAA,EACxC;AAAA,EAEQ,YAAY,GAAwB;AAC1C,QAAI,CAAC,KAAK,YAAY,EAAE,KAAK,GAAG;AAC9B,WAAK,YAAY,EAAE,KAAK,IAAI;AAC5B,WAAK,OAAO,MAAM,GAAG,KAAK,MAAMA,MAAK,MAAM,aAAa,EAAE,KAAK,CAAC,CAAC;AAAA,CAAI;AAAA,IACvE;AACA,QAAI,EAAE,UAAU,WAAW,EAAE,KAAK;AAChC,YAAM,SAAS,KAAK,MAAMA,MAAK,KAAK,IAAI,EAAE,IAAI,IAAI,EAAE,KAAK,GAAG;AAC5D,WAAK,OAAO,MAAM,KAAK,KAAK,UAAU,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,KAAK,MAAM;AAAA,CAAI;AAAA,IACzE,WAAW,EAAE,UAAU,UAAU,EAAE,UAAU,SAAS;AAEpD,UAAI,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,GAAG;AACrC,aAAK,OAAO,MAAM,KAAK,KAAK,MAAMA,MAAK,KAAK,GAAG,EAAE,KAAK,IAAI,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC;AAAA,CAAI;AAAA,MAC3F;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,UAAU,QAAoC;AACpD,QAAI,WAAW,OAAW,QAAO,KAAK,MAAMA,MAAK,KAAK,cAAK;AAC3D,QAAI,UAAU,OAAO,SAAS,IAAK,QAAO,KAAK,MAAMA,MAAK,OAAO,OAAO,MAAM,CAAC;AAC/E,QAAI,UAAU,OAAO,SAAS,IAAK,QAAO,KAAK,MAAMA,MAAK,QAAQ,OAAO,MAAM,CAAC;AAChF,WAAO,KAAK,MAAMA,MAAK,KAAK,OAAO,MAAM,CAAC;AAAA,EAC5C;AAAA,EAEQ,eAAe,GAAwB;AAC7C,UAAM,IAAI,KAAK,IAAI;AACnB,UAAM,OAAO,EAAE,SAAS,EAAE;AAC1B,QAAI,CAAC,QAAQ,IAAI,KAAK,cAAc,KAAK,WAAY;AACrD,SAAK,cAAc;AACnB,SAAK,OAAO,MAAM,WAAW,YAAY,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,KAAK;AAAA,CAAI;AAAA,EAC5E;AAAA,EAEQ,YAAkB;AACxB,QAAI,KAAK,SAAS,KAAK,UAAU;AAC/B,WAAK,OAAO,MAAM,WAAW;AAC7B,WAAK,WAAW;AAAA,IAClB;AAAA,EACF;AAAA,EAEQ,SAAS,KAAqB;AACpC,UAAM,QAAQ,KAAK,OAAO,WAAW;AAErC,UAAM,SAAS,KAAK,IAAI,IAAI,QAAQ,EAAE;AACtC,QAAI,IAAI,UAAU,OAAQ,QAAO;AACjC,WAAO,SAAI,IAAI,MAAM,IAAI,SAAS,SAAS,CAAC,CAAC;AAAA,EAC/C;AACF;","names":["DEFAULT_UA","titleMissing","canonicalMissing","hreflangMissing","probes","pathOf","rowOf","count","segments","count","cheerio","relaxTlsIfRequested","relaxTlsIfRequested","DEFAULTS","resolve","normalise","sorted","count","cheerio","resolveBase","DEFAULTS","hostOf","resolve","sameOrigin","favicon","entry","ANSI","paint","VERDICT_FLAG","verdictColor","stripBackticks","RATIO_IDEAL","RATIO_ACCEPTABLE","hostOf","segments","ANSI"]}