Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions src/vouch/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,22 @@ def search_kb(
)


def _page_is_live(store: KBStore, page_id: str) -> bool:
"""False for an archived page, or one whose yaml is gone.

Shared by ``kb.search``'s hit filter and both context-pack builders. The
claim half of this predicate is inlined at each call site because those
callers need the fetched claim anyway (citations, origin tags); pages are
only ever tested, so the check lives here once — keeping it in three
places is what let ``kb.context`` keep serving archived pages after #581
fixed ``kb.search``.
"""
try:
return store.get_page(page_id).status is not PageStatus.ARCHIVED
except ArtifactNotFoundError:
return False


def _filter_live_hits(
store: KBStore,
hits: list[tuple[str, str, str, float]],
Expand All @@ -607,13 +623,8 @@ def _filter_live_hits(
continue
if claim.status in _RETRACTED_CLAIM_STATUSES:
continue
elif kind == "page":
try:
page = store.get_page(artifact_id)
except ArtifactNotFoundError:
continue
if page.status is PageStatus.ARCHIVED:
continue
elif kind == "page" and not _page_is_live(store, artifact_id):
continue
kept.append((kind, artifact_id, summary, score))
if limit is not None and len(kept) >= limit:
break
Expand Down Expand Up @@ -674,6 +685,8 @@ def _append_graph_neighbors(
if claim.status in _RETRACTED_CLAIM_STATUSES:
continue
cites = list(claim.evidence)
elif kind == "page" and not _page_is_live(store, nid):
continue
via = node.get("via", "")
parent_score = seed_scores.get(via, 0.5)
distance = int(node.get("distance", 1))
Expand Down Expand Up @@ -789,6 +802,10 @@ def build_context_pack(
continue
cites = list(claim.evidence)
origin = _origin_from_tags(claim.tags)
elif kind == "page" and not _page_is_live(store, hid):
# Archiving a page must remove it from recall, not just from
# kb.search — this is the surface that seeds agent context.
continue
summary = _enrich_summary(store, kind, hid, summary)
items.append(
ContextItem(
Expand Down
43 changes: 43 additions & 0 deletions tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,49 @@ def test_context_pack_excludes_archived_claims(store: KBStore) -> None:
assert not any(it["id"] == "c1" for it in pack["items"]), pack


def test_context_pack_excludes_archived_pages(store: KBStore) -> None:
"""The mirror of #581 on the recall side: kb.search learned to drop
archived pages, build_context_pack never did — so archiving a page hid
it from search while it kept being injected into every context pack."""
from vouch.models import Page, PageStatus, PageType

src = store.put_source(b"e")
store.put_page(Page(
id="p-live", title="mongodb ops", body="live page about mongodb",
type=PageType.CONCEPT, sources=[src.id],
))
store.put_page(Page(
id="p-arch", title="mongodb old", body="archived mongodb notes",
type=PageType.CONCEPT, status=PageStatus.ARCHIVED, sources=[src.id],
))
health.rebuild_index(store)

pack = context.build_context_pack(store, query="mongodb", limit=10)
ids = {item["id"] for item in pack["items"]}

assert "p-live" in ids, pack
assert "p-arch" not in ids, pack


def test_page_is_live_rejects_archived_and_missing(store: KBStore) -> None:
"""The shared predicate behind both context builders and kb.search.

The graph-expansion path carried the same gap as the main loop and now
routes through this, so pinning its contract pins both call sites.
"""
from vouch.models import Page, PageStatus, PageType

src = store.put_source(b"e")
store.put_page(Page(
id="p-arch", title="mongodb old", body="archived mongodb notes",
type=PageType.CONCEPT, status=PageStatus.ARCHIVED, sources=[src.id],
))
health.rebuild_index(store)

assert context._page_is_live(store, "p-arch") is False
assert context._page_is_live(store, "p-missing") is False


def test_search_kb_excludes_retracted_claims(store: KBStore) -> None:
"""Regression for #581: search_kb must apply the same retracted-status
filter as build_context_pack — otherwise kb.search leaks archived /
Expand Down
Loading