/** * Lexical index + BM25 scorer — the "keyword truth" rail. * * This file is deliberately narrow: it produces one trustworthy primitive, * `scoreLexical(query, index)`, that returns deterministic field-aware * BM25 scores over v2 chunks. No fusion, no modes, no synthesis — slice 3 * layers those on top. * * Four fields are scored independently and then combined into a single * `score`, but the per-field breakdown is preserved on every result so * slice 3 can recombine them (e.g. title_path mode) without reworking * this layer. * * Field identity matters: * - title : the file's first H1 (if any) * - heading_path : the breadcrumb stack for the chunk's section * - path : file path tokens (dirs, base name, extension) * - body : the chunk text itself * * Determinism guarantees: * - Sort by score desc, then (path asc, chunk_index asc) as tie-break. * - Zero-score chunks are omitted. * - Tokenizer is pure: same input → same tokens, independent of locale. * - Stopword set is a fixed list in this file (no env lookup). */ import type { CorpusChunk } from "./storage.js"; export declare const DEFAULT_FIELD_WEIGHTS: { readonly title: 3; readonly heading: 2; readonly path: 1.5; readonly body: 1; }; export declare function isStopword(term: string): boolean; /** * Generic tokenizer for body/title/heading text. * Lowercases, splits on non-alphanumeric, drops stopwords and empties. * No stemming, no fuzzy matching — those are explicit slice-3+ decisions. */ export declare function tokenize(text: string): string[]; /** * Path tokenizer — same splitter as body, but runs the full path through * so dirs, base name stem, and extension each become searchable tokens. * Absolute-path prefixes (drive letters, leading slashes) contribute * nothing once split, which is the right behavior. */ export declare function tokenizePath(path: string): string[]; /** * Heading tokenizer — flattens the heading_path breadcrumb into tokens. * Deeper headings contribute the same as shallower ones; weighting by * depth is a slice-3+ concern. */ export declare function tokenizeHeadings(headingPath: string[]): string[]; export type FieldName = "body" | "heading" | "title" | "path"; interface LexicalDoc { chunkId: string; chunkIndex: number; path: string; tokens: Record>; len: Record; } export interface LexicalIndex { chunkCount: number; avg: Record; /** Document frequency per field: term -> number of chunks whose field contains it. */ df: Record>; docs: LexicalDoc[]; } export interface LexicalScore { chunkId: string; chunkIndex: number; path: string; score: number; fieldScores: Record; matchedTerms: string[]; } /** * Build the in-memory lexical index from the chunks of a loaded corpus. * Idempotent and deterministic: same input corpus → identical index. */ export declare function buildLexicalIndex(chunks: CorpusChunk[], titles: Record): LexicalIndex; export interface ScoreLexicalOptions { weights?: Partial>; /** Override the query tokenizer. Default = tokenize(). */ queryTokens?: string[]; } /** * Score every chunk in the index against `query`. Returns only chunks * with score > 0, sorted score desc, (path asc, chunk_index asc) tie-break. * * Field scores are preserved on every result so slice 3 can recombine * without rescoring. */ export declare function scoreLexical(query: string, index: LexicalIndex, opts?: ScoreLexicalOptions): LexicalScore[]; export {}; //# sourceMappingURL=lexical.d.ts.map