From 8c380bb6f347c72ac9e6063048891dd75573ffcf Mon Sep 17 00:00:00 2001 From: richtondag-art Date: Fri, 17 Jul 2026 10:16:28 +0100 Subject: [PATCH 1/2] fix(salience): exclude retired claims from the reflex the entity-salience reflex counted every claim referencing a matched entity, skipping the retired-claim exclusion that context, graph, digest, health and experts all apply. a superseded, archived or redacted claim therefore inflated an entity's claim_count and could be handed back as its top_claim_id, pointing an agent at knowledge the reviewer had already retracted. #78 closed this leak on the context path and the same exclusion was carried to the other read surfaces; the salience sidebar was missed. compute_salience now skips retired claims, so _meta.vouch_salience reports live evidence only. the regression test covers all three retired statuses. it fails on the previous code: the retired claim sorts ahead of the live one, so it both inflates claim_count and takes top_claim_id. --- CHANGELOG.md | 8 ++++++++ src/vouch/salience.py | 13 +++++++++++++ tests/test_salience.py | 27 ++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65407d84..40aaa629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ All notable changes to vouch are documented here. Format follows ## [Unreleased] +### Fixed +- the entity-salience reflex skipped the retired-claim exclusion every + other read surface applies, so a superseded, archived or redacted claim + still counted toward an entity's `claim_count` and could be handed back + as its `top_claim_id` — pointing an agent at redacted text the reviewer + had already pulled. `_meta.vouch_salience` now counts live claims only, + consistent with context, graph, digest, health and experts (#78). + ## [1.4.0] — 2026-07-17 ### Added diff --git a/src/vouch/salience.py b/src/vouch/salience.py index a6038f72..ef04b30b 100644 --- a/src/vouch/salience.py +++ b/src/vouch/salience.py @@ -28,12 +28,23 @@ from typing import Any from . import index_db +from .models import ClaimStatus from .storage import KBStore DEFAULT_WINDOW = 8 DEFAULT_TOP_K = 3 _EXPIRY_SECONDS = 30 * 60 +# A superseded / archived / redacted claim is not live evidence, so it must +# neither inflate an entity's claim_count nor surface as its top_claim_id +# (consistent with issue #78, and with the same exclusion in context, graph, +# digest, health and experts). +_RETIRED_CLAIM_STATUSES = frozenset({ + ClaimStatus.SUPERSEDED, + ClaimStatus.ARCHIVED, + ClaimStatus.REDACTED, +}) + @dataclass class _SessionBuffer: @@ -141,6 +152,8 @@ def compute_salience( # Claims referencing each matched entity, by claim id (for stable picking). claims_by_entity: dict[str, list[str]] = {} for claim in store.list_claims(): + if claim.status in _RETIRED_CLAIM_STATUSES: + continue for eid in claim.entities: if eid in scores: claims_by_entity.setdefault(eid, []).append(claim.id) diff --git a/tests/test_salience.py b/tests/test_salience.py index bfd47c68..f80f50b7 100644 --- a/tests/test_salience.py +++ b/tests/test_salience.py @@ -7,7 +7,7 @@ import pytest from vouch import health, salience -from vouch.models import Claim, Entity, EntityType +from vouch.models import Claim, ClaimStatus, Entity, EntityType from vouch.storage import KBStore @@ -45,6 +45,31 @@ def test_record_then_compute_highlights_entity(store: KBStore) -> None: assert rec["top_claim_id"] == "c1" +@pytest.mark.parametrize( + "status", + [ClaimStatus.SUPERSEDED, ClaimStatus.ARCHIVED, ClaimStatus.REDACTED], +) +def test_retired_claims_are_not_salient(store: KBStore, status: ClaimStatus) -> None: + # a retired claim is not live evidence: it must neither inflate claim_count + # nor surface as top_claim_id. "c0" sorts before the live "c1", so a + # retired claim that is still counted wins the top slot. + src = store.put_source(b"auth notes") + store.put_claim( + Claim( + id="c0", + text="auth used basic auth", + evidence=[src.id], + entities=["jwt"], + status=status, + ) + ) + for _ in range(3): + salience.record_query("sess-1", "jwt") + rec = salience.compute_salience(store, "sess-1")[0] + assert rec["claim_count"] == 1 + assert rec["top_claim_id"] == "c1" + + def test_attach_adds_meta_when_enabled(store: KBStore) -> None: for _ in range(3): salience.record_query("sess-1", "jwt") From e10376055ebea258d916642bd593ee1db4d34b15 Mon Sep 17 00:00:00 2001 From: richtondag-art Date: Fri, 17 Jul 2026 10:22:39 +0100 Subject: [PATCH 2/2] style(salience): lowercase the exclusion comment house style is lowercase prose in comments; the new frozenset comment opened with a capital. no behaviour change. --- src/vouch/salience.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vouch/salience.py b/src/vouch/salience.py index ef04b30b..889015fc 100644 --- a/src/vouch/salience.py +++ b/src/vouch/salience.py @@ -35,7 +35,7 @@ DEFAULT_TOP_K = 3 _EXPIRY_SECONDS = 30 * 60 -# A superseded / archived / redacted claim is not live evidence, so it must +# a superseded / archived / redacted claim is not live evidence, so it must # neither inflate an entity's claim_count nor surface as its top_claim_id # (consistent with issue #78, and with the same exclusion in context, graph, # digest, health and experts).