/** * Title Matcher * * Fuzzy comparison between a CLAIMED page title and the title that * was actually rendered. Used by the verification engine's * `titleFuzzyMatches` assertion to validate that an AI-generated * citation actually points at the document the AI claimed it does. * * ## Why fuzzy and not exact * * Real page titles are messy. They include publisher suffixes, * year parentheticals, edition markers, translated titles, * "Wikipedia, the free encyclopedia"-style banners, and so on: * * Claimed: "On the Origin of Species" * Extracted: "On the Origin of Species (1859) - Wikisource, the free online library" * * Exact-string matching would reject this. Substring matching * would accept "On the Origin of Species" being a substring of * "Compendium On the Origin of Species and Other Essays" — a * different work that just shares the title prefix. * * The right answer is a fuzzy SET match: tokenize both titles, * drop short filler words, lowercase, strip punctuation, then * require some configurable fraction of the claimed tokens to * also appear in the extracted title. Default threshold is 0.5 * — half of the claimed words must hit. Callers tighten or * loosen via the assertion. * * ## Limitations * * - Bag-of-words: word ORDER is ignored. "Origin of Species" and * "Species of Origin" both match. For citation verification this * is acceptable because real page titles preserve word order * anyway and we'd rather be permissive than strict. * - Tokenization is Unicode-aware via `\p{L}\p{N}` so Cyrillic, * Greek, Hebrew, Arabic, and other space-separated scripts * tokenize correctly. CJK scripts that don't use whitespace * between words (Japanese, Chinese, Korean) tokenize the entire * title as one or two large tokens — comparison works only when * both the claim and the extracted title use the exact same * substring. This is a documented limitation; callers needing * word-level CJK matching should preprocess via a real * segmenter (kuromoji, jieba, etc.). * - Filler-word filter is hardcoded at 3+ characters. This drops * English stop-words ("the", "of", "to") which is desirable. * Short proper nouns ("AI", "EU", "FBI", "NYT", "WW2") would * normally be lost to this filter, BUT we add them back via a * separate all-caps extraction pass that runs on the original * un-lowercased input. This preserves acronyms in the claim * without loosening the stop-word filter for the common case. * See `tokenize()`. */ /** * Default token-overlap threshold for `titleFuzzyMatches`. Half of * the claimed tokens must appear in the extracted title. */ export declare const DEFAULT_TITLE_MATCH_THRESHOLD = 0.5; /** * Returns true if the claimed title fuzzily matches the extracted * title. The match passes when at least `threshold` fraction of the * claimed tokens (after tokenization) also appear in the extracted * tokens. * * Edge cases: * - Empty `claimed` → returns true (no grounds to reject; trust * the upstream when there's no claim to validate) * - Empty / null `extracted` → returns false (the page either * didn't load or has no title; can't claim a match) * - Claimed tokenizes to empty (e.g., all stop-words) → returns * true vacuously (no claimed tokens means nothing to fail) * * @param claimed The title the caller claims the page should have * @param extracted The title actually rendered on the page * @param threshold Fraction of claimed tokens that must hit (0..1) */ export declare function titleFuzzyMatches(claimed: string, extracted: string | null | undefined, threshold?: number): boolean; /** * Why a fuzzy title match did or didn't pass. Lets callers * distinguish a real successful match (`'matched'`) from a vacuous * one (`'no-claim'` or `'vacuous-match'`) so they can apply different * policies — for example, log a warning when a non-Latin claim or * an all-short-tokens claim falls through to the vacuous branch * silently. * * - `'matched'` — real token-overlap match passed threshold * - `'no-claim'` — claimed string was empty; trusted upstream * - `'vacuous-match'` — claimed had content but tokenized to * empty (all-short tokens, all-non-Latin * in CJK, etc.); we returned true but the * assertion did no useful work * - `'no-extracted'` — extracted title was null/empty * - `'below-threshold'` — token overlap was below the threshold */ export type TitleMatchReason = 'matched' | 'no-claim' | 'vacuous-match' | 'no-extracted' | 'below-threshold'; /** * Detailed result of a fuzzy title match. Useful for callers that * want to surface WHY a match passed or failed (which tokens hit, * which missed) rather than just a boolean. Used by the * verification engine to populate informative debug logs. */ export interface TitleMatchResult { matched: boolean; /** * Fraction of claimed tokens that hit. 0..1. Returns 1 in the * `'no-claim'` and `'vacuous-match'` branches by convention, * even though no real comparison happened — check `reason` to * disambiguate. */ score: number; /** Threshold that was applied. */ threshold: number; /** Why the match did or didn't pass. */ reason: TitleMatchReason; /** Claimed tokens that DID appear in the extracted title. */ hits: string[]; /** Claimed tokens that did NOT appear in the extracted title. */ misses: string[]; } /** * Same matching logic as `titleFuzzyMatches`, but returns the full * detail for callers that need to surface why the match did or * didn't pass. */ export declare function titleFuzzyMatchDetails(claimed: string, extracted: string | null | undefined, threshold?: number): TitleMatchResult; //# sourceMappingURL=title-matcher.d.ts.map