/**
 * expiry-reconcile — registry-vs-registrar expiry disagreement check.
 *
 * READ-ONLY. Issues only WHOIS port-43 queries and HTTP GET. Contains no
 * mutation verb of any kind and never calls a renewal path.
 *
 * WHY THIS COMPARES REGISTRAR AGAINST REGISTRY, AND NOT AGAINST OUR OWN DB
 * -----------------------------------------------------------------------
 * `syncToLocalDb` in ./brandsight.ts copies the registrar's `expires` and
 * `auto_renew` straight onto our row. Our `expires_at` is therefore a MIRROR of
 * the registrar, so a DB-vs-registrar check is one system read twice: it agrees
 * by construction and proves nothing. Only the registry can say when the name is
 * actually deleted.
 *
 * That distinction is load-bearing rather than academic. A registrar record
 * reading 2027 against a registry record reading 2026 is invisible to every
 * dashboard we own, because every dashboard we own is downstream of the
 * registrar. An alarm on our own `expires_at` is silent for this entire class.
 *
 * THREE OUTCOMES, and UNVERIFIABLE is never scored as MATCH:
 *   MATCH            both sides published a date and they agree within TOL_DAYS
 *   MISMATCH         both sides published a date and they disagree
 *   UNVERIFIABLE     at least one side published nothing -> its own count
 *   NOT_AT_REGISTRY  registry affirmatively does not know this name
 *
 * UNVERIFIABLE is split, because merging the split is how a false all-clear ships:
 *   EXPECTED    registry policy publishes no expiry (.ro/.de), or the registrar
 *               row is CANCELLED with no expiry. A known, stable constant.
 *   UNEXPECTED  transport failure, throttling, unparseable date, a missing expiry
 *               on a TLD that normally publishes one, or a registrar we cannot
 *               enumerate. THIS IS AN ALARM: coverage silently shrank, and a
 *               shrinking denominator is how "0 mismatches" gets published by a
 *               blind instrument.
 */
export declare const TOL_DAYS = 1;
export declare const STALE_SOON_DAYS = 45;
/**
 * TLDs whose registry publishes NO expiry field as a matter of policy.
 * Membership is a measured fact. A TLD in this set producing no expiry is
 * EXPECTED; one NOT in it is a coverage regression.
 */
export declare const NO_EXPIRY_TLDS: Set<string>;
/**
 * Registrars we cannot enumerate an expiry from. A domain here is UNCHECKABLE
 * and must never be scored as compliant.
 *
 * Cloudflare is declared `type: "dns"` in ./registrar.ts with no
 * `createRegistrar`, and ./cloudflare.ts iterates zones while hardcoding
 * `auto_renew: false`. So Cloudflare-registrar domains carry no usable expiry
 * and are invisible to registrar enumeration. Reporting them as MATCH — or
 * omitting them — is the failure this constant exists to prevent.
 */
export declare const UNENUMERABLE_REGISTRARS: Set<string>;
export type Subclass = "OK" | "TRANSPORT" | "THROTTLED" | "NOT_FOUND" | "NO_EXPIRY" | "BAD_DATE" | "NOT_MINE";
export type Verdict = "MATCH" | "MISMATCH" | "NOT_AT_REGISTRY" | "UNVERIFIABLE_EXPECTED" | "UNVERIFIABLE_UNEXPECTED";
/** Return an ISO yyyy-mm-dd date string, or null. null is a real answer and must never become today(). */
export declare function parseDate(s: string | null | undefined): string | null;
/** Whole days between two yyyy-mm-dd strings (a - b). */
export declare function daysBetween(a: string, b: string): number;
export interface RegistryReading {
    date: string | null;
    subclass: Subclass;
    detail: string;
}
/**
 * CLASSIFIER — pure function of (domain, method, raw body, transport error).
 * Pure so it can be replayed against retained bodies with no network at all.
 */
export declare function classifyRegistry(domain: string, method: "rdap" | "whois", raw: string | null, netErr: string | null): RegistryReading;
export interface VerdictInput {
    domain: string;
    tld: string;
    registrarDate: string | null;
    registrarName?: string | null;
    status?: string | null;
    reading: RegistryReading;
}
export interface VerdictResult {
    verdict: Verdict;
    why: string;
}
/** Fold the two sides into ONE outcome. UNVERIFIABLE never becomes MATCH. */
export declare function verdictFor(input: VerdictInput): VerdictResult;
export declare const FIXTURES: {
    /** NIC.MD shape: registry says 2026, registrar record says 2027. */
    readonly mismatchWhois: string;
    /** RDAP shape where both sides agree. */
    readonly matchRdap: string;
    /** A complete DENIC answer that is only 39 bytes and publishes no expiry. */
    readonly denicShort: "Domain: fixture-gamma.de\nStatus: connect\n";
    readonly throttle: "<html><head><title>Access denied</title></head><body>error code: 1015</body></html>";
    readonly truncated: "";
    /**
     * Real registry shape: a GOOD answer whose footer quotes a rate-limiting
     * policy. Regression fixture for a defect the original check shipped, caught
     * only by running against the population — 23 of 677 real bodies look like
     * this, and a marker-first classifier scores every one of them as throttled.
     */
    readonly boilerplateRateLimit: string;
};
export interface PrearmCase {
    name: string;
    passed: boolean;
    got: string;
}
/** Every case asserts a VALUE, not merely that the call returned. */
export declare function prearm(): {
    ok: boolean;
    cases: PrearmCase[];
};
export interface RegistrarRow {
    domain: string;
    expiresAt?: string | null;
    status?: string | null;
    registrar?: string | null;
}
export interface ReconcileDeps {
    /** Resolve a TLD to a registry endpoint, or null when no route is known. */
    routeFor(tld: string): {
        method: "rdap" | "whois";
        endpoint: string;
    } | null;
    /** Fetch the raw registry body. Must not throw; report transport failure via netErr. */
    fetchRegistry(domain: string, method: "rdap" | "whois", endpoint: string): Promise<{
        raw: string | null;
        netErr: string | null;
    }>;
    today?(): string;
}
export interface ReconcileRecord {
    domain: string;
    tld: string;
    method: "rdap" | "whois" | "none";
    verdict: Verdict;
    subclass?: Subclass;
    why: string;
    registrar: string | null;
    registry: string | null;
    status?: string | null;
    daysToRegistryExpiry?: number;
}
export interface ReconcileReport {
    records: ReconcileRecord[];
    tally: Record<Verdict, number>;
    comparable: number;
    expiringSoon: ReconcileRecord[];
    exitCode: number;
}
/** Deduplicate registrar rows — marker paging is inclusive and yields repeats. */
export declare function dedupeRegistrarRows(rows: RegistrarRow[]): {
    rows: RegistrarRow[];
    dupesRemoved: number;
};
export declare function reconcileExpiry(rows: RegistrarRow[], deps: ReconcileDeps): Promise<ReconcileReport>;
/**
 * Coverage regression against a prior run: a domain that WAS comparable and is
 * now unverifiable. A shrinking denominator is how "0 mismatches" gets
 * published by an instrument that has gone blind.
 */
export declare function coverageRegressions(current: ReconcileRecord[], baseline: Array<{
    domain: string;
    verdict: Verdict;
}>): string[];
//# sourceMappingURL=expiry-reconcile.d.ts.map