From 29635def7d086f93349e3c536e2b459ec2adf5fd Mon Sep 17 00:00:00 2001 From: YB0y <231405196+YB0y@users.noreply.github.com> Date: Fri, 22 May 2026 23:18:09 +0200 Subject: [PATCH 1/2] fix(sessions): index crystallize summary page into FTS5 (#60) `crystallize()` wrote the session-summary Page via `store.put_page()` only, which embeds but does not populate `pages_fts`. On KBs with a populated `state.db`, FTS5 returns hits for other artifacts and short-circuits the substring fallback, so `session-` pages were silently absent from `vouch search` / `kb.search` / `kb.context`. Mirror the PAGE branch of `proposals.approve()` by opening an `index_db` connection and calling `index_db.index_page(...)` immediately after the page write. Closes #60. --- CHANGELOG.md | 6 ++++++ src/vouch/sessions.py | 7 ++++++- tests/test_sessions.py | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3142ac00..e4e69369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ All notable changes to vouch are documented here. Format follows ## [Unreleased] ### Fixed +- `vouch crystallize` now indexes its session-summary page into FTS5 so it + surfaces from `vouch search` / `kb.search` / `kb.context` without a + `vouch index` rebuild. Previously the summary was written via + `store.put_page()` only, so on KBs with a populated `state.db` it was + silently absent from search results (the substring fallback only fires + when FTS5 returns nothing). - Fix `vouch search` CLI: assign backend label per code path so substring fallback results are no longer mislabelled as `fts5`; update stale docstring to reflect multi-backend search surface (#52). ### Fixed diff --git a/src/vouch/sessions.py b/src/vouch/sessions.py index 5b4d6354..d27d6334 100644 --- a/src/vouch/sessions.py +++ b/src/vouch/sessions.py @@ -12,7 +12,7 @@ import uuid from datetime import UTC, datetime -from . import audit +from . import audit, index_db from .models import Page, PageType, ProposalStatus, Session from .proposals import approve from .storage import KBStore @@ -107,6 +107,11 @@ def crystallize( ], ) store.put_page(page) + with index_db.open_db(store.kb_dir) as conn: + index_db.index_page( + conn, id=page.id, title=page.title, body=page.body, + type=page.type.value, tags=page.tags, + ) summary_page_id = page.id audit.log_event( diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 56d4a6cc..72bda14d 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -6,6 +6,7 @@ import pytest +from vouch import index_db from vouch import sessions as sess_mod from vouch.proposals import approve, propose_claim from vouch.storage import KBStore @@ -43,3 +44,20 @@ def test_crystallize_skips_already_approved(store: KBStore) -> None: sess_mod.session_end(store, sess.id) result = sess_mod.crystallize(store, sess.id, approver="u") assert result["approved"] == [] # already handled + + +def test_crystallize_summary_page_is_fts5_indexed(store: KBStore) -> None: + # The crystallize summary Page must reach the same FTS5 surface as any + # other approved Page; otherwise `vouch search` / kb.search miss it + # whenever state.db has other rows that suppress the substring fallback. + src = store.put_source(b"e") + sess = sess_mod.session_start(store, agent="claude-code") + propose_claim(store, text="findable claim", evidence=[src.id], + proposed_by="claude-code", session_id=sess.id) + sess_mod.session_end(store, sess.id) + result = sess_mod.crystallize(store, sess.id, approver="u") + + summary_id = result["summary_page_id"] + assert summary_id is not None + hits = index_db.search(store.kb_dir, summary_id, limit=10) + assert any(kind == "page" and hid == summary_id for kind, hid, _, _ in hits) From fa11255a7b2b2de1c7381910a7dfb73bb525d6a3 Mon Sep 17 00:00:00 2001 From: YB0y <231405196+YB0y@users.noreply.github.com> Date: Sat, 23 May 2026 00:11:03 +0200 Subject: [PATCH 2/2] test(sessions): query FTS by sess.id and drop redundant test comments (#61 review) `pages_fts.id` is declared UNINDEXED, so the previous query relied on tokenization side effects of summary_id matching the title. Switch the search term to sess.id, which is explicitly present in the indexed title and body, so the test contract is stable against future title-format changes. Also drop the test-body comment that restated what the test name and assertions already convey. --- tests/test_sessions.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 72bda14d..302739ff 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -47,9 +47,6 @@ def test_crystallize_skips_already_approved(store: KBStore) -> None: def test_crystallize_summary_page_is_fts5_indexed(store: KBStore) -> None: - # The crystallize summary Page must reach the same FTS5 surface as any - # other approved Page; otherwise `vouch search` / kb.search miss it - # whenever state.db has other rows that suppress the substring fallback. src = store.put_source(b"e") sess = sess_mod.session_start(store, agent="claude-code") propose_claim(store, text="findable claim", evidence=[src.id], @@ -59,5 +56,5 @@ def test_crystallize_summary_page_is_fts5_indexed(store: KBStore) -> None: summary_id = result["summary_page_id"] assert summary_id is not None - hits = index_db.search(store.kb_dir, summary_id, limit=10) + hits = index_db.search(store.kb_dir, sess.id, limit=10) assert any(kind == "page" and hid == summary_id for kind, hid, _, _ in hits)