From dfd3274016f33aeb1a068ce6d86f2a36f6fd64b5 Mon Sep 17 00:00:00 2001 From: Tet-9 Date: Thu, 21 May 2026 09:54:51 +0100 Subject: [PATCH 1/3] fix: assign backend label per code path in vouch search CLI The search command assigned a single backend variable after deciding which path ran, then stamped it on every result. Substring fallback hits were mislabelled as fts5 when FTS5 returned an empty list instead of raising. Restructured the branches so backend is set only when the path that produced the hits is known. Also updates the stale docstring from 'FTS5 search over claims, pages, and entities' to reflect the current multi-backend search surface. Fixes #52 --- CHANGELOG.md | 3 +++ src/vouch/cli.py | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d17081cc..3142ac00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ All notable changes to vouch are documented here. Format follows ## [Unreleased] +### Fixed +- 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 - Bundle import rejects tar members whose path escapes `kb_dir` (CVE-2007-4559, #9). Previously a crafted `.tar.gz` with a member diff --git a/src/vouch/cli.py b/src/vouch/cli.py index 4f1078cc..4ceb4c87 100644 --- a/src/vouch/cli.py +++ b/src/vouch/cli.py @@ -466,16 +466,16 @@ def crystallize(session_id: str, no_page: bool) -> None: @click.argument("query") @click.option("--limit", default=10, show_default=True, type=int) def search(query: str, limit: int) -> None: - """FTS5 search over claims, pages, and entities.""" + """Search claims, pages, and entities (embedding → fts5 → substring).""" from . import index_db store = _load_store() try: hits = index_db.search(store.kb_dir, query, limit=limit) - if not hits: + if hits: + backend = "fts5" + else: hits = store.search_substring(query, limit=limit) backend = "substring" - else: - backend = "fts5" except Exception: hits = store.search_substring(query, limit=limit) backend = "substring" From a8addb083f6262a8fcc9c66059b7d1dbb48d944b Mon Sep 17 00:00:00 2001 From: Tet-9 Date: Thu, 21 May 2026 22:04:03 +0100 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20revert=20logic=20change=20=E2=80=94?= =?UTF-8?q?=20keep=20docstring=20update=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The branch restructure was equivalent to the original and did not fix a real bug. Revert to the original control flow; keep only the docstring update from FTS5-only to multi-backend description. Fixes #52 --- src/vouch/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vouch/cli.py b/src/vouch/cli.py index 4ceb4c87..c00877d3 100644 --- a/src/vouch/cli.py +++ b/src/vouch/cli.py @@ -471,11 +471,11 @@ def search(query: str, limit: int) -> None: store = _load_store() try: hits = index_db.search(store.kb_dir, query, limit=limit) - if hits: - backend = "fts5" - else: + if not hits: hits = store.search_substring(query, limit=limit) backend = "substring" + else: + backend = "fts5" except Exception: hits = store.search_substring(query, limit=limit) backend = "substring" From 75f13fdc539a012976a2b7feaf7681db938dd933 Mon Sep 17 00:00:00 2001 From: Tet-9 Date: Thu, 21 May 2026 22:39:52 +0100 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20revert=20logic=20change=20=E2=80=94?= =?UTF-8?q?=20keep=20docstring=20update=20and=20add=20backend=20label=20te?= =?UTF-8?q?sts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the control-flow restructure (equivalent to original, no real bug). Keep only the docstring update from 'FTS5 search' to 'Search claims, pages, and entities (embedding -> fts5 -> substring)'. Add two tests asserting vouch search prints the correct backend label: - test_search_fts5_backend_label: FTS5 hit -> (fts5) - test_search_substring_backend_label: state.db absent -> (substring) Fixes #52 --- tests/test_cli.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index 6fa10343..0fde74f9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -78,3 +78,44 @@ def test_propose_entity_empty_name_shows_clean_error(store: KBStore) -> None: def test_show_missing_proposal_shows_clean_error(store: KBStore) -> None: result = CliRunner().invoke(cli, ["show", "no-such-proposal"]) _assert_clean_error(result, "proposal no-such-proposal") + + +def test_search_fts5_backend_label( + store: KBStore, monkeypatch: pytest.MonkeyPatch +) -> None: + """vouch search prints (fts5) when FTS5 returns hits.""" + from vouch import index_db + from vouch.proposals import approve as do_approve + from vouch.proposals import propose_claim + src = store.put_source(b"e") + pr = propose_claim(store, text="findable token", evidence=[src.id], proposed_by="agent") + do_approve(store, pr.id, approved_by="reviewer") + # Index only the FTS5 tables directly — no embedding stack needed + with index_db.open_db(store.kb_dir) as conn: + index_db.index_claim( + conn, id="c-findable", text="findable token", + type="observation", status="actionable", tags=[], + ) + runner = CliRunner() + result = runner.invoke(cli, ["search", "findable"]) + assert result.exit_code == 0, result.output + assert "(fts5)" in result.output + + +def test_search_substring_backend_label( + store: KBStore, monkeypatch: pytest.MonkeyPatch +) -> None: + """vouch search prints (substring) when FTS5 raises and fallback runs.""" + from vouch.proposals import approve as do_approve + from vouch.proposals import propose_claim + src = store.put_source(b"e") + pr = propose_claim(store, text="findable token", evidence=[src.id], proposed_by="agent") + do_approve(store, pr.id, approved_by="reviewer") + # Remove state.db so FTS5 raises and substring fallback runs + state_db = store.kb_dir / "state.db" + if state_db.exists(): + state_db.unlink() + runner = CliRunner() + result = runner.invoke(cli, ["search", "findable"]) + assert result.exit_code == 0, result.output + assert "(substring)" in result.output