feat(api): reject explicit unsupported search algorithm with 422 (ADR-030)#1020
Conversation
…-030)
Advertising `supported_search_types` on `/api/v1/status` was only half the
contract. `/api/v1/search` and `/api/v1/vector-viz/search` still *coerced* an
explicit `algorithm` the server can't serve (notably `semantic` while
`SEARCH_MODE=keyword`) down to `bm25`, returning lexical results dressed up as a
semantic answer.
Make the contract strict: an explicit unsupported algorithm now returns HTTP 422
`{error:"unsupported_search_type", requested, supported_search_types}` so the
client (astrolabe) can fail loud and self-correct from the advertised set. An
absent `algorithm` still defaults gracefully across modes.
- `select_search_algorithm` (explicit → raise `UnsupportedSearchType`; absent →
default) wraps the retained lenient `resolve_search_algorithm`.
- Both search endpoints translate the exception to 422.
- Unit tests for the gate; contract-first pact gains the keyword-mode 422
interaction (provider state `the server advertises keyword-only search
support`); ADR-030 documents the strict behaviour.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @cbcoutinho's task in 1m 59s —— View job Claude finished reviewing this PRTodo list
SummaryThis is a clean, well-scoped change: it turns the previously-silent Findings🟢 Nit — 🟢 Nit — algorithm gate runs after the empty-query short-circuit, in both endpoints ( What's solid
Not verifiedI don't have shell execution permissions in this environment, so I couldn't independently re-run |
Round-1 review follow-ups: - Add test_search_endpoint_algorithm_gate.py: drives the real unified_search / vector_search Starlette handlers via TestClient and asserts an explicit unsupported algorithm yields the 422 body (and a supported one passes the gate) — the wiring was previously only exercised by provider verification. - Extract _unsupported_search_type_response() so both endpoints share one 422 payload builder instead of duplicating the try/except block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Round-1 findings addressed in
4 new tests pass; |
|
Round-2 nits addressed in
32 tests pass; ruff/ty green. |
Round-2 review nits:
- select_search_algorithm now logs a debug line on the hard-reject branch,
mirroring resolve_search_algorithm's coercion log, so operators can see why an
explicit algorithm got a 422.
- Docstring caveat: explicit JSON "algorithm": null is indistinguishable from an
omitted key and takes the graceful-default path.
- Parametrize the keyword-mode 422 tests (helper + endpoint wiring) over both
dense-requiring algorithms ("semantic" and "hybrid"), not just "semantic".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
Round-3 verdict is a pass — thanks. Both 🟢 nits are non-blocking and I'm leaving them as-is with rationale:
No code change this round. Treating this as merge-ready pending the re-triggered CI going green. |
Behavior-preserving cleanup of the query-type gate merged in #1020 — no wire contract, status, or 422 change (existing unit + pact tests pass unchanged). - management.py: merge resolve_search_algorithm into select_search_algorithm (it was the sole caller, via the implicit-default branch). One function now expresses the whole contract: absent → graceful default; explicit supported → pass through; explicit unsupported → raise UnsupportedSearchType (422). - visualization.py: extract _build_search_algorithm() and use it in both unified_search and vector_search, removing the byte-identical select + try/except + if-semantic-else-bm25hybrid block from each endpoint. - viz_routes.py: the OAuth-session PCA-viz search selected the pure-dense SemanticSearchAlgorithm from a raw query param with no mode check, so in keyword mode it built a dense algorithm that fails against the sparse-only collection. Reject semantic when dense is disabled (bm25_hybrid stays valid in both modes). New unit test asserts the dense algo is never constructed. - Contract: parametrize the keyword-mode 422 pact over both /api/v1/search and /api/v1/vector-viz/search (they now share one gate). ADR-030 updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>



Summary
GET /api/v1/statusalready advertisessupported_search_types(ADR-030). This makes the enforcement side strict:/api/v1/searchand/api/v1/vector-viz/searchnow reject an explicit unsupportedalgorithmwith HTTP 422 instead of silently coercing it (e.g.semantic→bm25whileSEARCH_MODE=keyword).Motivation: a keyword-only tenant was still returning results for
semantic/hybrid queries because the server coerced the algorithm — lexical results dressed up as a semantic answer. Now the client (astrolabe) fails loud and self-corrects from the advertised set.Changes
api/management.py: newselect_search_algorithm(explicit → raiseUnsupportedSearchType; absent → graceful default) wrapping the retained lenientresolve_search_algorithm; newUnsupportedSearchTypeexception.api/visualization.py: both search endpoints translate the exception to 422. Absentalgorithmstill defaults gracefully across modes.TestSelectSearchAlgorithmunit tests; contract-first pact gains the keyword-mode 422 interaction (provider statethe server advertises keyword-only search support, already registered).docs/ADR-030: documents the strict rejection.Contract alignment
Paired with the astrolabe consumer PR (gates the algorithm client-side from
/api/v1/statusand surfaces this 422 as the backstop; adds the matching pact-php consumer interaction). Deck card #512.Test plan
pytest tests/unit/test_management_status_endpoint.py(26 passed)pytest -m contract tests/contract/test_mcp_status_search_types_consumer.py(3 passed)ruff+ty-check(pre-commit) cleanThis PR was generated with the help of AI, and reviewed by a Human