fix(v0211): importNote 가 rebuildFtsTagsForNote 호출 (final review fix)

final code review 발견: F5 import path 가 note_tags INSERT 후 notes_fts.tags 갱신
안 해서 import 한 노트의 tag 가 keyword 검색에서 매칭 안 되는 회귀.

Cut C 의 importNote capture revision 누락 패턴과 동일 — single write path
정책 (Cut D 도입) 의 강제 검사 누락. importNote transaction 끝에서 호출하도록
fix + 회귀 test 2건 (insert path / fork path) 추가.

NoteRepository 안 note_tags INSERT path 는 updateAiResult / updateUserAiFields /
importNote 3곳, 셋 다 rebuildFtsTagsForNote 호출 보장 — invariant 회복.
This commit is contained in:
altair823
2026-05-10 00:46:58 +09:00
parent 5801a98a00
commit 735d5494f2
2 changed files with 60 additions and 0 deletions

View File

@@ -780,6 +780,11 @@ export class NoteRepository {
* 'capture' 첫 revision INSERT (createdAt = edited_at). 미수행 시 first user
* edit 직후 import 시점 본문이 history 에서 사라지는 회귀 (final review 발견).
*
* v0.2.11 Cut D — INSERT/fork 시 tags 추가 후 rebuildFtsTagsForNote(finalId)
* 호출 — m007 trigger 가 빈 tags='' 로 FTS row 만들고, note_tags INSERT 만으로는
* notes_fts.tags 갱신 안 됨. 미수행 시 import 한 노트가 tag keyword 검색에서
* 매칭 안 되는 회귀 (final review 발견).
*
* deletedAt merge (v0.2.3 #4, spec §8.2): source/dest 중 IS NOT NULL 우선
* (삭제 보존). skip 케이스에서 source NN + dest NULL 일 때만 dest 갱신.
* insert/fork 는 source 의 deletedAt 그대로 보존.
@@ -850,6 +855,8 @@ export class NoteRepository {
if (t.source === 'ai') linkAi.run(finalId, row.id);
else linkUser.run(finalId, row.id);
}
// v0.2.11 Cut D — note_tags 변경 후 notes_fts.tags 동기화 (single write path).
this.rebuildFtsTagsForNote(finalId);
}
});
tx();

View File

@@ -1103,4 +1103,57 @@ describe('NoteRepository — notes_fts tags sync (v0.2.11 Cut D)', () => {
.get(id) as { tags: string };
expect(row.tags.split(' ').sort()).toEqual(['new1', 'new2']);
});
it('importNote insert path: notes_fts.tags 가 csv 로 sync (final review fix)', () => {
const repo = new NoteRepository(db);
const r = repo.importNote({
id: '00000000-0000-0000-0000-000000000010',
rawText: 'imported with tags',
createdAt: '2026-04-01T00:00:00Z',
updatedAt: '2026-04-01T00:00:00Z',
aiTitle: 'imported title',
aiSummary: 'imported summary',
titleEditedByUser: false,
summaryEditedByUser: false,
aiProvider: 'p',
aiGeneratedAt: '2026-04-01T00:00:00Z',
userIntent: null,
intentPromptedAt: null,
tags: [
{ name: '기획', source: 'ai' },
{ name: '회의', source: 'user' }
]
});
expect(r.status).toBe('inserted');
const row = db
.prepare(`SELECT tags FROM notes_fts WHERE note_id=?`)
.get(r.id) as { tags: string };
expect(row.tags.split(' ').sort()).toEqual(['기획', '회의']);
});
it('importNote fork path: forked 노트의 notes_fts.tags 동기 (final review fix)', () => {
const repo = new NoteRepository(db);
const existing = repo.create({ rawText: 'v1' });
const r = repo.importNote({
id: existing.id,
rawText: 'imported v2 with tags',
createdAt: '2026-04-01T00:00:00Z',
updatedAt: '2026-04-01T00:00:00Z',
aiTitle: null,
aiSummary: null,
titleEditedByUser: false,
summaryEditedByUser: false,
aiProvider: null,
aiGeneratedAt: null,
userIntent: null,
intentPromptedAt: null,
tags: [{ name: '결재', source: 'user' }]
});
expect(r.status).toBe('forked');
expect(r.id).not.toBe(existing.id);
const row = db
.prepare(`SELECT tags FROM notes_fts WHERE note_id=?`)
.get(r.id) as { tags: string };
expect(row.tags).toBe('결재');
});
});