/** * v0.4 Site Classifier — runs BEFORE rules to gate which rule set applies. * * Today the engine runs every rule against every audited site regardless of * whether the site is actually programmatic-SEO. A 23-page marketing site * gets pSEO-targeted findings that don't apply; a 50,000-page directory * gets the same audit shape as a small blog. This pre-flight classifier * decides which "kind" of site we're auditing and emits a list of ruleIds * that the dispatcher will skip when site type is small-marketing/blog. * * Heuristics (v1, ships in v0.4): * - Sitemap URL count: <50 = bias small/blog; ≥1000 = bias programmatic. * - URL-pattern clustering: normalize numeric/slug segments; if top-3 * templates cover ≥60% of URLs → strong programmatic signal. * - Framework signal: pass-through from dev-server detection. * * Deferred to v0.4.1+ (per spec §4.11): * - DOM-skeleton hashing across pages. * - Per-cluster classification (mixed pSEO + marketing). * - Per-page applicability tagging on findings. */ export type SiteType = "programmatic-directory" | "small-marketing" | "blog" | "ecommerce" | "docs" | "unclear"; export type ClassificationSignal = { kind: "sitemap-url-count"; value: number; } | { kind: "url-pattern-cluster-coverage"; topTemplate: string; pages: number; ratio: number; } | { kind: "framework-detected"; value: "nextjs" | "vite" | "astro" | "unknown"; } /** * v0.5.3 — emitted when `applyDegenerationGuard` downgrades a * `small-marketing` or `blog` classification to `unclear` because the * corpus is degenerate (mostly thin / mostly identical titles). Surfacing * this in `signals` lets the UI explain why severity demotions didn't * apply. */ | { kind: "degeneration-guard-tripped"; reason: "median-thin" | "title-duplicate-heavy"; value: number; }; export interface SiteClassification { type: SiteType; /** 0–1 confidence. Below 0.6 → treat as `unclear` and run all rules. */ confidence: number; /** Ordered observations that fed the classification. UI surfaces these. */ signals: ClassificationSignal[]; /** * RuleIds suppressed because of this classification. The dispatcher * checks this list before invoking each rule. * * Empty array when type is `programmatic-directory`, `ecommerce`, or * `unclear` — those run all rules. */ suppressedRules: string[]; } /** Rules suppressed for non-pSEO sites (small-marketing / blog). */ export declare const PSEO_ONLY_RULE_IDS: readonly string[]; /** * Normalize a pathname into a hashable template by replacing path segments * that look like values with type placeholders. * * Examples: * /california/los-angeles/plumbers → /:slug/:slug/:slug * /blog/hello-world → /blog/:slug * /post/12345 → /post/:n * / → / */ export declare function normalizePathToTemplate(pathname: string): string; /** Compute template-cluster ratios from a URL list. Returns top entries first. */ export declare function clusterUrlTemplates(urls: string[]): Array<{ template: string; count: number; ratio: number; }>; export interface ClassifySiteInput { /** All discovered URLs (sitemap + crawl). Unfiltered. */ urls: string[]; /** Framework detected via dev-server response headers, if any. */ framework?: "nextjs" | "vite" | "astro" | "unknown"; } /** * Classify a site from its URL list + framework signal. Pure function. * * Contract: callers must pass the FULL discovered URL list (sitemap + * crawl), not the post-sample list. The classifier needs the raw size * signal to distinguish a 5000-page directory from a 25-page sample of one. */ export declare function classifySite(input: ClassifySiteInput): SiteClassification; /** * v0.5.3 — corpus-quality guard against "small-marketing" / "blog" * classification masking degenerate sites. The `small-marketing` profile * demotes `spam/thin-content`, `aeo/citable-facts`, `aeo/freshness-signals`, * `spam/doorway-pattern` etc. to `info` to avoid false-positives on legit * 6-page marketing sites (linear.app etc). But a 6-page site with 0 unique * content per page (e.g. an un-translated language switcher pretending to be * a directory) trips the same shape and inherits the demotions, escaping * with grade B. * * This guard runs AFTER classification, with parsed-page stats. If the * corpus is degenerate (median word count < 50 OR ≥50% of pages share an * identical title), the classification is downgraded to `unclear` so the * demotion table doesn't apply — the natural rule severities then fire. * * Only `small-marketing` and `blog` are guarded. The other types either * already run all rules (`unclear`, `programmatic-directory`, `ecommerce`, * `docs`) or aren't reached by the small-corpus path. */ export declare function applyDegenerationGuard(classification: SiteClassification, corpusStats: { medianWordCount: number; identicalTitleRatio: number; pageCount: number; }): SiteClassification; /** * Compute the corpus stats `applyDegenerationGuard` consumes. Pulled out so * tests can pass a fixture stat block directly without constructing * `ParsedPage` instances. */ export declare function corpusStatsFromPages(pages: ReadonlyArray<{ title: string; contentText: string; }>): { medianWordCount: number; identicalTitleRatio: number; pageCount: number; }; //# sourceMappingURL=site-classifier.d.ts.map