Skip to content

fix(experts): scope-filter claims before ranking entities - #715

Merged
plind-junior merged 4 commits into
vouchdev:testfrom
joaovictor91123:fix/experts-viewer-scope-leak
Jul 31, 2026
Merged

fix(experts): scope-filter claims before ranking entities#715
plind-junior merged 4 commits into
vouchdev:testfrom
joaovictor91123:fix/experts-viewer-scope-leak

Conversation

@joaovictor91123

Copy link
Copy Markdown
Contributor

What changed

rank_experts now takes an optional viewer: ViewerContext | None = None
(defaulting to scoping.viewer_from(config_path=store.config_path)) and
filters every claim through scoping.is_visible(claim.scope, viewer)
before it's eligible to contribute to an entity's ranking. The FTS
candidate fetch is routed through scoping.scoped_fetch_limit so a
mostly-out-of-scope KB doesn't starve the candidate pool before the
filter runs.

Why

rank_experts aggregated evidence density over every claim in the KB with
no viewer/scope filtering at all — the one claim-aggregating read surface
that never got it. Every sibling (context.py, graph.py, digest.py,
health.py, compile.py, and especially themes.detect_themes, the
closest shape-wise sibling) already excludes claims the viewer can't
retrieve. A project- or agent-scoped claim the caller cannot otherwise
fetch still inflated claim_count, citation_count, and score, and
could surface verbatim in top_claim_ids — handing the caller a claim id
it has no access to. Filtering before aggregating (not just scrubbing the
id list afterward) matters: claim_count, citation_count, and score
are all computed over the same set, so post-filtering only the ids would
still leave an entity whose evidence is mostly private outranking one the
viewer can actually read.

Confirmed with a repro: an entity with 3 project-scoped claims (a project
the caller isn't in) plus 1 public claim reports claim_count: 4 and
lists all 3 private claim ids in top_claim_ids, regardless of caller —
rank_experts had no viewer parameter to even attempt scoping with.

Fixes #714

What might break

Nothing for users with an existing .vouch/ directory — no on-disk shape,
object model, or CLI/MCP/JSONL surface signature change (viewer is an
internal, optional, keyword-only parameter with a config-resolved default,
mirroring detect_themes's exact contract; none of the three surfaces
pass it explicitly yet, so every existing caller keeps working unchanged).
Behaviorally: a KB with more than one project/agent scope in use will see
kb.experts results narrow to what the caller can actually retrieve,
which is the intended, documented behavior for every other read surface —
this was the one surface where it was missing.

VEP

Not applicable — no object model, kb.* method signature, on-disk
layout, bundle format, or audit-log shape change. An internal
scope-filtering correctness fix inside a read-only aggregation.

Tests

  • Local make check-equivalent: ruff clean (src + tests); mypy
    clean on experts.py; all tests/test_experts.py cases pass
    (9 pre-existing + 1 new)
  • New / changed behaviour has a test —
    test_scopes_ranking_to_the_viewer
  • CHANGELOG.md updated under ## [Unreleased]

rank_experts aggregated evidence density over every claim in the kb
with no viewer/scope filtering at all, unlike every sibling
claim-aggregating read surface (context.py, graph.py, digest.py,
health.py, compile.py, and themes.detect_themes, the closest
shape-wise sibling). a project- or agent-scoped claim the caller
cannot otherwise retrieve still inflated claim_count, citation_count
and score, and could surface verbatim in top_claim_ids - handing the
caller a claim id it cannot fetch.

filter claims through scoping.is_visible before they're eligible to
contribute, defaulting viewer to scoping.viewer_from(...) when the
caller passes none - matching detect_themes exactly. score after
filtering rather than only scrubbing the id list afterward: a
post-filter id scrub would still leave an entity whose evidence is
mostly private outranking one the viewer can actually read. route the
fts candidate fetch through scoped_fetch_limit so a mostly-out-of-scope
kb doesn't starve the candidate pool before the filter runs, the same
failure mode detect_themes already guards against.

Fixes vouchdev#714
@github-actions github-actions Bot added docs documentation, specs, examples, and repo guidance tests tests and fixtures size: S 50-199 changed non-doc lines labels Jul 31, 2026
joaovictor91123 and others added 3 commits July 31, 2026 01:01
a6c6862 (vouchdev#686) fixed capture.load_config's min_observations and
dedup_window_seconds to fall back to their defaults on a malformed
config value via the coerce_numeric() helper, instead of raising
ValueError straight out of load_config. 47eaf56 (vouchdev#645, realtime
opt-in) branched off the pre-fix capture.py and reintroduced the bare
int()/float() calls when it merged into test - the coerce_numeric
import survived (nothing else referenced it), but the two call sites
it fed didn't, silently reverting the fix and breaking ruff's
unused-import gate for every PR built on top of `test`.

restore the coerce_numeric() calls, matching recall.load_config's
still-intact equivalent.

unrelated to this PR's own change (experts.py viewer scoping); needed
only to get CI green on top of a currently-broken `test`.
changelog union only; no source conflict.
changelog union only; no source conflict.
@plind-junior
plind-junior merged commit 42a2b0c into vouchdev:test Jul 31, 2026
11 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

diff coverage: n/a — this PR changes no python under src/vouch/, so there is nothing for the gate to measure.

@github-actions github-actions Bot added the ci: passing ci is green label Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci: passing ci is green docs documentation, specs, examples, and repo guidance size: S 50-199 changed non-doc lines tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

kb.experts leaks out-of-scope claims into entity rankings (no viewer filtering)

2 participants