fix(search): address PR #813 review (viz verify-on-read, owners cache, docs)

- viz_routes: run verify_search_results before returning results. After the
  accessible_owners expansion the viz can surface OTHER users' shared docs, so
  it must drop ones the caller can no longer access (revoked share) — same as
  the nc_semantic_search tool path. (Blocking review item.)
- access_filter: cache list_accessible_owners per user for 30s to keep the OCS
  shares round-trip off the search hot path (failures aren't cached); document
  the single-page OCS limitation; add a clear_accessible_owners_cache() test
  helper. Comment the empty-accessible_owners MatchAny([]) edge case.
- verification: comment why cross-user eviction is a deliberate no-op (eviction
  is scoped to the querying user's id, so a recipient's revoked access never
  deletes the owner's points; the recipient self-heals via accessible_owners).
- algorithms: declare SearchResult.original_score (set by the viz route) so the
  now-precisely-typed result list type-checks.
- tests: cross-user eviction-no-op safety test; autouse owners-cache reset in
  the access_filter + shared-search tests; replace async-no-await qdrant fakes
  with AsyncMock (clears SonarCloud S7503).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-29 02:15:44 +02:00
co-authored by Claude Opus 4.8
parent 6206f4a634
commit b1fac2d7a8
8 changed files with 122 additions and 16 deletions
+13 -4
View File
@@ -38,6 +38,7 @@ from nextcloud_mcp_server.search.context import (
get_chunk_bbox_and_page_from_qdrant,
get_chunk_with_context,
)
from nextcloud_mcp_server.search.verification import verify_search_results
from nextcloud_mcp_server.utils.validation import is_valid_nextcloud_doc_id
from nextcloud_mcp_server.vector.oauth_sync import (
NotProvisionedError,
@@ -226,10 +227,18 @@ async def vector_visualization_search(request: Request) -> JSONResponse:
# Sort by score before verification
all_results.sort(key=lambda r: r.score, reverse=True)
# No verification needed for visualization - we only need Qdrant metadata
# (title, excerpt, doc_type) which is already in search results.
# Verification is only needed for sampling (LLM needs full content).
search_results = all_results[:limit]
# Verify-on-read (ADR-019). Now that accessible_owners is expanded
# via OCS shares, the result set can include OTHER users' shared
# documents — so we must drop any the caller can no longer access
# (e.g. a revoked share whose index entry hasn't reconciled yet),
# exactly as the nc_semantic_search tool path does. Skipping this
# would let the viz surface stale titles/excerpts from another
# user's index after a share is revoked.
with trace_operation("vector_viz.verify_on_read"):
verified_results, _dropped = await verify_search_results(
nc_client, all_results
)
search_results = verified_results[:limit]
search_duration = time.perf_counter() - search_start
# Store original scores and normalize for visualization