From 35ae4879e73ebc79a1e937f5333edcdcccc99d0c Mon Sep 17 00:00:00 2001 From: richtondag-art Date: Fri, 17 Jul 2026 10:24:21 +0100 Subject: [PATCH] fix(hot-memory): keep actionable claims in the sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the sidebar's live set was working / stable / contested, which left actionable out. actionable is not a retired status — vault_sync treats it as approved knowledge alongside stable and contested, and synthesize pairs it with working. the effect was backwards. kb.confirm promotes a working claim to actionable and bumps last_confirmed_at, and the sidebar ranks on exactly that timestamp — so re-confirming a claim, the strongest signal it is still live, dropped it out of the "what just changed?" sidebar at the moment it became the freshest thing in the kb. _is_active is now the complement of the retired statuses (superseded / archived / redacted), so the sidebar agrees with every other read surface about what is live. the regression test walks the real lifecycle path: it asserts the claim is in the sidebar, calls confirm, and asserts it is still there. on the previous code the sidebar comes back empty. --- CHANGELOG.md | 8 ++++++++ src/vouch/hot_memory.py | 14 +++++++++++++- tests/test_hot_memory.py | 16 ++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65407d84..446c1789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ All notable changes to vouch are documented here. Format follows ## [Unreleased] +### Fixed +- `_meta.vouch_hot_memory` dropped `actionable` claims: the sidebar's live + set was `working` / `stable` / `contested`, so `kb.confirm` — which + promotes a working claim to `actionable` and bumps `last_confirmed_at` — + removed a claim from the recency sidebar at the moment it became the + freshest thing in the kb. the live set is now the complement of the + retired statuses, matching `vault_sync`. + ## [1.4.0] — 2026-07-17 ### Added diff --git a/src/vouch/hot_memory.py b/src/vouch/hot_memory.py index fea42ea1..8d8014e6 100644 --- a/src/vouch/hot_memory.py +++ b/src/vouch/hot_memory.py @@ -231,8 +231,20 @@ def _preview(text: str) -> str: return flat[: TEXT_PREVIEW_CHARS - 1] + "…" +# the live statuses — the complement of the retired set (superseded / archived +# / redacted). actionable belongs here: kb.confirm promotes a working claim to +# actionable and bumps last_confirmed_at, so leaving it out dropped a claim +# from the sidebar at the moment it became freshest. +_ACTIVE_STATUSES = frozenset({ + ClaimStatus.WORKING, + ClaimStatus.ACTIONABLE, + ClaimStatus.STABLE, + ClaimStatus.CONTESTED, +}) + + def _is_active(status: ClaimStatus) -> bool: - return status in {ClaimStatus.WORKING, ClaimStatus.STABLE, ClaimStatus.CONTESTED} + return status in _ACTIVE_STATUSES def query_bias_for_page(page: Page) -> str: diff --git a/tests/test_hot_memory.py b/tests/test_hot_memory.py index 102444aa..c5b7b9f8 100644 --- a/tests/test_hot_memory.py +++ b/tests/test_hot_memory.py @@ -10,6 +10,7 @@ from vouch import hot_memory as hot_mod from vouch.capabilities import METHODS from vouch.jsonl_server import handle_request +from vouch.lifecycle import confirm from vouch.models import ClaimStatus, Entity, EntityType, Page, PageType from vouch.proposals import approve, propose_claim from vouch.storage import KBStore @@ -50,6 +51,21 @@ def test_recently_approved_claim_appears(store: KBStore) -> None: assert "approved_at" in row +def test_confirmed_claim_stays_in_sidebar(store: KBStore) -> None: + # kb.confirm promotes a working claim to actionable and bumps + # last_confirmed_at. the claim is at its freshest, so dropping it from the + # recency sidebar is backwards. + cid = _approved_claim(store, "the sky is blue today") + assert [r["id"] for r in hot_mod.compute_hot_memory(store, limit=5)] == [cid] + + confirm(store, claim_id=cid, actor="reviewer") + hot_mod.reset_sidebar_cache() + + rows = hot_mod.compute_hot_memory(store, limit=5) + assert [r["id"] for r in rows] == [cid] + assert rows[0]["status"] == ClaimStatus.ACTIONABLE.value + + def test_empty_kb_yields_empty_list(store: KBStore) -> None: assert hot_mod.compute_hot_memory(store, limit=5) == []