fix(run): --embedder-config is refused, not silently dropped

Walked from a fresh clone: `--embedder-config` was accepted in every mode
without `--semantic-retrieval` and then had no effect whatsoever. MEASURED,
not inferred — an injected embedder is consulted ZERO times with the flag
off and once with it on, because the only consumer is the HybridRanker that
flag builds; the default StructuralRetriever takes no embedder at all.

That is the silent-ignore this CLI's flag contract exists to prevent, and
the same ground on which `--semantic-retrieval` itself is already refused
when it cannot take effect.

REFUSED, not wired — the opposite call from `--scripted-replies` in
portfolio mode, and for a stated reason: there the seam already existed, so
refusing would have left a whole mode without an offline door. Here there
is nothing to wire to.

Mode-independent (both modes gate the embedder on the same flag) and placed
ABOVE the scripted door, mirroring the required-args hoist: a refused run
must not first print a banner claiming a scripted loop closed.

Five mutations against the WHOLE suite, all red, each isolating one seam:
detach the refusal (3 red) · scope it to single-project mode (portfolio arm
red) · move it below the banner (banner arm red, rc intact) · build the
ranker unconditionally (the zero-consultation measurement red) · ignore the
injected embedder (its control red).

663 -> 668 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GyAbxJoyypnLLUDcMvnKh8
This commit is contained in:
Kjell Tore Guttormsen 2026-08-05 11:39:49 +02:00
commit a109a703e2
2 changed files with 169 additions and 0 deletions

View file

@ -1273,6 +1273,33 @@ def main(argv: list[str] | None = None) -> int:
)
return 1
# --embedder-config selects the embedder for the HybridRanker, and that ranker is built ONLY
# when semantic_retrieval is on; the default StructuralRetriever takes no embedder at all. So
# without --semantic-retrieval the config is loaded fail-fast and then dropped on the floor —
# MEASURED, not inferred: an injected embedder is consulted ZERO times with the flag off and
# once with it on (tests/test_run_cli.py::test_injected_embedder_is_never_consulted_with_the
# _flag_off + its control). That is the silent-ignore this CLI's flag contract exists to
# prevent, and the same ground on which --semantic-retrieval itself is refused above when it
# cannot take effect.
#
# REFUSED, not wired — the opposite call from --scripted-replies in portfolio mode, and for a
# stated reason: there the seam already existed (run_portfolio takes the same client_factory),
# so refusing would have left a whole mode without an offline door. Here there is nothing to
# wire to; an embedder has no job outside the hybrid ranker.
#
# MODE-INDEPENDENT (hence above the portfolio dispatch, not inside either branch): both modes
# gate the embedder on the same flag, since run_portfolio forwards it to run_project unchanged.
# Placed ABOVE the scripted door for the reason the required-args guard was hoisted there — a
# refused run must not first print a banner claiming a scripted loop closed.
if args.embedder_config is not None and not args.semantic_retrieval:
print(
"run refused: --embedder-config requires --semantic-retrieval (the embedder is only "
"consulted by the hybrid ranker that flag builds — without it the config would be "
"loaded and then ignored)",
file=sys.stderr,
)
return 1
# The scripted door (offline WHOLE-loop run over the caller's own data). Resolved BEFORE the
# dry-run branch so the two offline modes cannot both be honoured — and BEFORE the portfolio
# dispatch, because the door serves BOTH modes. It originally sat below that dispatch, which