From caf60ea939cb36a9bc80e6d4c28ed7ea373ca945 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Wed, 1 Jul 2026 01:52:57 +0200 Subject: [PATCH] =?UTF-8?q?fix(memory):=20FTS5-safe=20search-term=20quotin?= =?UTF-8?q?g=20=E2=80=94=20"TSK-0037"-style=20queries=20no=20longer=20cras?= =?UTF-8?q?h=20(TSK-0043)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- src/core/index.ts | 10 +++++++++- tests/memoryService.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/core/index.ts b/src/core/index.ts index 2b10c4b..d93f726 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -101,6 +101,8 @@ export class Index { } search(query: string): Array<{ id: string; type: string; title: string }> { + const ftsQuery = toFts5Phrase(query); + if (!ftsQuery) return []; const stmt = this.db.prepare(` SELECT e.id, e.type, e.title FROM search s @@ -108,7 +110,7 @@ export class Index { WHERE search MATCH @query ORDER BY rank `); - return stmt.all({ query }) as Array<{ id: string; type: string; title: string }>; + return stmt.all({ query: ftsQuery }) as Array<{ id: string; type: string; title: string }>; } list(type?: string, filters?: { status?: string; role?: string; assignedTo?: string }): IndexEntry[] { @@ -162,3 +164,9 @@ export class Index { this.db.close(); } } + +function toFts5Phrase(query: string): string { + const trimmed = query.trim(); + if (!trimmed) return ''; + return `"${trimmed.replace(/"/g, '""')}"`; +} diff --git a/tests/memoryService.test.ts b/tests/memoryService.test.ts index 37f9a83..3738faf 100644 --- a/tests/memoryService.test.ts +++ b/tests/memoryService.test.ts @@ -18,6 +18,29 @@ describe('memoryService', () => { expect(listMemory(cwd)).toHaveLength(1); }); + it('searches literal task IDs and other FTS5-special strings without throwing', () => { + addMemory(cwd, { + title: 'TSK-0037 result', + category: 'implementation', + content: 'Linked HOF-0030 and a:b plus foo-bar all belong to this note.', + }); + + expect(searchMemory(cwd, 'TSK-0037')).toHaveLength(1); + expect(searchMemory(cwd, 'HOF-0030')).toHaveLength(1); + expect(searchMemory(cwd, 'a:b')).toHaveLength(1); + expect(searchMemory(cwd, 'foo-bar')).toHaveLength(1); + expect(searchMemory(cwd, 'TTL')).toHaveLength(0); + }); + + it('keeps normal word search functional after FTS5 quoting', () => { + addMemory(cwd, { title: 'DNS cache', category: 'technical', content: 'Use TTL' }); + addMemory(cwd, { title: 'Other note', category: 'technical', content: 'No cache keyword here' }); + + const results = searchMemory(cwd, 'TTL'); + expect(results).toHaveLength(1); + expect(results[0].title).toBe('DNS cache'); + }); + it('stores relatedTasks in the memory object', () => { const m = addMemory(cwd, { title: 'TTL lesson',