diff --git a/src/vouch/context.py b/src/vouch/context.py index 2eaa8707..41369355 100644 --- a/src/vouch/context.py +++ b/src/vouch/context.py @@ -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]], @@ -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 @@ -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)) @@ -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( diff --git a/tests/test_context.py b/tests/test_context.py index 049ea0b8..5b91d4f0 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -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 /