feat(trash): #4 휴지통 + migration v3 (v0.2.3 2/7) #14

Merged
altair823 merged 26 commits from feat/v023-trash into main 2026-05-01 14:06:24 +00:00

26 Commits

Author SHA1 Message Date
altair823
87b6d71628 fix(trash): add repo.countTrashed() — fix UI 200-cap mismatch (review 회차 1)
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>
2026-05-01 22:45:11 +09:00
altair823
2ac4d648c1 chore(trash): #4 closure — gates verified + roadmap mark complete
v0.2.3 #4 휴지통 (soft delete + migration v3) 종료.

게이트:
- typecheck: 0 errors
- 단위 테스트: 245 → 292 (+47, schema/repo/AiWorker/CaptureService/Continuity/
  ImportService/ExportService/store 전반)
- e2e smoke: 1/1 PASS

기능:
- migration v3 — deleted_at + last_recalled_at + recall_dismissed_at
- NoteRepository: trash/restore/permanentDelete/emptyTrash/listTrashed
- AiWorker.processJob deletedAt 가드
- CaptureService 4 신규 메서드 + idempotency 가드 + 4 telemetry emit
- telemetryStats: 4 신규 컬럼 + 휴지통 회수율 ratio
- ImportService: deletedAt 보존 + skip-merge 정책
- ExportService 회귀 가드 (T5 listAll filter 자동 동작)
- IPC 5 신규 채널 + native dialog confirm
- zustand store: showTrash/trashNotes/trashCount + 5 actions
- App.tsx 헤더 탭 + 휴지통 view + bulk 비우기
- NoteCard mode='trash' read-only

기타 fix (cross-task):
- ContinuityService streak 가 trash 노트 무시
- getPendingCount 가 trash 노트 무시 (drift 방지)
- MediaGc intentional non-filter 주석 (restore 시 media 보존)

deferred (v0.2.4 backlog):
- exhaustiveness check on stats union
- restore 시 pending_jobs 재생성 정책
- inbox:trashCount cap 200 → repo.countTrashed()
- inbox:delete 채널 rename
- 탭 ARIA role="tab" 정정
- per-note 영구 삭제 텔레메트리 기반 retire 검토

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:53:26 +09:00
altair823
03bca3ed59 feat(trash): Inbox 탭 toggle + 휴지통 view + NoteCard mode prop (#4 v0.2.3) 2026-05-01 21:48:15 +09:00
altair823
df85b88424 fix(trash): T13 review — trashCount clobber guard + restoreNote test (review I1+I2+M5)
- 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>
2026-05-01 21:43:59 +09:00
altair823
99cdc346d2 feat(trash): zustand store — showTrash/trashNotes/trashCount + 5 actions (#4 v0.2.3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 21:38:30 +09:00
altair823
3e4ad6ec91 refactor(trash): emptyTrash IPC dedup query (review T12 nit) 2026-05-01 21:35:31 +09:00
altair823
dd74aec884 feat(trash): IPC 5 channels + native dialog confirm + InboxApi extension (#4 v0.2.3) 2026-05-01 21:32:22 +09:00
altair823
cdceb609e6 test(trash): ExportService excludes trashed notes (regression guard, #4 v0.2.3) 2026-05-01 21:28:12 +09:00
altair823
6f0d032ff1 refactor(trash): import skip-merge reuses trash() for pending_jobs invariant (review T10 minor #1) 2026-05-01 21:26:54 +09:00
altair823
a5f23b925e feat(trash): ImportService deletedAt preservation + skip-merge policy (#4 v0.2.3) 2026-05-01 21:23:23 +09:00
altair823
468ea90d6c fix(trash): idempotency guards on delete/restore/permanent (review T9 important #1+#2)
review T9 flagged 2 service-layer defenses:

#1: deleteNote/restoreNote/permanentDeleteNote 의 idempotency. 이미 trash 인
노트를 trash 하거나, 이미 active 인 노트를 restore 하거나, 존재하지 않는 노트를
permanentDelete 시 telemetry 가 spurious 하게 emit → restore/trash ratio (T8)
오염. findById 가드로 의미 없는 emit skip.

#2: permanentDeleteNote 의 disk cleanup unguarded. store.deleteNoteDirectory
실패 시 (Windows file-lock 등) telemetry 가 영영 emit 안 되고 IPC 호출자가
이미 성공한 작업에 에러 propagate. emptyTrash 와 동일하게 try/catch 로 감싸
best-effort. orphan dir 은 future janitor 가 정리.

Tests: 12/12 still pass. typecheck 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:20:03 +09:00
altair823
b19ea6423a feat(trash): CaptureService soft-delete + restore/permanent/empty + 4 emits (#4 v0.2.3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 21:16:26 +09:00
altair823
e6a945cad4 feat(trash): telemetryStats 4 new counters + 휴지통 회수율 ratio (#4 v0.2.3) 2026-05-01 21:11:15 +09:00
altair823
c5329f1ccc refactor(test): replace as-cast with discriminant narrowing (review T7 I-1) 2026-05-01 21:09:04 +09:00
altair823
284bfcbdd1 feat(trash): telemetry 4 new kinds (trash/restore/permanent_delete/empty_trash) (#4 v0.2.3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 21:05:06 +09:00
altair823
78c10e8817 feat(trash): AiWorker.processJob deletedAt guard (#4 v0.2.3) 2026-05-01 21:00:09 +09:00
altair823
3c780a7464 fix(trash): close active-query invariant leaks (review T5 important #1+#2)
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>
2026-05-01 20:58:18 +09:00
altair823
2203bcf65b feat(trash): active queries exclude deleted_at IS NOT NULL (#4 v0.2.3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:53:03 +09:00
altair823
70a69f0ae3 refactor(trash): emptyTrash uses DELETE...RETURNING (review T4 S1) 2026-05-01 20:51:06 +09:00
altair823
11703b976e feat(trash): NoteRepository.permanentDelete/emptyTrash/listTrashed (#4 v0.2.3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:47:05 +09:00
altair823
bf49b8351e feat(trash): NoteRepository.restore (#4 v0.2.3) 2026-05-01 20:42:42 +09:00
altair823
13da554461 feat(trash): NoteRepository.trash with pending_jobs cleanup (#4 v0.2.3) 2026-05-01 20:38:17 +09:00
altair823
3797e6c4f3 docs(m003): add dormant-columns rationale comment (review T1 minor #1) 2026-05-01 20:36:27 +09:00
altair823
5bcfd26bfd feat(trash): migration v3 + Note type extension (#4 v0.2.3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:32:52 +09:00
altair823
b93185edd5 docs(plan): #4 휴지통 구현 계획 (v0.2.3 2/7)
15 task TDD plan — migration v3, Note type extension, NoteRepository 신규
4메서드 + active query 일괄 변경, AiWorker deletedAt guard, telemetry 4 new
kinds + stats.md 회수율 ratio, CaptureService soft delete + 3 신규 메서드
+ 4 emit, ImportService deletedAt 보존, ExportService 회귀 가드, IPC 5 신규
채널 + native dialog confirm, zustand store + 5 actions, Inbox 탭 toggle +
NoteCard mode prop, 게이트 + closure marker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:16:26 +09:00
altair823
61e277f36c docs(spec): #4 휴지통 (soft delete + migration v3) 설계
v0.2.3 두 번째 항목의 mini-brainstorm 결과 lock.

UI=A (Inbox 탭 toggle), 필터=A (명시적 WHERE deleted_at IS NULL),
AiWorker race=C (pending_jobs cleanup + processJob 가드),
액션=B (per-card 영구 삭제 추가 — IPC 4채널 → 5채널, telemetry 3 → 4 events),
confirm/정렬/카드차이 모두 A.

self-review 후 ExportService/ImportService 충돌 정책 ambiguity 명시화.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:04:47 +09:00