fix(memory): FTS5-safe search-term quoting — "TSK-0037"-style queries no longer crash (TSK-0043)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-07-01 01:52:57 +02:00
parent 8b17baae96
commit caf60ea939
2 changed files with 32 additions and 1 deletions

View File

@ -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, '""')}"`;
}

View File

@ -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',