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

@ -52,8 +52,11 @@ from portfolio_optimiser.validator import Rejection, ValidatedProposal
from portfolio_optimiser import okf, outbox
from portfolio_optimiser.semretrieval import (
SEMANTIC_WEIGHT_DEFAULT,
Embedder,
FakeEmbedder,
HybridRanker,
build_embedder,
load_embedder_config,
)
from portfolio_optimiser.verdicts import (
ExpeLContextProvider,
@ -258,6 +261,7 @@ async def run_project(
meter: TokenMeter | None = None,
live_dry_run: bool = False,
semantic_retrieval: bool = False,
embedder: Embedder | None = None,
) -> RunResult | DryRunReport:
"""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
@ -395,7 +399,11 @@ async def run_project(
# of that object — including a subsequent run with the flag OFF. Flag off => ranker stays None
# => ``retrieve`` falls through to the StructuralRetriever default.
ranker = (
HybridRanker(FakeEmbedder(), similarity, SEMANTIC_WEIGHT_DEFAULT)
HybridRanker(
embedder if embedder is not None else FakeEmbedder(),
similarity,
SEMANTIC_WEIGHT_DEFAULT,
)
if semantic_retrieval
else None
)
@ -553,6 +561,7 @@ async def run_portfolio(
top_k: int = 3,
meter_factory: Callable[[], TokenMeter] | None = None,
semantic_retrieval: bool = False,
embedder: Embedder | None = None,
) -> PortfolioResult:
"""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
@ -632,6 +641,7 @@ async def run_portfolio(
max_tokens=max_tokens,
top_k=top_k,
semantic_retrieval=semantic_retrieval,
embedder=embedder,
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 "
"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(
"--outbox-dir",
default=None,
@ -761,6 +778,7 @@ def main(argv: list[str] | None = None) -> int:
"--run-id": args.run_id is not None,
"--dimension-config": args.dimension_config is not None,
"--semantic-retrieval": args.semantic_retrieval,
"--embedder-config": args.embedder_config is not None,
}
if any(report_forbidden.values()):
print(
@ -824,12 +842,18 @@ def main(argv: list[str] | None = None) -> int:
goals = load_goal_config(args.goals) if args.goals else None
ledger = SavingsLedger.load(args.ledger) if args.ledger 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
portfolio_result = asyncio.run(
run_portfolio(
project_ids,
args.profile,
dimension=dimension,
embedder=embedder,
ledger=ledger,
goals=goals,
semantic_retrieval=args.semantic_retrieval,
@ -902,6 +926,11 @@ def main(argv: list[str] | None = None) -> int:
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
),
outbox_dir=args.outbox_dir,
run_id=args.run_id,
verdict_input={"decision": args.decision, "rationale": args.rationale},
@ -943,6 +972,11 @@ def main(argv: list[str] | None = None) -> int:
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
),
outbox_dir=args.outbox_dir,
run_id=args.run_id,
verdict_input={"decision": args.decision, "rationale": args.rationale},