Skip to content

fix(context,storage): exclude retracted claims from kb.context and reindex FTS5 on update#79

Merged
plind-junior merged 2 commits into
vouchdev:testfrom
galuis116:fix/context-filter-and-reindex-retracted-claims
May 28, 2026
Merged

fix(context,storage): exclude retracted claims from kb.context and reindex FTS5 on update#79
plind-junior merged 2 commits into
vouchdev:testfrom
galuis116:fix/context-filter-and-reindex-retracted-claims

Conversation

@galuis116

@galuis116 galuis116 commented May 25, 2026

Copy link
Copy Markdown
Contributor

Problem

kb.context returns claims whose status is ARCHIVED, SUPERSEDED,
or REDACTED as if they were live. Two compounding bugs combine to
produce this:

Bug A — build_context_pack has no status filter.
src/vouch/context.py walks the hits from _retrieve and appends
every match to the context pack regardless of claim.status. Once
a claim is indexed, it stays in retrieval forever.

Bug B — store.update_claim never refreshes the FTS5 row. It
calls _embed_and_store (semantic cache) but never
index_db.index_claim (FTS5). So lifecycle.archive,
lifecycle.supersede, lifecycle.contradict, and
lifecycle.confirm — all of which finish with
store.update_claim(...) — leave claims_fts.status stuck at
whatever value it had at first-index time.

Compound effect: even a future fix that adds a status filter to
context.py would still leak archived claims, because the FTS5
status column it would filter on is stale. Both bugs must be fixed
for retrieval to be correct.

The whole point of ClaimStatus.{ARCHIVED, SUPERSEDED, REDACTED}
is to remove a claim from active circulation while keeping its
history. Today those states are purely decorative on the read side;
agents quote retracted knowledge as if it were live.

End-to-end repro on main shows:

on-disk claim.status: archived
FTS5 row after archive: status='working'   (expected: 'archived')
items returned: 1                          (the archived claim)

With this PR applied:

on-disk claim.status: archived
FTS5 row after archive: status='archived'  (expected: 'archived')
items returned: 0

Fix

Two files, both required:

  1. src/vouch/storage.pyupdate_claim now also calls
    index_db.index_claim under a short-lived sqlite connection,
    mirroring proposals.approve's first-index pattern. FTS5 errors
    are logged-and-skipped so a lifecycle op never fails because the
    embedding/FTS5 layer is misconfigured.

  2. src/vouch/context.pybuild_context_pack resolves each
    claim hit, drops it if the on-disk status is in {ARCHIVED, SUPERSEDED, REDACTED}, and drops it if the claim YAML has been
    deleted between index time and now. CONTESTED claims keep
    surfacing so contradictions remain visible (consistent with
    how vouch lint treats them as cautions rather than removals).

The _citations_for_claim helper is folded into the same
get_claim call since the body is needed for the status check
anyway, avoiding a double lookup.

No on-disk-layout, schema, or bundle-format change. Existing
KBs are unaffected: the FTS5 row gets updated on the next
mutation; archived claims that survive an old index are still
correctly filtered out at context-pack assembly time.

Tests

Three regression tests in tests/test_context.py:

  • test_context_pack_excludes_archived_claims — file a claim,
    archive it, assert it's absent from kb.context results.
  • test_context_pack_excludes_superseded_claims — supersede
    old by new, assert new is retrievable and old is gone.
  • test_update_claim_refreshes_fts5_status — direct SQL query
    against state.db confirms claims_fts.status moved from
    'working' to 'archived' after lifecycle.archive.

All 8 tests/test_context.py tests pass on Windows (Python
3.12.13). Lint clean (ruff check src tests). The full suite
shows the same pre-existing Windows-only failures present on
main (O_NOFOLLOW in storage.py, bundle round-trip CRLF) —
both untouched by this PR.

Fixes #78

Summary by CodeRabbit

  • Bug Fixes

    • Context results now exclude archived, superseded, and redacted claims for more accurate context packs.
    • Full‑text search index is refreshed when claim lifecycle statuses change so results reflect current claim states.
  • Tests

    • Added regression tests validating archived/superseded claims are excluded and that the search index stays in sync after status updates.

Review Change Stack

…index FTS5 on update (vouchdev#78)

Two compounding bugs let archived / superseded / redacted claims keep
flowing back to agents through kb.context:

  1. build_context_pack had no claim.status filter, so any retrieval
     hit was appended to the pack regardless of subsequent lifecycle
     mutations.

  2. store.update_claim refreshed the embedding cache but never
     re-indexed the FTS5 row, so claims_fts.status stayed frozen at
     first-index time. Every lifecycle op (archive, supersede,
     contradict, confirm) routes through update_claim and was
     therefore invisible to FTS5 / semantic search.

This made ClaimStatus.{ARCHIVED, SUPERSEDED, REDACTED} purely
decorative on the read side — agents kept quoting retracted
knowledge as if it were live.

Fix:

  - storage.update_claim now also calls index_db.index_claim under a
    short-lived sqlite connection (mirroring proposals.approve's
    first-index pattern). FTS5 errors are logged-and-skipped so a
    lifecycle op never fails because the embedding/FTS5 layer is
    misconfigured.

  - context.build_context_pack resolves each claim hit, drops it if
    the on-disk status is in {ARCHIVED, SUPERSEDED, REDACTED}, and
    drops it if the claim YAML has been deleted between index time
    and now. CONTESTED claims keep surfacing so contradictions
    remain visible.

Tests:

  - test_context_pack_excludes_archived_claims
  - test_context_pack_excludes_superseded_claims
  - test_update_claim_refreshes_fts5_status (asserts the FTS5 status
    column via direct SQL against state.db)

No on-disk-layout, schema, or bundle-format change.
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 93086540-d42c-4553-8066-3e9f9f516296

📥 Commits

Reviewing files that changed from the base of the PR and between 0708c74 and 23290a0.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • src/vouch/storage.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

📝 Walkthrough

Walkthrough

This PR fixes a bug where archived, superseded, and redacted claims were still appearing in kb.context. The fix adds status filtering in context pack building and ensures FTS5 status rows are reindexed when claims are updated during lifecycle mutations.

Changes

Retracted Claim Filtering and FTS5 Sync

Layer / File(s) Summary
Retracted claim filtering in context pack
src/vouch/context.py
Imports ClaimStatus and related types, defines _RETRACTED_CLAIM_STATUSES frozenset, removes _citations_for_claim helper, and updates build_context_pack to skip claims with retracted statuses and resolve missing claims gracefully.
FTS5 reindexing on claim update
src/vouch/storage.py
Imports sqlite3 and extends KBStore.update_claim to reindex the claim in the FTS5 search index after persisting YAML, with graceful error handling that logs warnings but does not abort the update.
Regression tests for filtering and index sync
tests/test_context.py
Adds three tests: one verifying archived claims are excluded from context results, one verifying superseded claims show the new claim while hiding the old, and one verifying FTS5 status is synchronized after archival via direct database inspection.
Changelog documentation
CHANGELOG.md
Documents the fix in the Unreleased section, explaining that kb.context filters out retracted statuses and how the fix is implemented via FTS5 reindexing and context filtering.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit reviewed the filtering quest,
Where archived claims could finally rest,
FTS5 refreshed with nimble paws,
Retracted lines no longer cause,
Context clean and tidy—huzzah!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the PR's main changes: excluding retracted claims from kb.context and reindexing FTS5 on claim updates.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #78: filtering retracted claims (ARCHIVED, SUPERSEDED, REDACTED) in build_context_pack, reindexing FTS5 rows in update_claim, and adding comprehensive regression tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing issue #78: CHANGELOG.md documents the fix, context.py filters retracted claims, storage.py reindexes FTS5, and tests validate both components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/vouch/context.py (1)

173-176: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Filter explain rows the same way as items.

At Line [173], explain is built from raw hits, so retracted/missing claims can still surface when explain=True even though they were excluded from items.

Suggested fix
 def build_context_pack(
@@
 ) -> ContextPack | dict[str, Any]:
     hits = _retrieve(store, query, limit)
     items: list[ContextItem] = []
+    explain_rows: list[dict[str, Any]] = []
     for kind, hid, summary, score, backend in hits:
@@
         items.append(
             ContextItem(
                 id=hid, type=cast(ContextItemKind, kind), summary=summary, score=score,
                 backend=backend, citations=cites,
                 freshness="unknown",
             )
         )
+        if explain:
+            explain_rows.append(
+                {"kind": kind, "id": hid, "score": score, "backend": backend}
+            )
@@
     result["backend"] = hits[0][4] if hits else "none"
     if explain:
-        result["explain"] = [
-            {"kind": k, "id": i, "score": sc, "backend": hits[0][4] if hits else "none"}
-            for k, i, _sn, sc, _be in hits
-        ]
+        result["explain"] = explain_rows
     return result
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/context.py` around lines 173 - 176, The explain list is currently
built directly from raw hits (variable hits) which allows retracted or missing
claims to appear even when those were filtered out of items; modify construction
of result["explain"] to iterate the same filtered sequence used to build items
(e.g., use filtered_hits or the same comprehension/predicate applied when
creating items) so that each {"kind": k, "id": i, "score": sc, "backend": ...}
entry is only created for hits that passed the items filter; ensure you
reference the same hit tuple structure and backend extraction (hits[0][4] or
similar) as in the original comprehension.
🧹 Nitpick comments (1)
tests/test_context.py (1)

83-142: ⚡ Quick win

Add a direct REDACTED regression case.

These tests cover ARCHIVED and SUPERSEDED well, but the runtime filter also excludes REDACTED. A dedicated test would prevent regressions on that status path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_context.py` around lines 83 - 142, Add a test that mirrors the
archived/superseded cases to cover the REDACTED path: create a new test (e.g.
test_context_pack_excludes_redacted_claims) that uses store.put_source and
store.put_claim to insert a claim, calls health.rebuild_index, verifies the
claim is returned by context.build_context_pack for a matching query, then call
lifecycle.redact(store, claim_id="c1", actor="reviewer") and assert the claim is
no longer present in the pack; optionally also check the FTS row (via
sqlite3.connect(store.kb_dir / "state.db")) to assert claims_fts.status ==
"redacted" similar to test_update_claim_refreshes_fts5_status.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/vouch/context.py`:
- Around line 173-176: The explain list is currently built directly from raw
hits (variable hits) which allows retracted or missing claims to appear even
when those were filtered out of items; modify construction of result["explain"]
to iterate the same filtered sequence used to build items (e.g., use
filtered_hits or the same comprehension/predicate applied when creating items)
so that each {"kind": k, "id": i, "score": sc, "backend": ...} entry is only
created for hits that passed the items filter; ensure you reference the same hit
tuple structure and backend extraction (hits[0][4] or similar) as in the
original comprehension.

---

Nitpick comments:
In `@tests/test_context.py`:
- Around line 83-142: Add a test that mirrors the archived/superseded cases to
cover the REDACTED path: create a new test (e.g.
test_context_pack_excludes_redacted_claims) that uses store.put_source and
store.put_claim to insert a claim, calls health.rebuild_index, verifies the
claim is returned by context.build_context_pack for a matching query, then call
lifecycle.redact(store, claim_id="c1", actor="reviewer") and assert the claim is
no longer present in the pack; optionally also check the FTS row (via
sqlite3.connect(store.kb_dir / "state.db")) to assert claims_fts.status ==
"redacted" similar to test_update_claim_refreshes_fts5_status.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a04edce3-01dc-4f79-8a3e-7bac3d1bd7ec

📥 Commits

Reviewing files that changed from the base of the PR and between c339193 and 0708c74.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src/vouch/context.py
  • src/vouch/storage.py
  • tests/test_context.py

@plind-junior

Copy link
Copy Markdown
Member

PR should target test branch

@galuis116
galuis116 changed the base branch from main to test May 27, 2026 16:42
@galuis116

Copy link
Copy Markdown
Contributor Author

target test branch

Done

@plind-junior
plind-junior merged commit 16afe71 into vouchdev:test May 28, 2026
5 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jun 9, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: kb.context returns archived/superseded claims; lifecycle mutations never re-index FTS5 status

2 participants