fix(s31): close 1 review BLOCKER — EmbedderConfig registry + --embedder-config, never an import path
This commit is contained in:
parent
cabe05acf3
commit
b9dd91cdbe
4 changed files with 240 additions and 1 deletions
|
|
@ -52,8 +52,11 @@ from portfolio_optimiser.validator import Rejection, ValidatedProposal
|
||||||
from portfolio_optimiser import okf, outbox
|
from portfolio_optimiser import okf, outbox
|
||||||
from portfolio_optimiser.semretrieval import (
|
from portfolio_optimiser.semretrieval import (
|
||||||
SEMANTIC_WEIGHT_DEFAULT,
|
SEMANTIC_WEIGHT_DEFAULT,
|
||||||
|
Embedder,
|
||||||
FakeEmbedder,
|
FakeEmbedder,
|
||||||
HybridRanker,
|
HybridRanker,
|
||||||
|
build_embedder,
|
||||||
|
load_embedder_config,
|
||||||
)
|
)
|
||||||
from portfolio_optimiser.verdicts import (
|
from portfolio_optimiser.verdicts import (
|
||||||
ExpeLContextProvider,
|
ExpeLContextProvider,
|
||||||
|
|
@ -258,6 +261,7 @@ async def run_project(
|
||||||
meter: TokenMeter | None = None,
|
meter: TokenMeter | None = None,
|
||||||
live_dry_run: bool = False,
|
live_dry_run: bool = False,
|
||||||
semantic_retrieval: bool = False,
|
semantic_retrieval: bool = False,
|
||||||
|
embedder: Embedder | None = None,
|
||||||
) -> RunResult | DryRunReport:
|
) -> RunResult | DryRunReport:
|
||||||
"""Run the vertical slice for ONE project. ``client_factory`` is the test-injection seam
|
"""Run the vertical slice for ONE project. ``client_factory`` is the test-injection seam
|
||||||
(defaults to the real backend). ``verdict_input`` carries the expert decision/rationale
|
(defaults to the real backend). ``verdict_input`` carries the expert decision/rationale
|
||||||
|
|
@ -395,7 +399,11 @@ async def run_project(
|
||||||
# of that object — including a subsequent run with the flag OFF. Flag off => ranker stays None
|
# of that object — including a subsequent run with the flag OFF. Flag off => ranker stays None
|
||||||
# => ``retrieve`` falls through to the StructuralRetriever default.
|
# => ``retrieve`` falls through to the StructuralRetriever default.
|
||||||
ranker = (
|
ranker = (
|
||||||
HybridRanker(FakeEmbedder(), similarity, SEMANTIC_WEIGHT_DEFAULT)
|
HybridRanker(
|
||||||
|
embedder if embedder is not None else FakeEmbedder(),
|
||||||
|
similarity,
|
||||||
|
SEMANTIC_WEIGHT_DEFAULT,
|
||||||
|
)
|
||||||
if semantic_retrieval
|
if semantic_retrieval
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
@ -553,6 +561,7 @@ async def run_portfolio(
|
||||||
top_k: int = 3,
|
top_k: int = 3,
|
||||||
meter_factory: Callable[[], TokenMeter] | None = None,
|
meter_factory: Callable[[], TokenMeter] | None = None,
|
||||||
semantic_retrieval: bool = False,
|
semantic_retrieval: bool = False,
|
||||||
|
embedder: Embedder | None = None,
|
||||||
) -> PortfolioResult:
|
) -> PortfolioResult:
|
||||||
"""Fan out over a portfolio of independent projects SEQUENTIALLY, composing ``run_project``
|
"""Fan out over a portfolio of independent projects SEQUENTIALLY, composing ``run_project``
|
||||||
as-is (every project's execution state — meter, debate, retrieval context — is built fresh
|
as-is (every project's execution state — meter, debate, retrieval context — is built fresh
|
||||||
|
|
@ -632,6 +641,7 @@ async def run_portfolio(
|
||||||
max_tokens=max_tokens,
|
max_tokens=max_tokens,
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
semantic_retrieval=semantic_retrieval,
|
semantic_retrieval=semantic_retrieval,
|
||||||
|
embedder=embedder,
|
||||||
meter=meter_factory() if meter_factory is not None else None,
|
meter=meter_factory() if meter_factory is not None else None,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -671,6 +681,13 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
help="fail-fast dimension scope config (JSON): scopes the run to one cost axis; a "
|
help="fail-fast dimension scope config (JSON): scopes the run to one cost axis; a "
|
||||||
"missing or malformed file refuses the run (authoritative startup config, not a RAW inbox)",
|
"missing or malformed file refuses the run (authoritative startup config, not a RAW inbox)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--embedder-config",
|
||||||
|
default=None,
|
||||||
|
help='fail-fast embedder config (JSON, e.g. {"type": "fake"}): selects the embedder '
|
||||||
|
"used by --semantic-retrieval from a CLOSED registry. Not an import path — a config file "
|
||||||
|
"can never name arbitrary code to load (a new embedder is added as a registry branch)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--outbox-dir",
|
"--outbox-dir",
|
||||||
default=None,
|
default=None,
|
||||||
|
|
@ -761,6 +778,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
"--run-id": args.run_id is not None,
|
"--run-id": args.run_id is not None,
|
||||||
"--dimension-config": args.dimension_config is not None,
|
"--dimension-config": args.dimension_config is not None,
|
||||||
"--semantic-retrieval": args.semantic_retrieval,
|
"--semantic-retrieval": args.semantic_retrieval,
|
||||||
|
"--embedder-config": args.embedder_config is not None,
|
||||||
}
|
}
|
||||||
if any(report_forbidden.values()):
|
if any(report_forbidden.values()):
|
||||||
print(
|
print(
|
||||||
|
|
@ -824,12 +842,18 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
goals = load_goal_config(args.goals) if args.goals else None
|
goals = load_goal_config(args.goals) if args.goals else None
|
||||||
ledger = SavingsLedger.load(args.ledger) if args.ledger else None
|
ledger = SavingsLedger.load(args.ledger) if args.ledger else None
|
||||||
dimension = load_dimension(args.dimension_config) if args.dimension_config else None
|
dimension = load_dimension(args.dimension_config) if args.dimension_config else None
|
||||||
|
embedder = (
|
||||||
|
build_embedder(load_embedder_config(args.embedder_config))
|
||||||
|
if args.embedder_config
|
||||||
|
else None
|
||||||
|
)
|
||||||
project_ids = (args.project_id,) if args.project_id is not None else None
|
project_ids = (args.project_id,) if args.project_id is not None else None
|
||||||
portfolio_result = asyncio.run(
|
portfolio_result = asyncio.run(
|
||||||
run_portfolio(
|
run_portfolio(
|
||||||
project_ids,
|
project_ids,
|
||||||
args.profile,
|
args.profile,
|
||||||
dimension=dimension,
|
dimension=dimension,
|
||||||
|
embedder=embedder,
|
||||||
ledger=ledger,
|
ledger=ledger,
|
||||||
goals=goals,
|
goals=goals,
|
||||||
semantic_retrieval=args.semantic_retrieval,
|
semantic_retrieval=args.semantic_retrieval,
|
||||||
|
|
@ -902,6 +926,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
dimension=(
|
dimension=(
|
||||||
load_dimension(args.dimension_config) if args.dimension_config else None
|
load_dimension(args.dimension_config) if args.dimension_config else None
|
||||||
),
|
),
|
||||||
|
embedder=(
|
||||||
|
build_embedder(load_embedder_config(args.embedder_config))
|
||||||
|
if args.embedder_config
|
||||||
|
else None
|
||||||
|
),
|
||||||
outbox_dir=args.outbox_dir,
|
outbox_dir=args.outbox_dir,
|
||||||
run_id=args.run_id,
|
run_id=args.run_id,
|
||||||
verdict_input={"decision": args.decision, "rationale": args.rationale},
|
verdict_input={"decision": args.decision, "rationale": args.rationale},
|
||||||
|
|
@ -943,6 +972,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
dimension=(
|
dimension=(
|
||||||
load_dimension(args.dimension_config) if args.dimension_config else None
|
load_dimension(args.dimension_config) if args.dimension_config else None
|
||||||
),
|
),
|
||||||
|
embedder=(
|
||||||
|
build_embedder(load_embedder_config(args.embedder_config))
|
||||||
|
if args.embedder_config
|
||||||
|
else None
|
||||||
|
),
|
||||||
outbox_dir=args.outbox_dir,
|
outbox_dir=args.outbox_dir,
|
||||||
run_id=args.run_id,
|
run_id=args.run_id,
|
||||||
verdict_input={"decision": args.decision, "rationale": args.rationale},
|
verdict_input={"decision": args.decision, "rationale": args.rationale},
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ os.environ.setdefault("OMP_NUM_THREADS", "1")
|
||||||
os.environ.setdefault("MKL_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
|
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
|
if TYPE_CHECKING: # verdicts imports agent_framework — keep it out of the runtime import graph
|
||||||
from portfolio_optimiser.verdicts import ProposalFeatures, Verdict
|
from portfolio_optimiser.verdicts import ProposalFeatures, Verdict
|
||||||
|
|
@ -132,6 +133,63 @@ class FakeEmbedder:
|
||||||
return np.ascontiguousarray(vec / norm, dtype="<f8")
|
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:
|
def cosine(a: np.ndarray, b: np.ndarray) -> float:
|
||||||
"""Cosine similarity, with a zero-norm guard returning ``0.0``.
|
"""Cosine similarity, with a zero-norm guard returning ``0.0``.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ content so the dry-run reaches its offline return.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -604,6 +605,92 @@ def test_semantic_retrieval_with_both_dirs_is_not_refused(tmp_path, capsys) -> N
|
||||||
assert "--semantic-retrieval" not in out.err
|
assert "--semantic-retrieval" not in out.err
|
||||||
|
|
||||||
|
|
||||||
|
def test_embedder_config_valid_file_parses_offline(tmp_path, capsys) -> None:
|
||||||
|
"""``--embedder-config <valid>`` is accepted and the run still stops offline — the registry is
|
||||||
|
reachable from the CLI, not merely importable."""
|
||||||
|
cfg = tmp_path / "embedder.json"
|
||||||
|
cfg.write_text('{"type": "fake"}', encoding="utf-8")
|
||||||
|
rc = run.main(
|
||||||
|
[
|
||||||
|
_PID,
|
||||||
|
"--docs-dir",
|
||||||
|
str(BUNDLE_DIR),
|
||||||
|
"--bundle-dir",
|
||||||
|
str(BUNDLE_DIR),
|
||||||
|
"--verdict-dir",
|
||||||
|
str(tmp_path / "inbox"),
|
||||||
|
"--semantic-retrieval",
|
||||||
|
"--embedder-config",
|
||||||
|
str(cfg),
|
||||||
|
"--live-dry-run",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert rc == 0
|
||||||
|
assert "LIVE-DRY-RUN OK" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
def test_embedder_config_missing_file_refuses(tmp_path, capsys) -> None:
|
||||||
|
"""Fail-fast startup config: a missing file refuses the run (rc 1, no traceback), surfacing
|
||||||
|
through the existing structured-refusal handler."""
|
||||||
|
rc = run.main(
|
||||||
|
[
|
||||||
|
_PID,
|
||||||
|
"--docs-dir",
|
||||||
|
str(BUNDLE_DIR),
|
||||||
|
"--bundle-dir",
|
||||||
|
str(BUNDLE_DIR),
|
||||||
|
"--verdict-dir",
|
||||||
|
str(tmp_path / "inbox"),
|
||||||
|
"--semantic-retrieval",
|
||||||
|
"--embedder-config",
|
||||||
|
"/nonexistent-embedder-config.json",
|
||||||
|
"--live-dry-run",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
err = capsys.readouterr().err
|
||||||
|
assert rc == 1
|
||||||
|
assert "refused" in err.lower()
|
||||||
|
assert "Traceback" not in err
|
||||||
|
|
||||||
|
|
||||||
|
def test_embedder_config_unknown_type_refuses(tmp_path, capsys) -> None:
|
||||||
|
"""A config naming an embedder outside the closed registry is REFUSED, never resolved — this
|
||||||
|
is the CLI-level face of the no-import-path rule."""
|
||||||
|
cfg = tmp_path / "embedder.json"
|
||||||
|
cfg.write_text('{"type": "my_pkg.mod:NetworkEmbedder"}', encoding="utf-8")
|
||||||
|
rc = run.main(
|
||||||
|
[
|
||||||
|
_PID,
|
||||||
|
"--docs-dir",
|
||||||
|
str(BUNDLE_DIR),
|
||||||
|
"--bundle-dir",
|
||||||
|
str(BUNDLE_DIR),
|
||||||
|
"--verdict-dir",
|
||||||
|
str(tmp_path / "inbox"),
|
||||||
|
"--semantic-retrieval",
|
||||||
|
"--embedder-config",
|
||||||
|
str(cfg),
|
||||||
|
"--live-dry-run",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
err = capsys.readouterr().err
|
||||||
|
assert rc == 1
|
||||||
|
assert "refused" in err.lower()
|
||||||
|
assert "my_pkg" not in sys.modules
|
||||||
|
|
||||||
|
|
||||||
|
def test_report_with_embedder_config_is_refused(tmp_path, capsys) -> None:
|
||||||
|
"""--report stays an ALLOWLIST: a new config flag must be refused there like every other one,
|
||||||
|
else it would be silently dropped."""
|
||||||
|
cfg = tmp_path / "embedder.json"
|
||||||
|
cfg.write_text('{"type": "fake"}', encoding="utf-8")
|
||||||
|
ledger_file = tmp_path / "ledger.json"
|
||||||
|
SavingsLedger(entries=[]).save(str(ledger_file))
|
||||||
|
rc = run.main(["--report", "--ledger", str(ledger_file), "--embedder-config", str(cfg)])
|
||||||
|
assert rc == 1
|
||||||
|
assert "refused" in capsys.readouterr().err.lower()
|
||||||
|
|
||||||
|
|
||||||
def test_semantic_retrieval_is_not_refused_in_portfolio_mode(capsys) -> None:
|
def test_semantic_retrieval_is_not_refused_in_portfolio_mode(capsys) -> None:
|
||||||
"""The flag is valid in BOTH modes (like --dimension-config), so the portfolio partition must
|
"""The flag is valid in BOTH modes (like --dimension-config), so the portfolio partition must
|
||||||
not name it. Probed via a run that IS refused for a different flag: the refusal lists
|
not name it. Probed via a run that IS refused for a different flag: the refusal lists
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,18 @@ from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from portfolio_optimiser.semretrieval import (
|
from portfolio_optimiser.semretrieval import (
|
||||||
EMBED_DIM,
|
EMBED_DIM,
|
||||||
|
Embedder,
|
||||||
|
EmbedderConfig,
|
||||||
FakeEmbedder,
|
FakeEmbedder,
|
||||||
HybridRanker,
|
HybridRanker,
|
||||||
StructuralRetriever,
|
StructuralRetriever,
|
||||||
|
build_embedder,
|
||||||
cosine,
|
cosine,
|
||||||
|
load_embedder_config,
|
||||||
load_vector_store,
|
load_vector_store,
|
||||||
save_vector_store,
|
save_vector_store,
|
||||||
)
|
)
|
||||||
|
|
@ -291,6 +296,61 @@ def test_hybrid_ranker_at_weight_zero_equals_the_structural_ranking() -> None:
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# --- The embedder config seam: a CLOSED registry, never an import path -------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_embedder_returns_the_shipped_fake_for_the_known_type() -> None:
|
||||||
|
"""The registry's one shipped entry. Mirrors ``tests/test_notify.py``'s build_notifier tests."""
|
||||||
|
built = build_embedder(EmbedderConfig(type="fake"))
|
||||||
|
assert isinstance(built, FakeEmbedder)
|
||||||
|
assert isinstance(built, Embedder) # satisfies the runtime-checkable protocol
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_embedder_rejects_an_unknown_type() -> None:
|
||||||
|
"""Closed vocabulary: an unknown type RAISES rather than falling back to a default, and the
|
||||||
|
message names the seam so the constraint is discoverable at the failure."""
|
||||||
|
with pytest.raises(ValueError, match="unknown embedder type"):
|
||||||
|
build_embedder(EmbedderConfig(type="nope"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_embedder_refuses_an_import_path_instead_of_importing_it() -> None:
|
||||||
|
"""SECURITY — the registry must never resolve a dotted ``module:Class`` string from config.
|
||||||
|
|
||||||
|
Doing so 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 — routing around this module's no-network guard,
|
||||||
|
which is scoped to ``semretrieval.py`` and blind to a third module by construction. The config
|
||||||
|
is accepted as DATA and then refused; nothing is imported."""
|
||||||
|
hostile = EmbedderConfig(type="evil_pkg.embedders:NetworkEmbedder")
|
||||||
|
with pytest.raises(ValueError, match="unknown embedder type"):
|
||||||
|
build_embedder(hostile)
|
||||||
|
assert "evil_pkg" not in sys.modules
|
||||||
|
|
||||||
|
|
||||||
|
def test_embedder_config_rejects_an_empty_type() -> None:
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
EmbedderConfig(type="")
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_embedder_config_is_fail_fast_on_a_missing_file(tmp_path: Path) -> None:
|
||||||
|
"""Authoritative startup config — absence RAISES, in deliberate contrast to the tolerant
|
||||||
|
verdict-inbox RAW layer which skips bad files."""
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
load_embedder_config(tmp_path / "nope.json")
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_embedder_config_is_fail_fast_on_a_malformed_file(tmp_path: Path) -> None:
|
||||||
|
bad = tmp_path / "embedder.json"
|
||||||
|
bad.write_text('{"type": 17}', encoding="utf-8")
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
load_embedder_config(bad)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_embedder_config_round_trips_a_valid_file(tmp_path: Path) -> None:
|
||||||
|
good = tmp_path / "embedder.json"
|
||||||
|
good.write_text('{"type": "fake"}', encoding="utf-8")
|
||||||
|
assert isinstance(build_embedder(load_embedder_config(good)), FakeEmbedder)
|
||||||
|
|
||||||
|
|
||||||
# --- SC3 / SC7: the vector store is byte-deterministic and fails fast on a row/line mismatch ---
|
# --- SC3 / SC7: the vector store is byte-deterministic and fails fast on a row/line mismatch ---
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue