Commit Graph

92 Commits

Author SHA1 Message Date
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
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
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
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
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
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
06cfa1c151 chore(release): v0.2.2 — F7 + Quick Capture 스크롤 fix
- F7 (이미 main 병합): AI-primary due_date flow, 다중 후보 추출
- fix(quickcapture): textarea min-height: 0 + .card overflow: hidden 으로 hint 노출 보장

CHANGELOG.md / package.json 0.2.1 → 0.2.2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:25:53 +09:00
altair823
723dccd61d feat(ai): AI-primary due_date flow — rule as prompt candidates only
Flow 반전 (F7-D 채택):
- 기존: rule.iso ?? ai.dueDate (rule 우선)
- 신규: ai.dueDate ?? null (AI 우선)

규칙은 parseAllCandidates 로 모든 매치를 추출 → prompt 에 후보
힌트로 주입. AI 가 종합 판단. AI 실패 시 due_date null (별 fallback 없음).

해결되는 케이스: '내일 모레' → AI 가 ambiguous 인지 → null.

PROMPT_VERSION → 3. GenerateInput.dueDateCandidates 신규.
buildPrompt(rawText, todayKst, candidates) — 빈 배열일 때 hint 섹션 생략.

Tests:
- AiWorker.test.ts — 'rule priority' 테스트 → 'AI dueDate wins' flip
- AiWorker.test.ts — passes todayKst 테스트 확장 (dueDateCandidates 도 검증)
- AiWorker.test.ts — 신규 'passes parseAllCandidates result as dueDateCandidates'
- LocalOllamaProvider.test.ts / ollama-golden.test.ts — generate 호출에 dueDateCandidates: [] 추가

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:06:12 +09:00
altair823
1c72b64c2f feat(due-date): parseAllCandidates — extract all matches (text order)
기존 parseDueDate (first-match-wins) 는 backward compat 로 보존.
parseAllCandidates 가 모든 high/medium 매치를 text 순서로 반환 — F7
AI-primary flow 의 prompt 후보 주입 입력으로 사용.

Overlapping-span suppression: 다음 주 월요일 같은 케이스에서 rule 10
(전체) 이 rule 13 (다음 주 alone) 을 포함하면 후자 매치 제거.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:04:17 +09:00
altair823
bcd1151a24 feat(cue): IdentityCounter + tray refresh — 오늘 N번 잡아뒀다
F4-C·F (cue 강화) — Inkling 의 두 표면에 정체성 신호를 추가.

F4-C 환경 앵커 (tray):
- nativeImage 색 변경은 미지원이므로 색 뱃지 대신 tooltip + 메뉴 첫
  비활성 라벨로 대체. tooltip 은 항상 `Inkling — 오늘 N`,
  메뉴 첫 항목은 N>0 일 때 `오늘 N번 잡아둠` (비활성).
- `tray.ts` 가 `refreshTray(todayCount)` 를 export 하여 main 이
  60s interval + AiWorker.onUpdate hook 에서 갱신을 트리거.
- N=0 일 때는 라벨을 띄우지 않아 메뉴가 자연스럽게 시작.

F4-F 정체성 고리 (Inbox 헤더):
- ContinuityBadge 옆에 새 IdentityCounter 컴포넌트.
- N>0 → `오늘 N번 잡아뒀다` (정체성 강화 카피).
- N=0 → `오늘은 처음 한 줄?` (priming 카피로 첫 캡처 유도).
- 갱신은 `loadInitial` / `refreshMeta` (focus + note:updated) 경로
  공유 — 별도 IPC subscription 없음.

Wiring:
- `NoteRepository.countToday()` 를 `inbox:todayCount` IPC 로 노출.
- preload bridge `getTodayCount`, `InboxApi.getTodayCount()` 타입.
- 스토어에 `todayCount: number` 필드 추가, 두 메타 fetch 경로 모두에서 갱신.

스키마 변경 없음. 197/197 unit pass, 1/1 e2e pass.
2026-04-26 11:49:09 +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
66725dacae feat(sync): wire SyncService — tray '지금 동기화' + on-quit drain
- src/main/index.ts: SyncService instantiate with paths.profileDir + exportSvc
- 트레이 6번째 콜백 — 토스트로 not_configured / done / unchanged / failed 안내
- before-quit 훅에 sync drain 추가 (backup 완료 후, syncSvc.isConfigured() 인 경우만)
- src/main/tray.ts: 6번째 callback runSync, '지금 동기화' 메뉴 (내보내기/복원 다음)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:40:50 +09:00
altair823
eaf66e6c10 feat(sync): SyncService — F5 export + git add/commit/push to <profileDir>/sync/
F6-L2 MVP 의 오케스트레이터.

- isConfigured(): syncDir 가 git repo + origin remote 있을 때만 true
- sync():
    1) ExportService.export(<syncDir>, includeMedia: true) — F5 트리 그대로 덮어쓰기
    2) git add -A
    3) git commit -m "chore(notes): sync <ts>"
    4) "nothing to commit" 이면 changed=false 로 정상 반환
    5) git push (upstream 미설정이면 -u origin <branch> 자동)
- GitClient.push() 에 hasUpstream() + 자동 -u 추가 (첫 push 케이스)
- 5 vitest cases — bare local remote 로 push 검증, 두 번째 sync 는 변경 없음 확인

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:39:42 +09:00
altair823
32c7becd47 feat(sync): GitClient — async wrapper for git CLI
얇은 git CLI 래퍼. F6-L2 sync MVP 의 빌딩 블록.

- run/isRepo/hasRemote/addAll/commit/push/currentBranch
- commit() 은 "nothing to commit" 을 changed=false 로 구분 (정상 path)
- 그 외 실패는 throw, exitCode + stderr 보존
- 8 vitest cases — empty file 로 GIT_CONFIG_GLOBAL/SYSTEM 격리

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:37:43 +09:00
altair823
2d90a48621 feat(copy): replace '기억 구출' framing with 표면별 자연 동사 + Zeigarnik priming
F3: '구출' (rescue) is unnatural everyday Korean. Replace per-surface:
  - 트레이 '구출한 메모 보기' → '보관한 메모 보기'
  - 트레이 '기억 구출하기' → '한 줄 적기'
  - 토스트 #2 → '머릿속에서 꺼내 두었습니다.'
  - 토스트 #3 → '방금 한 줄 잡아뒀습니다.'
  - QC 힌트 'Ctrl+Enter 구출' → 'Ctrl+Enter 저장'
  - package.json description → 'local-first 한 줄 보관 도구'

F4-E (Zeigarnik priming): empty state copy reframed to evoke the
"unfinished thought tugging at memory" → "외재화로 해소" loop:
  - '첫 기억을 구출해보세요.' → '머릿속에 떠다니는 한 줄을 적어보세요.'

E2E smoke assertion updated to match. Slice §1.1 invariant 5
('실패/끊김/연속 실패' 금지) preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:29:31 +09:00
altair823
8373f06045 feat(inbox): tag chip click = filter, separate × button + undo toast
Splits the tag chip into two actions per F2 dogfood feedback:
- short click on chip text → applies the tag to the inbox filter
  (Inbox header shows "🔎 필터: #tag (n개)" banner with ✕ 해제 button)
- × button on chip → immediately removes the tag and surfaces a
  module-level pub/sub undo toast for 5 seconds; clicking the toast
  restores the tag

`TagUndoToast` is a tiny self-contained component: `pushTagUndo()` from
NoteCard publishes an entry, the mounted `<TagUndoToast />` near the
end of `<App>` subscribes and renders it. Auto-dismiss after 5 s,
click-to-undo cancels the timer and runs the restore callback.

AI vs user tags share the same behaviour — only the chip background
colour distinguishes them, matching the F2 decision table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:25:24 +09:00
altair823
aad9d403ce feat(inbox): tagFilter store + pure selectFilteredNotes + tests
Adds `tagFilter: string | null` and `setTagFilter` to the inbox store, plus
an extracted pure `selectFilteredNotes` selector so unit tests can import it
under vitest's `node` environment without dragging `api.ts` (which touches
`window.inkling` at module load).

Tests cover four cases: null filter passes through, single-tag match,
no-match empty result, and any-tag-matches semantics.

F2 dogfood feedback step 1/3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:25:13 +09:00
altair823
9ab70868d1 feat(due-date): NoteCard badge + edit + IPC
Inline 📅 YYYY-MM-DD badge appears in NoteCard between summary and
tags. Click to edit (HTML date input). Past dates: gray + line-through.
AI label shown when not user-edited (mirrors title/summary AI badge
policy). Empty state shows '📅 마감일 추가' link in gray.

New IPC inbox:setDueDate routes to NoteRepository.setDueDate which
sets due_date_edited_by_user=1 (per slice §1.1 invariant 2 — user
edit blocks future AI overwrite). Preload bridge + InboxApi type
extended.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:17:57 +09:00
altair823
adae90eb61 feat(ai): AiWorker merges rule parser + AI due_date
GenerateInput gains todayKst field. AiWorker computes KST-aligned
date once per job, runs parseDueDate on rawText, calls provider.generate
with todayKst, then merges: rule.iso wins if matched (deterministic),
else AI's due_date, else null. Logs dueDateSource (rule|ai|none) for
debugging. now() injection for testability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:14:46 +09:00
altair823
4ee135dcd6 feat(ai): zod due_date field + prompt {{TODAY_KST}} injection
AiResponse extends with dueDate: string|null. zod regex
^\d{4}-\d{2}-\d{2}$, follow-up roundtrip check coerces invalid
dates (2026-13-99 etc.) to null. PROMPT_VERSION → 2: prompt now
takes todayKst arg, asks model to extract due_date as ISO or null.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:12:45 +09:00
altair823
95ba1653d7 feat(due-date): pure rule parser for Korean date expressions
Regex + KST math, returns ISO YYYY-MM-DD or null. 14 high-confidence
rules (literal date, N월 N일, MM/DD, N일/주/개월 뒤, 모레/내일/글피/오늘,
다음/이번 주 X요일, 다음 달). Ambiguous tokens (월말, 주말, 퇴근 전,
시각) return iso=null + confidence='medium' so caller (AiWorker)
can defer to AI. 26+ golden fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:09:51 +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
e728a11e09 feat(import): wire ImportService — tray '백업에서 복원...' + preview dialog
Tray gets 5th callback. Directory chooser → preview (count of
new/skip/forked + media) → confirm message box → run. Slice §1.1
copy policy preserved (no '실패'/'끊김'). Notification on
success/failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 10:56:29 +09:00
altair823
d76cca68df feat(import): ImportService with conflict policy + media copy
Three-state outcome per note: inserted (new id), skipped (id+rawText
match), forked (id match but rawText differs → new uuidv7 to preserve
raw_text invariant from slice §1.1). Media files copied into
MediaStore convention <profileDir>/media/{noteId}/{n}.{ext} with
new media DB rows.

NoteRepository.importNote handles full provenance: ai_status='done',
ai_provider, ai_generated_at, edited flags, intent fields, tags
with source preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 10:55:13 +09:00
altair823
e8587c1986 feat(import): pure parser for F5 export format
parseExportNote reverses composeMarkdown — minimal YAML parser
covering only the variants F5 emits (plain, single-quoted, block
scalar, tag/image lists). Body extraction strips h1 + blockquote +
image refs to recover rawText. Round-trip tested against
exportFormat.composeMarkdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 10:53:29 +09:00
altair823
27666178a2 feat(export): wire ExportService — tray '내보내기...' menu + dialog
Tray now has 4th callback that opens directory chooser, exports all
notes via ExportService with includeMedia=true default. Dialog
message warns about raw_text plain-text + recommends private location.
Native toast on success/failure with note + media counts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 10:44:38 +09:00
altair823
9fdfd6610c feat(export): ExportService writing frontmatter tree + media + manifest
ExportService composes pure exportFormat layer + reads from
NoteRepository.listAll (new, asc-ordered) + MediaStore.absolutePath
(new helper). Writes notes/{date-id8-slug.md}, media/{id8__n.ext},
index.jsonl, manifest.json, README.md to user-picked dir.
6 unit tests against tmp dirs + :memory: DB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 10:42:43 +09:00