456 Commits

Author SHA1 Message Date
altair823
00423fb235 feat(expiry): NoteRepository.findExpiredCandidates (#5 v0.2.3) 2026-05-01 23:57:53 +09:00
altair823
0a9dab4a7f feat(expiry): KST util — todayInKstString + nextKstMidnightMs (#5 v0.2.3) 2026-05-01 23:53:20 +09:00
altair823
a5e6859ac9 docs(plan): v0.2.3 #5 만료 추천 구현 계획
8 task TDD 분할 + 단위 26개 (spec §8 의 16개 충족 + 6 over):
- T1 KST util (todayInKstString + nextKstMidnightMs)
- T2 NoteRepository.findExpiredCandidates
- T3 NoteRepository.trashBatch (atomic)
- T4 telemetry 2 events + stats.md 만료 trash ratio
- T5 CaptureService listExpired/trashExpiredBatch + IPC 2채널 + preload
- T6 zustand store 확장
- T7 ExpiryBanner 컴포넌트 + App.tsx mount
- T8 closure

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:30:48 +09:00
altair823
c45e613b31 docs(spec): #5 expiry — move dedup to main, keep IPC at 2 channels
§6.2 의 expired_banner_shown signature dedup 위치를 zustand store(renderer)
→ CaptureService(main) 로 변경. 결과: 신규 IPC 채널 1개 추가 회피.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:25:12 +09:00
altair823
4c2769fd82 docs(spec): v0.2.3 #5 만료 추천 design
mini-brainstorm 결과 5개 결정 박힘:
- Q1=B due_date_edited_by_user 필터 없음 (AI + 수동 모두)
- Q2=A 만료만 (D-7 임박 v0.2.4)
- Q3=C unchecked default + 전체선택 토글 (데이터 안전)
- Q4=B PendingBanner 아래 (system → progress → actionable)
- Q5=A 후보 0건 / snooze 시 collapse (PendingBanner 패턴)

T1-T10 작업 순서 + 단위 ≥ 16개 + IPC 2채널 + telemetry 2이벤트.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:22:38 +09:00
df60c5a5b2 Merge pull request 'feat(trash): #4 휴지통 + migration v3 (v0.2.3 2/7)' (#14) from feat/v023-trash into main
Reviewed-on: #14
2026-05-01 14:06:21 +00:00
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
6f8ae75ff7 Merge pull request 'feat(telemetry): #7 telemetry skeleton (v0.2.3 1/7)' (#13) from feat/v023-telemetry into main
Reviewed-on: #13
2026-05-01 10:37:55 +00:00
altair823
7e8e2b598d fix(telemetry): 회차 1 review 반영 — attempts 의미 통일 + DI 우회 제거 + 매직 슬라이스 제거
PR #13 회차 1 리뷰의 actionable 1건 + suggestion 3건 반영.

- `AiWorker` 의 `attempts` 필드가 success/failure 경로에서 비대칭 의미 (0-index vs count) 였던 문제. 둘 다 `attempt + 1` (실제 시도 횟수, 1-based) 로 통일. stats markdown 의 평균/분포 해석이 일관됨.
- `Date.now()` 직접 호출이 `opts.now` DI 를 우회하던 두 곳을 `this.now().getTime()` 으로 교체. 추후 durationMs 분포 테스트 작성 가능.
- `TelemetryService.emit` 의 `this.now()` 두 번 호출을 한 번 캐시로 통합. KST 자정 경계에서 ts 와 파일명 일자 불일치 가능성 제거.
- `readAllRecent` 의 `n.slice(7, 17)` 매직 슬라이스를 정규식 capture 그룹으로 교체. prefix 변경 시 한 곳만 수정.

테스트: AiWorker 성공 케이스의 `attempts: 0` → `attempts: 1` 갱신.
게이트: typecheck 0 errors, 245/245 unit tests pass.

Deferred (v0.2.4 backlog): 'aborted' user-cancel false-positive, tray menu submenu 분리.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:41:26 +09:00
altair823
5c97397cbe chore(telemetry): #7 closure — gate verification + .catch consistency + spec fix
- Add .catch(...) to telemetry.cleanupOldFiles fire-and-forget for consistency
  with backup.runDaily pattern (M1 from T10 code review).
- Mark Roadmap §3 #7 as completed (✓).
- Correct spec: tray:exportTelemetry was never an IPC channel — tray callbacks
  run in main process directly. Replace with "트레이 콜백 (main 내부)".

Closes v0.2.3 task 1 of 7. Next task: #4 휴지통 (migration v3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:37:00 +09:00
altair823
fe24ff577f feat(telemetry): wire TelemetryService + tray export (#7 v0.2.3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 17:30:54 +09:00
altair823
dca6aed44e docs(tray): restore F4-C identity-signal intent comment
The T9 full-file replacement accidentally dropped the inline comment
documenting why the count label is conditional on _todayCount > 0
(F4-C UX rationale). No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:28:04 +09:00
altair823
4213745dc7 feat(telemetry): tray menu '사용 로그 내보내기...' (#7 v0.2.3) 2026-05-01 17:25:52 +09:00
altair823
01447ddaad feat(telemetry): AiWorker emits ai_succeeded/ai_failed with reason (#7 v0.2.3) 2026-05-01 17:21:08 +09:00
altair823
f0cef95d3f feat(telemetry): CaptureService emits capture event (#7 v0.2.3) 2026-05-01 17:15:24 +09:00
altair823
36a5c67ed6 feat(telemetry): exportTo writes events.jsonl + stats.md (#7 v0.2.3) 2026-05-01 17:08:34 +09:00
altair823
2036c687d2 test(telemetry): add KST regression test for near-midnight UTC bucketing
Original 'counts events per KST day' test used UTC times that bucket
identically under both KST and naive UTC slice — would not catch a regression
where kstDate was replaced with ev.ts.slice(0,10). Add an explicit
near-midnight case (2026-05-01T15:30Z = 2026-05-02 00:30 KST) that fails
under naive UTC and passes under correct KST conversion.

6 tests pass (was 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:06:29 +09:00
altair823
9a066ed807 feat(telemetry): telemetryStats.aggregateStats (#7 v0.2.3) 2026-05-01 17:03:31 +09:00
altair823
729a3f9c47 feat(telemetry): readAllRecent with malformed-line tolerance (#7 v0.2.3) 2026-05-01 16:58:45 +09:00
altair823
0501bd1762 feat(telemetry): cleanupOldFiles with 14-day KST retention (#7 v0.2.3) 2026-05-01 16:54:36 +09:00
altair823
50b6d05bcb fix(telemetry): silent-fs-error test exercises the actual code path
Earlier test used '/proc/0/...' as the unwritable dir. On Windows this
resolved to 'C:\proc\0\...' and mkdir({recursive: true}) silently created
it — the silent code path was never exercised, plus filesystem side-effect
leaked outside the test tmpdir.

Replace with a path that points to an existing file (mkdir on a file path
fails on every platform). Also add a companion test that confirms silent
is opt-in: without {silent: true}, the same fs failure DOES throw.

7 tests pass (was 6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:52:11 +09:00
altair823
93e278b241 feat(telemetry): TelemetryService.emit with KST rotation (#7 v0.2.3) 2026-05-01 14:18:59 +09:00
altair823
0a0ef11327 feat(telemetry): event schema + privacy invariant (#7 v0.2.3) 2026-05-01 14:14:19 +09:00
altair823
358cada017 docs(plan): #7 telemetry skeleton 구현 계획 (v0.2.3 1/7)
11 task TDD plan — events schema/privacy invariant, JSONL emit/rotation,
14d cleanup, readAllRecent, stats aggregator, exportTo(folder),
CaptureService/AiWorker hooks, tray menu, index.ts wiring, gates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:02:48 +09:00
altair823
22a25cc622 docs(spec): v0.2.3 dogfood feedback roadmap (7 items, single cut)
v0.2.2 dogfood 7항목 (#7 telemetry 신설 + #1~#6) 단일 cut 로드맵.
데이터 안전 우선 (C 채택), schema migration v3 3컬럼 한 묶음 (B),
trash↔backup/export B 정책, #6 = 1 spike 흡수.

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