기존 UI 가 listTrash 200 limit 후 length 사용 → 350개 trash 시 dialog
"200개 영구 삭제" 표시되지만 실제 350 모두 삭제. 사용자 혼동 해소.
- NoteRepository.countTrashed() 신규 — SELECT COUNT(*) WHERE deleted_at IS NOT NULL
- IPC inbox:trashCount → countTrashed 사용
- 단위 +2 cases (>200 not capped, empty 0)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
restore 가 deleted_at = NULL 만 했음 → ai_status='failed' 인 노트는
영구 fail 상태로 복구. atomic transaction 안에서 ai_status='pending' reset
+ INSERT OR IGNORE INTO pending_jobs.
- failed → pending + pending_jobs 재처리 path 복구
- done 은 영향 X (이미 결과 있음)
- pending 은 pending_jobs 재생성 (defensive — trash 도중 jobs 미정상 상태 가능)
- 단위 +3 cases (failed/done/pending 각 케이스)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- abortController 가 method-local 에서 private instance field 로 이동
- public abort() 메서드 — 외부에서 in-flight generate 강제 중단
- ProviderHolder.replace() 시 호출되어 endpoint 변경 즉시 반영
- 단위 +2 cases (abort cancellation, model 파라미터)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- i1 (Important): AiWorker per-tag emit 루프에 res.tags Set dedup
AI 가 같은 태그 중복 응답 시 hit count 2번 emit 되던 통계 왜곡 수정
+ 테스트 1개 (중복 태그 1 hit + 1 miss 검증)
- m2 (Minor): NoteRepository.getTopUsedTags LIMIT-then-filter 테스트 갭
+ 테스트 1개 (limit=3 + 한글 1 + kebab 2 → 결과 length=2 lock-in)
skip: m1 (per-tag serial await — ai_succeeded 패턴 일관),
n1 (prompt 빈 줄 cosmetic), n2 (tagId positive — AUTOINCREMENT 1+)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- EmitInput union 13 → 15
- narrowing guards (noteId 없는 kind 분기) 에 tag_vocab_hit/miss 추가
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- GenerateInput.vocab?: string[] (optional, 미전달 시 빈 배열 처리)
- LocalOllamaProvider.generate 가 input.vocab ?? [] 를 buildPrompt 4th 인자로
- 단위 +1 case (vocab → prompt body)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
m1 — NoteRepository.test.ts 에 retryAllFailed OR IGNORE race-safe 회귀
가드 1 case 추가. failed 노트인데 pending_jobs row 가 이미 존재하는
비정상 race 상태 시뮬레이션 → INSERT OR IGNORE 라 duplicate 안 됨,
기존 attempts/next_run_at 보존.
m2 — store.retryAllFailed 의 r.count 무시 의도 주석 1줄.
단일 process (Electron) 환경 + 모든 ai_status='failed' 가 retry 대상이라
사용자 시점 카운트는 0 reset 가 정확.
n1 — AiWorker unreachableBackoffStep increment 명료화.
Math.min(..., length-1) → 명시적 if 가드 (step < length-1) 로 cap 도달 시
no-op 의도 가시화. 동작 동일.
n2 — AiWorker.processJob 의 max 의미 주석 1줄. unreachable/timeout 분기는
attempt -= 1 로 인덱스 stay 라 max 무관 — future maintainer 위해 명시.
n3 (FailedBanner inline style) 은 v0.2.4 backlog (banner theme cleanup).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
m1 — HealthChecker.last={ok:true} sentinel 의도 주석 (line 17).
첫 healthy=ok=true 면 transition 으로 인식 안 됨, ok=false 면 unreachable
transition 으로 정상 인식. telemetry 누락 0.
m2 — runOnce in-flight guard 추가. polling 첫 호출이 늦게 끝나는 동안
setInterval 가 두 번째 호출 시작하면 같은 promise 반환. healthCheck 가
idempotent HTTP 라 race 안전하지만, 이중 onUpdate/telemetry emit 회피.
m3 — main.ts before-quit 핸들러 통합. trayInterval cleanup 별도 핸들러
(line 349) 제거하고 health.stop() 핸들러 안에 흡수. 모든 cleanup 한 곳.
n1 — OllamaBanner 재확인 button 의 onClick 에 .catch 추가.
recheckOllama Promise rejection 시 console.warn (silent swallow 회피).
n2 — App.tsx useEffect deps array 의도 주석 1줄. onOllamaStatus 콜백이
useInbox.setState 직접 호출 — store reference 안정적이라 deps 불필요.
n3 — HealthChecker idempotent test 강화. <=2 → ===2 (정확).
두 timer 등록되면 4 (각 timer 마다 즉시+1s) 가 됨.
n4 — runOnce 의 manual emit 이 healthCheck *전에* fire 인 의도 주석.
provider 실패 시에도 manual 카운트 1:1 보장.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
m1 — spec §5.3 dialog 버튼 순서를 impl 패턴 (`['옮기기','취소'], defaultId=1, cancelId=1`) 으로 보정. project 의 permanentDelete/emptyTrash 와 일관 (위험 액션은 default focus = 취소).
m2 — telemetryEvents.test.ts 에 `expired_batch_trash` 의 extra-field 회귀 가드 추가. `expired_banner_shown` 과 대칭 (privacy invariant).
m3 — ExpiryBanner.InnerProps.candidates 타입을 narrow subset → `Note` 로 통일. v0.2.4 에서 Note 타입 진화 시 silent drift 방지.
m4 — onTrash 의 `void trashExpiredBatch(ids)` → `.catch((e) => console.warn(...))` 로 Promise rejection 가시화. (project-wide error toast 도입은 v0.2.4 backlog 유지)
n1 — 24h+ 앱 켜둔 상태에서 snooze 자동 만료. `setTimeout(snoozeUntilMs - now)` 으로 자정 KST 시점에 force re-render. (refreshMeta trigger 의존 제거)
n2 — CaptureService.listExpired 의 dedup signature reset on empty 의도 주석 1줄. future maintainer 위해.
n3 (`as any[]`) 은 repo 전체 hydrate 패턴 — 단독 fix 시 inconsistency. v0.2.4 backlog #22 로 합산.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #14 회차 1 review actionable — `inbox:trashCount` 와 `emptyTrash` dialog
가 `listTrashed({limit:200})` 로 카운트를 도출하면서 (a) hot path 에서 N rows
+ tags/media JOIN hydrate 비효율 (b) trash > 200 시 dialog message 가
실제 SQL DELETE 동작과 mismatch ('200개 영구 삭제합니다' 표시 vs 500개
실제 삭제) 발생.
NoteRepository.countTrashed() — `SELECT COUNT(*) FROM notes WHERE deleted_at
IS NOT NULL` 단일 쿼리. hydrate 없이 정확한 카운트만 반환. 두 IPC 핸들러를
이 메서드 호출로 교체.
테스트: 3 신규 단위 테스트 (0 trash / 부분 trash / 200 cap 초과 범위)
292 → 295 (+3). typecheck 0 errors.
deferrable (v0.2.4 backlog 그대로): AiWorker race guard 강화, restore self-guard,
limit 200 매직 넘버 상수화.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- I1: trashCount 가 upsertNote 안에서 항상 trashNotes.length 로 덮어써져
server 값 (refreshMeta) 손상. showTrash=true (trashNotes cache-loaded)
일 때만 local recompute.
- I2: restoreNote 의 "fallback for missed event" 주석 부정확 — main 은
trash/restore 시 pushNoteUpdated 안 보냄. 자가 갱신이 primary mechanism.
주석 정정.
- M5: restoreNote 테스트가 IPC 호출만 검증, 노트 이동 미검증. trashNotes
→ notes 라우팅 + deletedAt=null 어설션 추가. + I1 회귀 가드 테스트 신규.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T5 reviewer identified 2 reads outside NoteRepository that were missing the
'WHERE deleted_at IS NULL' filter, breaking the silent invariant beyond the
3 originally-listed methods.
- ContinuityService.get() now excludes trashed notes from streak / weekCount
/ lastNoteAt / recovery-toast math. A trashed note no longer counts toward
weekly streak (regression: streak felt fake after trash).
- NoteRepository.getPendingCount() now excludes trashed-but-still-pending
notes. trash() removes the pending_jobs row but leaves notes.ai_status='pending';
the count would have drifted upward as users trashed pending notes.
- MediaGc.run() gets an inline comment documenting why it intentionally does
NOT filter — trashed notes still own their media until permanentDelete /
emptyTrash. Removing here would defeat restore.
Also: migrations.due_date.test.ts had 2 brittle assertions
(latestVersion()===2, user_version===2) that broke with v3. Migration-system
version assertions belong in migrations.test.ts (already covered there);
m002-specific test keeps the due_date column assertion which is version-stable.
Tests: 245 → 265 (+20). typecheck 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>