/**
 * Content health scanner — produces the issue rows that power the
 * dashboard "Content Health" card.
 *
 * ## Why a separate module from the cron / endpoint
 *
 * The scanner is pure logic — give it a document, get back an array
 * of issues. The cron drives it across the corpus and persists the
 * results; the endpoint reads the persisted results. Keeping the
 * scanner free of I/O dependencies makes it trivially testable
 * (~10 µs per call, no DB) and lets future callers (CI checks, a
 * pre-publish hook, etc.) reuse it without dragging in cron
 * infrastructure.
 *
 * ## Why HEAD probes for the link checker
 *
 * Internal links 404 in two flavors: a hard 404 from the framework
 * (page genuinely doesn't exist) and a soft 404 where the framework
 * returns 200 with a "not found" template. We only catch the first.
 * That's fine — the dashboard's job is to surface the high-confidence
 * issues, not chase ambiguous soft-404 heuristics. The scanner can
 * be extended with response-body matching later if the editor
 * feedback warrants it.
 *
 * ## Why we count alt-less images per document, not per image
 *
 * The dashboard surfaces total issue counts. Storing one row per
 * image would blow up the issue table for pages with 20+
 * unannotated photos and provide no extra value (the editor still
 * gets a single "12 images need alt text" row in the UI). We
 * collapse to one row per (document, type) with `count` carrying
 * the per-document magnitude.
 */
export type ContentIssueKind = 'MISSING_META_DESCRIPTION' | 'MISSING_ALT_TEXT' | 'BROKEN_INTERNAL_LINK' | 'OUTDATED_CONTENT';
export interface ScannerInput {
    id: string;
    /**
     * Document's `data` JSON. Fields we read:
     *   - `metaDescription`, `seoDescription` (optional)
     *   - `body`, `content`, `richText`, `html` (any string field used
     *     as the primary HTML payload — we try each, longest wins)
     */
    data: Record<string, unknown>;
    updatedAt: Date;
    publishedAt?: Date | null;
}
export interface ScannerIssue {
    type: ContentIssueKind;
    count: number;
    details?: Record<string, unknown>;
}
export interface ScannerOptions {
    /**
     * Cut-off (in days) for the OUTDATED_CONTENT check. A document
     * whose `updatedAt` is older than this is flagged. Default 90.
     */
    outdatedDays?: number;
    /**
     * Link-checker callback. Resolves to `true` if the link is
     * reachable, `false` if broken. Pass `null` to disable the
     * link-check (useful in tests). The default cron supplies
     * `checkInternalLinkReachable` from this module.
     */
    checkLink?: ((url: string) => Promise<boolean>) | null;
    /**
     * Origin used to resolve relative URLs and identify
     * "internal" links. Required for the link checker — passing
     * `null` here is the same as passing `checkLink: null` (no
     * link probing).
     */
    siteOrigin?: string | null;
}
/**
 * Count <img> tags in the HTML that don't carry an `alt` attribute.
 * Matches the existing a11y rule in `a11y/index.ts` so the scanner
 * agrees with the per-document audit panel.
 */
export declare function countMissingAltImages(html: string): number;
/**
 * Extract candidate URLs from <a href="..."> attributes. Returns
 * the raw href values; classification (internal vs external,
 * absolute vs relative) happens in the caller.
 */
export declare function extractLinks(html: string): string[];
/**
 * Decide whether a URL is "internal" relative to `siteOrigin`.
 * Internal links include:
 *   - relative paths: `/about`, `./page`, `../foo`
 *   - same-origin absolute URLs: `https://example.com/page`
 *
 * Excluded:
 *   - external URLs (different origin)
 *   - hash-only anchors (`#section`)
 *   - non-http schemes (`mailto:`, `tel:`, `javascript:`, etc.)
 *   - protocol-relative URLs to a different host (`//other.com/x`)
 */
export declare function classifyLink(url: string, siteOrigin: string): {
    isInternal: boolean;
    resolved: string | null;
};
/**
 * Single-link reachability probe. Default implementation used by
 * the cron — issues a HEAD with a 5s timeout, falls back to GET if
 * HEAD returns 405/501 (some frameworks reject HEAD).
 */
export declare function checkInternalLinkReachable(url: string, timeoutMs?: number): Promise<boolean>;
/**
 * Run all checks on a single document. The link checker is
 * concurrency-bounded to `LINK_CHECK_CONCURRENCY` per document so a
 * page with 200 links doesn't open 200 sockets at once.
 *
 * Each issue type appears at most once in the returned array. The
 * shape `{ type, count, details }` lets the caller upsert by
 * `(documentId, type)` and rely on `count` for the magnitude
 * surfaced in the dashboard.
 */
export declare function scanDocument(doc: ScannerInput, opts?: ScannerOptions): Promise<ScannerIssue[]>;
//# sourceMappingURL=scanner.d.ts.map