From 27b23a5cb47fc581ed1fad1ebf8b324b27882495 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:52:10 +0000 Subject: [PATCH 1/4] Initial plan From 4c661ce46aafc08da1fb7498fdd45dd0748f43c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:54:24 +0000 Subject: [PATCH 2/4] fix: add LIMIT to _query_chunk_search_fallback to bound result sets Co-authored-by: duck-lint <250741170+duck-lint@users.noreply.github.com> Agent-Logs-Url: https://github.com/duck-lint/local-agent/sessions/bed97c53-aa6e-4946-87d6-4562d2cf0d13 --- agent/corpus_db.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/agent/corpus_db.py b/agent/corpus_db.py index ca48865..d4df7bf 100644 --- a/agent/corpus_db.py +++ b/agent/corpus_db.py @@ -460,10 +460,10 @@ def query_chunks_lexical( actual_backend = "projection_substring" if not backend_warning: backend_warning = "FTS unavailable or yielded no tokenized query; using projection substring fallback." - candidates = _query_chunk_search_fallback(conn, query_text=raw_query) + candidates = _query_chunk_search_fallback(conn, query_text=raw_query, limit=fetch_limit) else: actual_backend = "projection_substring" - candidates = _query_chunk_search_fallback(conn, query_text=raw_query) + candidates = _query_chunk_search_fallback(conn, query_text=raw_query, limit=fetch_limit) if not candidates: return [] @@ -716,6 +716,7 @@ def _query_chunk_search_fallback( conn: sqlite3.Connection, *, query_text: str, + limit: int, ) -> list[dict[str, object]]: rows = conn.execute( """ @@ -746,6 +747,7 @@ def _query_chunk_search_fallback( OR instr(lower(journal_entry_date), lower(?)) > 0 OR instr(lower(layer), lower(?)) > 0 OR instr(lower(register), lower(?)) > 0 + LIMIT ? """, ( query_text, @@ -759,6 +761,7 @@ def _query_chunk_search_fallback( query_text, query_text, query_text, + max(1, int(limit)), ), ).fetchall() out: list[dict[str, object]] = [] From a2a3bf6e3fb9d6d9978f076b2ddd7d718c861d24 Mon Sep 17 00:00:00 2001 From: duck-lint <250741170+duck-lint@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:00:59 -0600 Subject: [PATCH 3/4] Update agent/corpus_db.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: duck-lint <250741170+duck-lint@users.noreply.github.com> --- agent/corpus_db.py | 1 + 1 file changed, 1 insertion(+) diff --git a/agent/corpus_db.py b/agent/corpus_db.py index d4df7bf..52f9711 100644 --- a/agent/corpus_db.py +++ b/agent/corpus_db.py @@ -747,6 +747,7 @@ def _query_chunk_search_fallback( OR instr(lower(journal_entry_date), lower(?)) > 0 OR instr(lower(layer), lower(?)) > 0 OR instr(lower(register), lower(?)) > 0 + ORDER BY chunk_key LIMIT ? """, ( From 0c8fc418d4466a6d521964180c07a07009ec1dfd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:04:11 +0000 Subject: [PATCH 4/4] test: add regression tests for _query_chunk_search_fallback LIMIT enforcement Co-authored-by: duck-lint <250741170+duck-lint@users.noreply.github.com> Agent-Logs-Url: https://github.com/duck-lint/local-agent/sessions/ad4ff7de-5ca2-4fd0-9a25-44ff1603d5f3 --- tests/test_retrieval_contract.py | 86 ++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/tests/test_retrieval_contract.py b/tests/test_retrieval_contract.py index e3c9cd3..b88e949 100644 --- a/tests/test_retrieval_contract.py +++ b/tests/test_retrieval_contract.py @@ -1,9 +1,11 @@ from __future__ import annotations from datetime import datetime, timezone +import sqlite3 import unittest from unittest.mock import patch +from agent.corpus_db import _query_chunk_search_fallback from agent.embeddings import sync_embeddings from agent.retrieval import RetrievedChunk, _apply_bounded_rerank from tests.support import AppFixture, dummy_embedder_factory @@ -262,5 +264,89 @@ def test_bounded_rerank_falls_back_to_mtime_when_journal_entry_date_is_missing(s self.assertEqual(reranked[0].chunk_key, "newer-mtime") +class FallbackLimitRegressionTests(unittest.TestCase): + """Regression tests asserting that _query_chunk_search_fallback respects its LIMIT.""" + + def _make_conn(self) -> sqlite3.Connection: + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = OFF") # no documents/sources tables in this minimal fixture + conn.execute( + """ + CREATE TABLE chunk_search ( + id INTEGER PRIMARY KEY, + doc_id INTEGER NOT NULL, + chunk_key TEXT NOT NULL UNIQUE, + chunk_kind TEXT NOT NULL, + rel_path TEXT NOT NULL, + body_text TEXT NOT NULL, + chunk_title TEXT NOT NULL, + heading_path TEXT NOT NULL, + canonical_name TEXT NOT NULL, + aliases_text TEXT NOT NULL, + tags_text TEXT NOT NULL, + note_type TEXT NOT NULL, + journal_entry_date TEXT, + layer TEXT NOT NULL, + register TEXT NOT NULL, + updated_at REAL NOT NULL + ) + """ + ) + return conn + + def _insert_rows(self, conn: sqlite3.Connection, count: int, *, query_word: str) -> None: + for i in range(count): + conn.execute( + """ + INSERT INTO chunk_search + (doc_id, chunk_key, chunk_kind, rel_path, body_text, + chunk_title, heading_path, canonical_name, aliases_text, + tags_text, note_type, journal_entry_date, layer, register, updated_at) + VALUES (?, ?, 'content', ?, ?, ?, '', '', '', '', 'knowledge', NULL, '', '', 0.0) + """, + ( + i, + f"chunk-{i}", + f"doc-{i}.md", + f"{query_word} body text for row {i}", + f"{query_word} title {i}", + ), + ) + conn.commit() + + def test_fallback_candidate_count_is_capped_at_limit(self) -> None: + conn = self._make_conn() + total_rows = 200 + limit = 10 + self._insert_rows(conn, total_rows, query_word="common") + + results = _query_chunk_search_fallback(conn, query_text="common", limit=limit) + + self.assertLessEqual( + len(results), + limit, + f"Expected at most {limit} rows but got {len(results)}", + ) + + def test_fallback_returns_all_matching_when_below_limit(self) -> None: + conn = self._make_conn() + self._insert_rows(conn, 5, query_word="raretoken") + + results = _query_chunk_search_fallback(conn, query_text="raretoken", limit=50) + + self.assertEqual(len(results), 5) + + def test_fallback_rows_include_backend_score(self) -> None: + conn = self._make_conn() + self._insert_rows(conn, 3, query_word="scorecheck") + + results = _query_chunk_search_fallback(conn, query_text="scorecheck", limit=10) + + for row in results: + self.assertIn("backend_score", row) + self.assertIsInstance(row["backend_score"], float) + + if __name__ == "__main__": unittest.main()