refactor(search): address PR #750 round 6 review feedback

Closes out the remaining nits flagged in the round-6 review.

Critical:
- _verify_files contract comment now enumerates all None-return cases
  (404 + malformed PROPFIND XML) and documents the false-eviction
  trade-off; self-healing via re-indexing recovers
- int(r.id) cast at the SemanticSearchResult boundary now raises a
  TypeError with explicit doc_type/value context instead of bubbling
  up as an opaque "Search failed: ..." McpError

Design observations:
- nc_semantic_search_answer docstring documents the per-note
  round-trip cost from the post-verification race guard
- News verification latency hint added to configuration.md
- SemanticSearchResponse exposes verified_count + dropped_count so
  short result pages on high-ghost-density indexes are
  distinguishable from genuine scarcity. verify_search_results now
  returns (kept, dropped_count); production caller and tests updated

Minor:
- Comment clarifies the .get() fallback in verify_search_results is
  defensive only (run_verifier always populates the entry)
- Eviction task-group guard narrowed from except Exception to
  except RuntimeError (the only documented failure mode of
  TaskGroup.start_soon on a closed group)
- Indexer logs a warning when a deck_card task is missing
  board_id/stack_id, surfacing data-quality issues at index time
  rather than at verification time
- New unit test covers the news verifier's non-numeric-id fail-open
  path (one bad doc_id keeps the entire batch)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-01 21:02:27 +02:00
co-authored by Claude Opus 4.7
parent ffcca23a7b
commit e8df6003c5
7 changed files with 159 additions and 29 deletions
+28 -2
View File
@@ -172,11 +172,12 @@ def configure_semantic_tools(mcp: FastMCP):
eviction_task_group = getattr(
ctx.request_context.lifespan_context, "eviction_task_group", None
)
verified_results = await verify_search_results(
verified_results, dropped_count = await verify_search_results(
client,
all_results,
eviction_task_group=eviction_task_group,
)
verified_count = len(verified_results)
search_results = verified_results[:limit]
# Convert SearchResult objects to SemanticSearchResult for response.
@@ -188,9 +189,24 @@ def configure_semantic_tools(mcp: FastMCP):
# public API.
results = []
for r in search_results:
try:
narrowed_id = int(r.id)
except (TypeError, ValueError) as e:
# Re-raise with explicit context so the outer handler logs
# something operators can act on (the generic "Search
# failed: invalid literal for int()" is opaque).
raise TypeError(
f"SemanticSearchResult.id must be int-convertible, "
f"got {r.id!r} (type={type(r.id).__name__}) for "
f"doc_type={r.doc_type!r}. This indicates a doc_type "
f"with non-numeric ids has been indexed but the "
f"public response model has not been widened. Add "
f"the doc_type to the SemanticSearchResult.id type "
f"or convert at the verifier layer."
) from e
results.append(
SemanticSearchResult(
id=int(r.id),
id=narrowed_id,
doc_type=r.doc_type,
title=r.title,
category=r.metadata.get("category", "") if r.metadata else "",
@@ -304,6 +320,8 @@ def configure_semantic_tools(mcp: FastMCP):
query=query,
total_found=len(results),
search_method=f"bm25_hybrid_{fusion}",
verified_count=verified_count,
dropped_count=dropped_count,
)
except ValueError as e:
@@ -382,6 +400,14 @@ def configure_semantic_tools(mcp: FastMCP):
Note: Requires MCP client to support sampling. If sampling is unavailable,
the tool gracefully degrades to returning documents with an explanation.
The client may prompt the user to approve the sampling request.
Latency profile: For each note in the result page, this tool fetches
the full note body via ``client.notes.get_note`` after upstream
verify-on-read has already round-tripped to the same endpoint as a
race guard (ADR-019). Expect one additional Nextcloud round-trip per
note result; raising ``limit`` above the default of 5 amplifies this
cost roughly linearly. File / news / deck results do not pay this
cost — they reuse the verified excerpt.
"""
# 1. Retrieve relevant documents via existing semantic search
search_response = await nc_semantic_search(