Commit Graph

22 Commits

Author SHA1 Message Date
altair823
6070562358 feat(v029): NoteRepository.requeueDisabled + countByAiStatus + AiProviderSection 처리 버튼 2026-05-09 16:35:53 +09:00
altair823
606ac94976 feat(v029): useInbox view enum + counts + setView + listByStatus/countsByStatus IPC
- store.ts: view enum ('inbox'|'completed'|'archived'|'trash'|'settings') + counts +
  setView + loadByView. setShowSettings delegates to setView (mirror).
- types.ts + preload + ipc/inboxApi: listByStatus + countsByStatus IPC.
- NoteRepository.countByStatus 신규.
- store.view.test (5) + NoteRepository.countByStatus test (1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:51:51 +09:00
altair823
facbf54025 feat(v029): NoteRepository.setStatus + listByStatus + restoreNote 재구현
- NoteStatus 타입 추가 ('active'/'completed'/'archived'/'trashed')
- Note interface 에 status / statusChangedAt / moveReason 필드 추가
- setStatus(id, status, reason, now?) — 단일 transaction 으로 status + move_reason +
  status_changed_at + updated_at 갱신. status='trashed' ↔ deleted_at 동기화
  (backward compat). 그 외 status 는 deleted_at NULL.
- listByStatus(status, opts) — status 별 필터 + ORDER BY COALESCE(status_changed_at,
  created_at) DESC. limit cap 200.
- hydrate 에 status / statusChangedAt / moveReason 매핑 추가. 미설정 row 는 'active' fallback.
- restoreNote 재구현 — setStatus('active', null) 로 status + deleted_at 동기화 +
  v0.2.6 #10 round 1 fix (ai_status='failed'/'pending' → pending_jobs 재투입) 보존.
- 기존 테스트 fixture 5건에 새 필드 추가 (NoteCard, store.expired/recall/tagFilter/trash).
- 신규 테스트 11건 (setStatus + listByStatus + restoreNote 회귀).
2026-05-09 15:33:49 +09:00
altair823
e2c53a28dc fix(v026): #12 trashCount cap → countTrashed() 정확 N (silent undercount 해소)
기존 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>
2026-05-05 01:18:10 +09:00
altair823
df27a9637e fix(v026): #10 restoreNote 가 failed 노트 시 pending_jobs 재생성
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>
2026-05-05 01:15:23 +09:00
altair823
0eb2e6282f feat(recall): NoteRepository — findRecallCandidate + markRecallOpened + dismissRecall (#6 v0.2.3)
- findRecallCandidate(): 7일+ 안 본 + 30일+ dismiss 만료 + ai='done' + 마감 안 임박 + LIMIT 1
- markRecallOpened(id, now): last_recalled_at 갱신
- dismissRecall(id, now): recall_dismissed_at 갱신
- KST 보정 SQL date('now','+9 hours')
- 단위 +5 cases (empty/recent/old/dismiss expiry/exclude variants)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:11:14 +09:00
altair823
d8621d55e0 fix(tag-vocab): PR review round 1 — i1 dedup + m2 test gap (#3 v0.2.3)
- 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>
2026-05-02 12:49:36 +09:00
altair823
e2b16d44d7 fix(tag-vocab): T1 review minor/nit 4건 일괄 (#3 v0.2.3)
- M1: getTopUsedTags 의 LIMIT-then-filter 의미 docstring 명시
- M2: AI+user source 통합 테스트 강화 — 카운트 차이로 정렬 검증 (toContain 만으론 약함)
  updateUserAiFields 는 tags REPLACE 방식 (DELETE+reinsert) 이므로
  fallback 패턴 사용: 3개 노트 각 1태그, AI/user 혼합으로 design=2 > meeting=1 검증
- N1: SQL "COUNT(*) c" → "COUNT(*) AS c" (countFailed 패턴과 일관)
- N2: kebab-case regex 모듈 상수 KEBAB_CASE_RE 로 hoist

skip: N3 (test 헬퍼 — verbosity 경미), N4 (it 블록 분리 — 코드베이스 패턴 유지)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:14:53 +09:00
altair823
df8a53aec1 feat(tag-vocab): NoteRepository — getTopUsedTags + getTagIdByName (#3 v0.2.3)
- getTopUsedTags(limit=20): top-N (count desc, id asc) + kebab-case 필터 + deleted_at 제외
- getTagIdByName(name): COLLATE NOCASE lookup
- AI+user source 통합 카운트 (Q1=C 결정)
- 단위 +7 cases (정렬, 필터, source 통합, deleted 제외, limit, getTagIdByName)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:10:36 +09:00
altair823
8f56814186 fix(retry): review round 1 — minor/nit 4건 일괄 (#2 v0.2.3)
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>
2026-05-02 03:47:08 +09:00
altair823
2e3f0edffd feat(retry): NoteRepository — findFailedIds/countFailed/retryAllFailed/setNextRunAt (#2 v0.2.3) 2026-05-02 03:15:05 +09:00
altair823
fec80361dd feat(expiry): NoteRepository.trashBatch atomic (#5 v0.2.3) 2026-05-02 00:01:03 +09:00
altair823
00423fb235 feat(expiry): NoteRepository.findExpiredCandidates (#5 v0.2.3) 2026-05-01 23:57:53 +09: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
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
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
cca3029b7e feat(repo): countToday(now?) — KST midnight bucket count
For the F4-C·F cue strengthening surfaces (tray tooltip + Inbox identity
counter), main + renderer need a single source of truth for "오늘 N번
잡아뒀다". Implements `NoteRepository.countToday(now?)` that computes the
half-open UTC interval covering the KST calendar date of `now` and counts
rows whose `created_at` falls inside.

`now` is injectable for deterministic tests across the KST/UTC boundary
(02:00 KST and 23:00 KST land on different UTC dates yet the same / a
different KST day). Four new cases cover empty DB, KST-day filtering,
KST-midnight crossover, and the default-arg branch.
2026-04-26 11:47:03 +09:00
altair823
0bb6c12bbb feat(db): migration v2 — due_date columns + pre-migration snapshot
ALTER TABLE notes adds due_date TEXT + due_date_edited_by_user INTEGER.
openDb takes <dbFile>.pre-v<N>.bak before running migrations
(F6-L1 follow-up #4 — preserves recoverable state if migration fails).
NoteRepository: updateAiResult accepts dueDate?, setDueDate +
edited-flag CASE WHEN guard mirroring title/summary pattern.
Note interface gains dueDate + dueDateEditedByUser fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:05:44 +09:00
altair823
797d97c392 feat(repo): NoteRepository with intent, edited flags, AI overwrite guard
Task 7 of the slice plan. Implements the full repository surface
backing every IPC inbox/capture path: create (UUID v7 + atomic
notes + pending_jobs insert), insertMedia, findById/list,
updateAiResult (CASE WHEN guard against title/summary
overwrite when *_edited_by_user flips), markAiFailed (truncates
ai_error to 500 chars + clears pending job), updateUserAiFields
(sets edited flags as a side effect, replaces user-source tags),
setIntent + dismissIntent (intent_prompted_at uses COALESCE so
the first stamp wins), delete, getPendingCount,
getAllPendingJobs, incrementJobAttempt, and a private hydrate
that joins notes with note_tags + media.

Plan deviation: list/list-with-cursor query gets a secondary
"id DESC" tiebreaker. Two notes created in the same millisecond
shared created_at and reordered nondeterministically; UUID v7
sorts monotonically with creation order, so id DESC restores
"newest first" within ties.

Verification: `npx vitest run tests/unit/NoteRepository.test.ts`
12 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:06:45 +09:00