"""Backend selection. One registry entry today. It exists so that adding a semantic/vector backend is a new module plus one line here - not a change to the command, the filters, or the output shape. Selecting several at once fuses them through RRF. """ from __future__ import annotations import os from pathlib import Path from typing import Callable, Optional from chemenu.errors import ValidationError from chemenu.search.base import SearchBackend from chemenu.search.ripgrep import RipgrepBackend DEFAULT_BACKEND = "rg" ENV_VAR = "WIKITOOL_SEARCH_BACKEND" BACKENDS: dict[str, Callable[..., SearchBackend]] = { "rg": RipgrepBackend, } class UnknownBackend(ValidationError): pass def resolve( spec: str | None = None, kb_dir: Optional[Path] = None, root: Optional[Path] = None, ) -> list[SearchBackend]: """Resolve a backend spec into instances. Precedence: explicit argument, then `WIKITOOL_SEARCH_BACKEND`, then the default. A comma-separated spec selects several and fuses their rankings. `kb_dir`/`root` are handed to the backend rather than left to its own defaults. Without them a caller could pass a corpus to `run_search` and still have the backend walk `config.KB_DIR` - the query answered from one tree and the pages read from another, with nothing saying so. """ raw = spec or os.environ.get(ENV_VAR) or DEFAULT_BACKEND names = [n.strip() for n in raw.split(",") if n.strip()] unknown = [n for n in names if n not in BACKENDS] if unknown: raise UnknownBackend( f"unknown search backend(s): {', '.join(unknown)}. " f"Available: {', '.join(sorted(BACKENDS))}" ) return [BACKENDS[name](kb_dir, root) for name in names]