/** * Wiki Search Service * * In-process full-text search across a wiki vault's `.md` files. Returns * per-file matches with line numbers + short snippets so the UI can render * a result list and let the user jump straight to a page. * * Ranking: Okapi **BM25** (term-frequency saturation via `k1`, length * normalisation via `b`, inverse-document-frequency weighting) computed * per request. No external index — the vaults here are O(100s) of pages so * a per-request scan + score is fine; revisit with a persisted index when * volumes justify it. Tokenisation is Unicode-aware: Latin word tokens plus * individual CJK characters, so Chinese/Japanese content is searchable. * * Scope: walks the vault directory AND the per-team overlay sources * (`sop/` → installed/custom SOPs, `team-norm/` → team norms) so SOP and * norm content is searchable even though it lives outside the vault. * * @module services/wiki/wiki-search.service */ /** Cap on how many .md files are inspected per search. */ export declare const WIKI_SEARCH_MAX_FILES = 500; /** Cap on how many results returned to the caller. */ export declare const WIKI_SEARCH_MAX_RESULTS = 50; /** Cap on how many snippets surfaced per matching file. */ export declare const WIKI_SEARCH_MAX_SNIPPETS_PER_FILE = 3; /** Maximum query length — guards against pathological input. */ export declare const WIKI_SEARCH_MAX_QUERY_LENGTH = 200; /** Cap on per-file size we will read; larger files are skipped. */ export declare const WIKI_SEARCH_MAX_FILE_BYTES: number; export interface WikiSearchSnippet { /** 1-based line number where the match was found. */ lineNumber: number; /** The matching line (trimmed for display). */ text: string; } export interface WikiSearchHit { /** Relative path inside the vault, with forward slashes. */ relativePath: string; /** True when the filename itself contains the query. */ filenameMatch: boolean; /** Number of total content matches across the file. */ matchCount: number; /** BM25 relevance score (higher = better). */ score: number; /** Up to `WIKI_SEARCH_MAX_SNIPPETS_PER_FILE` matching lines. */ snippets: WikiSearchSnippet[]; /** Owning vault path (set for all-vault search; absent for single-vault). */ vaultPath?: string; /** Human label for the owning vault (all-vault search only). */ vaultLabel?: string; } export interface WikiSearchResult { ok: true; /** Searched vault path, or '' for an all-vault search. */ vaultPath: string; query: string; hits: WikiSearchHit[]; /** True when the file/result cap was hit; UI should hint "narrow your search". */ truncated: boolean; } export interface WikiSearchFailure { ok: false; reason: 'vault_missing' | 'invalid_input' | 'invalid_query'; message: string; } /** Eligibility filters applied per page (frontmatter-driven). */ export interface WikiSearchFilters { /** Include pages marked `superseded_by` (default false). */ includeSuperseded?: boolean; /** Role of the reader; pages whose `visibility` excludes it are dropped. Undefined = owner (sees all). */ viewerRole?: string; /** Include `_proposed/` pages (default false). */ includeProposed?: boolean; } export interface WikiSearchInput extends WikiSearchFilters { vaultPath: string; query: string; } /** A vault to include in an all-vault search. */ export interface VaultRef { vaultPath: string; label?: string; } export interface WikiMultiSearchInput extends WikiSearchFilters { vaults: VaultRef[]; query: string; } /** Vault-relative document with optional inline content (log entries). */ export interface WikiSearchDoc { relativePath: string; absPath: string; content?: string; weight?: number; } /** * Tokenise text into searchable terms: lowercase Latin word runs * (`[a-z0-9]+`) plus individual CJK characters (Han, Hiragana, Katakana), * so both English and Chinese/Japanese content is matched. * * @param text - Raw text to tokenise. * @returns Array of lowercase tokens (order preserved, duplicates kept). */ export declare function tokenize(text: string): string[]; /** * Service exposing `search()`. Stateless singleton — matches the pattern of * the other wiki services (`WikiBookkeepService.getInstance()`, etc.). */ export declare class WikiSearchService { private static instance; /** Singleton accessor. */ static getInstance(): WikiSearchService; /** Reset for tests. */ static resetInstance(): void; /** Validate + normalise a query, or return a structured failure. */ private validateQuery; /** * Search a single vault and rank results with BM25. Returns either * `{ok:true, hits}` or a structured failure. Filesystem read errors are * silently skipped so one bad file doesn't blank the whole result. */ search(input: WikiSearchInput): Promise; /** * Search across MANY vaults and return one merged BM25-ranked list. IDF is * computed over the combined corpus so scores are comparable across vaults. * Each hit carries its owning `vaultPath` (+ `vaultLabel` when provided). */ searchAllVaults(input: WikiMultiSearchInput): Promise; /** * BM25-rank an explicit list of documents (rather than auto-collecting the * whole vault). Lets callers that need their own eligibility rules — e.g. the * `wiki-query` skill, which ranks only `llm-curated/` pages and skips * `log.md` — reuse the exact same Okapi BM25 scoring with IDF computed over * precisely the documents they supply. * * @param vaultPath - Owning vault (stamped on each hit's `vaultPath`). * @param docs - Candidate documents as `{ relativePath, absPath }`. * @param query - Raw query string (validated/normalised here). * @returns BM25-ranked hits, or an empty array for an invalid/empty query. */ searchCorpus(vaultPath: string, docs: WikiSearchDoc[], query: string, filters?: WikiSearchFilters): Promise; /** * Core BM25 search over an arbitrary corpus of doc refs (one or many vaults). * Builds the combined corpus statistics, scores, ranks, and attaches snippets * for the top results. Each hit carries its owning `vaultPath`. */ private _searchCorpus; /** * Collect candidate `.md` documents: the vault tree plus the per-team * overlay sources (`sop/`, `team-norm/`). Overlay paths are prefixed with * the folder name so they resolve back through the page endpoint. * * @param vaultPath - Absolute vault root. * @returns Deduped list of `{ relativePath, absPath }`, capped at the limit. */ private collectDocs; /** * Recent `log.md` entries of a vault as virtual documents (#1345): one per * entry from the last `SEARCH_WINDOW_DAYS`, content-free templated entries * and duplicate bodies dropped, newest first, capped, each weighted below * a page of equal relevance. * * @param vaultPath - Vault root * @returns Virtual documents (empty when the vault has no log) */ collectLogEntryDocs(vaultPath: string, now?: number): Promise; /** * Build the virtual document for one log entry. * * @param absLogPath - Absolute path of the log file (for reference only) * @param entry - Parsed entry * @returns Document with inline content and the log-entry weight */ private logEntryDoc; /** * Recursively collect `.md` files under `root`, returned as root-relative * POSIX paths. Skips dotfile dirs (`.git`, `.queue`), the rotated-log * archive, and stops at the cap. */ private walkMd; /** * Read a doc and compute its length + per-query-term frequencies. Filename * tokens are folded in (weighted) so title relevance contributes to BM25. * Returns null only on unreadable/oversize files with no filename match. */ private statDoc; /** * Standard Okapi BM25 score for a doc against the query terms. */ private bm25Score; /** * Re-read a top-ranked file and extract matching-line snippets (a line * matches when it contains any query term as a substring). Returns the * total content match count + up to the snippet cap. */ private buildSnippets; } //# sourceMappingURL=wiki-search.service.d.ts.map