fix(s31): close 1 review BLOCKER — EmbedderConfig registry + --embedder-config, never an import path

This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 12:50:19 +02:00
commit b9dd91cdbe
4 changed files with 240 additions and 1 deletions

View file

@ -40,6 +40,7 @@ os.environ.setdefault("OMP_NUM_THREADS", "1")
os.environ.setdefault("MKL_NUM_THREADS", "1")
import numpy as np # noqa: E402 — must follow the thread pins above; see module docstring
from pydantic import BaseModel, model_validator # noqa: E402 — same ordering constraint
if TYPE_CHECKING: # verdicts imports agent_framework — keep it out of the runtime import graph
from portfolio_optimiser.verdicts import ProposalFeatures, Verdict
@ -132,6 +133,63 @@ class FakeEmbedder:
return np.ascontiguousarray(vec / norm, dtype="<f8")
# --- Embedder config seam: a CLOSED registry, deliberately never an import path -----------------
class EmbedderConfig(BaseModel):
"""Declarative embedder choice. ``type`` membership is enforced by ``build_embedder`` (unknown
type ``ValueError``); the validator pins the per-type required fields.
Deliberately NOT an import path. A ``"my_pkg.mod:MyEmbedder"`` string would be arbitrary code
execution at config-load time, and would hand a config file the ability to import a module that
opens a socket precisely the capability ``NotifierConfig`` refuses to let config grant
("egress opt-in is a code-level factory kwarg, so config alone can never grant it"). It would
also route around this module's no-network guard, which is an AST/``sys.modules`` check scoped
to ``semretrieval.py`` and blind to a third module by construction.
A future networked embedder therefore enters as a NEW REGISTRY BRANCH plus a code-level
capability kwarg never as config."""
type: str
@model_validator(mode="after")
def _required_fields_by_type(self) -> EmbedderConfig:
if not self.type:
raise ValueError("embedder config requires a non-empty type")
return self
def build_embedder(config: EmbedderConfig) -> Embedder:
"""Type-dispatch factory over a CLOSED vocabulary. Mirrors ``notify.build_notifier``.
Shipped vocabulary: ``"fake"`` ``FakeEmbedder`` (a deterministic sha256 projection carrying
no semantics). Anything else raises ``ValueError`` naming the seam, rather than attempting to
resolve it see ``EmbedderConfig`` for why resolution from config is refused outright."""
if config.type == "fake":
return FakeEmbedder()
raise ValueError(
f"unknown embedder type: {config.type!r} (known: 'fake'; a new embedder is added as a "
"registry branch in semretrieval.build_embedder, never as an import path in config)"
)
def load_embedder_config(path: str | Path) -> EmbedderConfig:
"""Fail-fast standalone loader for an embedder config (mirrors ``dimension.load_dimension``).
An embedder config is *authoritative startup input*, so loading is fail-fast: a missing file
raises ``FileNotFoundError`` and malformed/invalid content raises ``pydantic.ValidationError``.
Deliberate contrast to the tolerant verdict-inbox RAW layer (``load_verdicts_from_dir``), which
skips bad files rather than raising startup configuration must never be silently degraded.
:raises FileNotFoundError: ``path`` does not point at an existing file.
:raises pydantic.ValidationError: the JSON is malformed or violates the schema.
"""
p = Path(path)
if not p.is_file():
raise FileNotFoundError(f"embedder config not found: {str(path)!r}")
return EmbedderConfig.model_validate_json(p.read_text(encoding="utf-8"))
def cosine(a: np.ndarray, b: np.ndarray) -> float:
"""Cosine similarity, with a zero-norm guard returning ``0.0``.