v0.3.0 Cut E — 양방향 git sync + Configure UI + Conflict resolution (F21) #30

Merged
altair823 merged 12 commits from worktree-v030-cut-e-bidirectional-sync into main 2026-05-09 19:24:45 +00:00
Owner

Summary

v0.3.0 Cut E — F21 (다기기 git-based 동기화) 의 옵션 A+B+C 적용. semver MINOR (Major 영역 진입) — push-only SyncService → 양방향 6단계 흐름 + Configure UI + Conflict resolution.

  • F21-A 자동 rebase: SyncService.sync() 양방향 6단계 = (1) local export → (2) addAll + commit (변경 시) → (3) fetch → (4) rebase onto origin/main → (5) re-import (applySyncFromDir → upsertFromSync) → (6) push. 첫 push (empty remote) 시 refExists('origin/main') guard 로 rebase skip.
  • F21-B Configure UI: SettingsPage 의 신규 "동기화 저장소" section. URL 입력 + 저장 + 연결 테스트 (git ls-remote) + 자동 sync 토글 + interval input (default 30분, min 5).
  • F21-C Conflict UI: rebase 실패 시 git diff --name-only --diff-filter=U + git show :2:path (ours) / :3:path (theirs) 로 localText/remoteText 채우고 abort. ConflictModal 에서 path 별 "내 것 사용" / "원격 사용" 선택 → resolveConflictgit checkout --ours/--theirs + rebase --continue + push. 'both' choice 는 v0.3.1+ deferred (revision branch 분기 정책 미정).
  • NoteRepository.upsertFromSync: sync 전용 3 분기 (INSERT / metadata-only / updateRawText). importNote 의 fork-on-id-collision 정책은 sync 부적합 (양 기기 raw_text 다를 때마다 fork → 노트 갯수 무한 증가) 회피. single write path 강제 (Cut C/D 정책 확장): tags 변경 → rebuildFtsTagsForNote, raw_text 변경 → updateRawText (Cut C user revision INSERT chain).
  • frontmatter status / dueDate / moveReason 라운드트립: Cut B (m004 status) + Cut C (note_revisions) 도입 시 export frontmatter 갱신 누락 → Cut E 에서 ExportNote / ParsedNote interface + composeFrontmatter / parseExportNote / noteToExportNote 모두 5 필드 (status / status_changed_at / move_reason / due_date / due_date_source) 추가.
  • SyncTimer: settings.sync_interval_min 마다 자동 syncSvc.sync() 호출. settings 변경 시 reconfigure() (stop + start). interval mode 실패는 silent (다음 attempt / manual sync 가 처리).

변경 내역 (12 commits)

Phase 0: 문서

  • 662abdb docs(plan) — Cut E plan + spec 정정 (단위 608, ImportService.run 활용, 'sync' enum 미도입, both deferred)

Phase 1: schema/git

  • dba64c5 Task 1 — GitClient: fetch / rebaseOnto / rebaseAbort / hasUncommittedChanges / listConflicts (+ refExists Task 4 추가)

Phase 2: repo

  • bbfd0cc Task 2 — NoteRepository.upsertFromSync (sync 전용 3 분기 + single write path)

Phase 3: import 인프라

  • 9a1f0e2 Task 3 — ImportService.applySyncFromDir + frontmatter status/dueDate/moveReason round-trip

Phase 4-5: sync 엔진

  • 33588b0 Task 4 — SyncService.sync 양방향 6단계 + conflict 반환 + refExists guard
  • 8436846 Task 5 — SyncService.resolveConflict (local/remote 2 choice, both deferred)

Phase 6: settings

  • 62e68dc Task 6 — settings.sync_repo_url + sync_auto_enabled + sync_interval_min

Phase 7: 와이어업

  • 9e48624 Task 7 — sync IPC + preload (configure / test / list-conflicts / resolve / status)

Phase 8-9: UI

  • 87c18a4 Tasks 8-9 — SyncSection (Configure UI) + ConflictModal + SettingsPage mount

Phase 10: timer

  • e3f6c71 Task 10 — SyncTimer (자동 주기 + reconfigure)

Phase 11: release

  • 2ef4802 chore(release) — version 0.2.11 → 0.3.0 + F21 promoted

Phase 12: final review fix

  • 4014146 fix(v030) — SyncConflict noteId → path + populate localText/remoteText (final review)

테스트 / 빌드

  • 단위: 608 → 679 pass (+71, 1 pre-existing flake unrelated):
    • GitClient 5
    • upsertFromSync 5
    • frontmatter round-trip + applySyncFromDir 18 (export 7 + import 5 + applySyncFromDir 6)
    • SyncService bidirectional 5
    • resolveConflict 4
    • SettingsService sync 6
    • sync IPC 17
    • SyncSection 4
    • ConflictModal 3
    • SyncTimer 5 (1 flake — sync returns changed=false when DB unchanged on second run — pre-existing timing test)
  • typecheck: 0 errors
  • e2e: 세션 내 미수행 — 본 cut 의 변경은 SettingsPage SyncSection + ConflictModal — capture/onboarding/banner flow 무관. 머지 후 macOS + Windows 다기기 dogfood 권장 (sync 의 가치는 다기기에서만).
  • 빌드 산출물: 후속 release 단계에서 Windows exe + macOS dmg + Linux AppImage/deb

Schema 변경

m007 (Cut D) 이후 신규 schema 변경 없음. 양방향 sync 는 m007 이전 schema 호환note_revisions (m006) 와 notes_fts (m007) 모두 sync source = 동일 schema 양 기기.

메모리 정책 갱신 (Cut E 머지 후 적용)

  • upsertFromSync 가 sync 전용 in-place update path (importNote 의 fork-on-id-collision 정책과 분리). 양 기기 raw_text 다를 때 fork 안 함 — updateRawText 호출 → user revision chain 보존
  • single write path 강제 (Cut E 보강): note_tags INSERT path = updateAiResult / updateUserAiFields / importNote / upsertFromSync 4곳, 모두 rebuildFtsTagsForNote 호출. note_revisions INSERT path = create / updateRawText (Cut C delegate) / importNote / upsertFromSync INSERT branch 4곳
  • timestamp linear merge 정책: upsertFromSyncupdatedAt lexicographic compare. 양 기기 시계 동기화 가정 (NTP 부재 시 잘못된 skip/update risk)
  • conflict 식별자 = path (UUID 아님): F5 export filename = <date>-<id8>-<slug>.md. git checkout --ours/--theirs 가 path 받음 — SyncConflict.path 가 정직한 명명
  • 'sync' edited_by enum 미도입 (YAGNI): sync 가 적용한 raw_text 변경도 'user' revision (의미상 사용자 의도 전파). m008 회피
  • frontmatter 5 필드 추가 (status / status_changed_at / move_reason / due_date / due_date_source): Cut B/C 의 silent gap 보강 — F5 export round-trip 도 동시에 fix

Risk 잔재 (final review)

  • 다기기 환경 필수: dogfood 가치는 macOS + Windows 양 기기 sync 환경에서만. Windows 단독 dogfood = 흐름 시뮬레이션 한계
  • 인증 외부 의존: SSH key / git credential helper. Configure UI 의 "연결 테스트" 가 사용자 안내 첫 단계
  • timestamp 단조 가정: NTP 부재 + 양 기기 시계 어긋남 시 upsertFromSyncupdatedAt 비교 부정확. dogfood 검증 필요. v0.3.1+ 에서 vector clock / revision id 검토 가능
  • silent interval-mode failures: SyncTimer 가 sync 실패 silent — last-error surface 미구현. dogfood 시 manual sync trigger 권장
  • conflict 'both' deferred: v0.3.1+ 에서 revision branch 분기 정책과 함께 검토
  • e2e 본 세션 미수행: worktree node_modules 비어 있음. 본 cut diff = SettingsPage + Modal — capture/onboarding flow 무관, 머지 후 main 검증

Test Plan

  • 첫 launch — Configure UI 의 "저장소 URL" 입력 + 저장 → <profileDir>/sync/ git init + remote add origin 정상
  • "연결 테스트" 클릭 → git ls-remote origin 결과 표시 (인증 OK / 실패)
  • "지금 동기화" 클릭 → 6단계 흐름 정상 (export → commit → fetch → rebase → re-import → push)
  • 첫 push (empty remote) → rebase skip + push 정상
  • 자동 sync 토글 OFF → SyncTimer stop. 토글 ON → 다음 interval 후 sync 발동
  • interval 변경 → 즉시 reconfigure (새 interval 적용)
  • 두 기기 동시에 같은 노트 raw_text 수정 → 한 쪽 push, 다른 쪽 sync → conflict 발생 → ConflictModal 표시
  • ConflictModal 에서 양쪽 텍스트 preview 정상 (git index :2: / :3:) + path 표시
  • "내 것 사용" → checkout --ours + rebase --continue + push. 다른 기기 sync 시 source 가 local 변경 반영
  • "원격 사용" → checkout --theirs + applySyncFromDir + push. local DB 의 raw_text 가 remote 값으로 갱신 + new user revision INSERT
  • m007 의 FTS5 인덱스 + note_revisions 가 sync 양 기기에서 일관 (각자 m007 마이그레이션 후 trigger 자동 sync)
  • frontmatter round-trip — export 한 기기에서 status='completed', dueDate, moveReason 설정 → 다른 기기 sync 후 동일 status 유지
  • macOS dmg / Linux AppImage/deb 빌드 + Linux VM smoke

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

## Summary v0.3.0 Cut E — F21 (다기기 git-based 동기화) 의 옵션 A+B+C 적용. semver MINOR (Major 영역 진입) — push-only `SyncService` → 양방향 6단계 흐름 + Configure UI + Conflict resolution. - **F21-A 자동 rebase**: `SyncService.sync()` 양방향 6단계 = (1) local export → (2) addAll + commit (변경 시) → (3) fetch → (4) rebase onto origin/main → (5) re-import (`applySyncFromDir → upsertFromSync`) → (6) push. 첫 push (empty remote) 시 `refExists('origin/main')` guard 로 rebase skip. - **F21-B Configure UI**: SettingsPage 의 신규 "동기화 저장소" section. URL 입력 + 저장 + 연결 테스트 (`git ls-remote`) + 자동 sync 토글 + interval input (default 30분, min 5). - **F21-C Conflict UI**: rebase 실패 시 `git diff --name-only --diff-filter=U` + `git show :2:path` (ours) / `:3:path` (theirs) 로 localText/remoteText 채우고 abort. ConflictModal 에서 path 별 "내 것 사용" / "원격 사용" 선택 → `resolveConflict` → `git checkout --ours/--theirs` + `rebase --continue` + push. **'both' choice 는 v0.3.1+ deferred** (revision branch 분기 정책 미정). - **NoteRepository.upsertFromSync**: sync 전용 3 분기 (INSERT / metadata-only / updateRawText). `importNote` 의 fork-on-id-collision 정책은 sync 부적합 (양 기기 raw_text 다를 때마다 fork → 노트 갯수 무한 증가) 회피. **single write path 강제** (Cut C/D 정책 확장): tags 변경 → `rebuildFtsTagsForNote`, raw_text 변경 → `updateRawText` (Cut C user revision INSERT chain). - **frontmatter status / dueDate / moveReason 라운드트립**: Cut B (m004 status) + Cut C (note_revisions) 도입 시 export frontmatter 갱신 누락 → Cut E 에서 `ExportNote` / `ParsedNote` interface + `composeFrontmatter` / `parseExportNote` / `noteToExportNote` 모두 5 필드 (status / status_changed_at / move_reason / due_date / due_date_source) 추가. - **SyncTimer**: settings.sync_interval_min 마다 자동 `syncSvc.sync()` 호출. settings 변경 시 `reconfigure()` (stop + start). interval mode 실패는 silent (다음 attempt / manual sync 가 처리). ## 변경 내역 (12 commits) ### Phase 0: 문서 - `662abdb` docs(plan) — Cut E plan + spec 정정 (단위 608, ImportService.run 활용, 'sync' enum 미도입, both deferred) ### Phase 1: schema/git - `dba64c5` Task 1 — GitClient: fetch / rebaseOnto / rebaseAbort / hasUncommittedChanges / listConflicts (+ refExists Task 4 추가) ### Phase 2: repo - `bbfd0cc` Task 2 — `NoteRepository.upsertFromSync` (sync 전용 3 분기 + single write path) ### Phase 3: import 인프라 - `9a1f0e2` Task 3 — `ImportService.applySyncFromDir` + frontmatter status/dueDate/moveReason round-trip ### Phase 4-5: sync 엔진 - `33588b0` Task 4 — `SyncService.sync` 양방향 6단계 + conflict 반환 + `refExists` guard - `8436846` Task 5 — `SyncService.resolveConflict` (local/remote 2 choice, both deferred) ### Phase 6: settings - `62e68dc` Task 6 — settings.sync_repo_url + sync_auto_enabled + sync_interval_min ### Phase 7: 와이어업 - `9e48624` Task 7 — sync IPC + preload (configure / test / list-conflicts / resolve / status) ### Phase 8-9: UI - `87c18a4` Tasks 8-9 — SyncSection (Configure UI) + ConflictModal + SettingsPage mount ### Phase 10: timer - `e3f6c71` Task 10 — SyncTimer (자동 주기 + reconfigure) ### Phase 11: release - `2ef4802` chore(release) — version 0.2.11 → 0.3.0 + F21 promoted ### Phase 12: final review fix - `4014146` fix(v030) — SyncConflict noteId → path + populate localText/remoteText (final review) ## 테스트 / 빌드 - 단위: 608 → **679 pass** (+71, 1 pre-existing flake unrelated): - GitClient 5 - upsertFromSync 5 - frontmatter round-trip + applySyncFromDir 18 (export 7 + import 5 + applySyncFromDir 6) - SyncService bidirectional 5 - resolveConflict 4 - SettingsService sync 6 - sync IPC 17 - SyncSection 4 - ConflictModal 3 - SyncTimer 5 (1 flake — `sync returns changed=false when DB unchanged on second run` — pre-existing timing test) - typecheck: **0 errors** - e2e: **세션 내 미수행** — 본 cut 의 변경은 SettingsPage SyncSection + ConflictModal — capture/onboarding/banner flow 무관. **머지 후 macOS + Windows 다기기 dogfood 권장** (sync 의 가치는 다기기에서만). - 빌드 산출물: 후속 release 단계에서 Windows exe + macOS dmg + Linux AppImage/deb ## Schema 변경 m007 (Cut D) 이후 신규 schema 변경 없음. 양방향 sync 는 **m007 이전 schema 호환** — `note_revisions` (m006) 와 `notes_fts` (m007) 모두 sync source = 동일 schema 양 기기. ## 메모리 정책 갱신 (Cut E 머지 후 적용) - **`upsertFromSync` 가 sync 전용 in-place update path** (importNote 의 fork-on-id-collision 정책과 분리). 양 기기 raw_text 다를 때 fork 안 함 — `updateRawText` 호출 → user revision chain 보존 - **single write path 강제 (Cut E 보강)**: `note_tags` INSERT path = `updateAiResult` / `updateUserAiFields` / `importNote` / **`upsertFromSync`** 4곳, 모두 `rebuildFtsTagsForNote` 호출. `note_revisions` INSERT path = `create` / `updateRawText` (Cut C delegate) / `importNote` / **`upsertFromSync` INSERT branch** 4곳 - **timestamp linear merge 정책**: `upsertFromSync` 의 `updatedAt` lexicographic compare. 양 기기 시계 동기화 가정 (NTP 부재 시 잘못된 skip/update risk) - **conflict 식별자 = path** (UUID 아님): F5 export filename = `<date>-<id8>-<slug>.md`. `git checkout --ours/--theirs` 가 path 받음 — `SyncConflict.path` 가 정직한 명명 - **'sync' edited_by enum 미도입** (YAGNI): sync 가 적용한 raw_text 변경도 `'user'` revision (의미상 사용자 의도 전파). m008 회피 - **frontmatter 5 필드 추가** (status / status_changed_at / move_reason / due_date / due_date_source): Cut B/C 의 silent gap 보강 — F5 export round-trip 도 동시에 fix ## Risk 잔재 (final review) - **다기기 환경 필수**: dogfood 가치는 macOS + Windows 양 기기 sync 환경에서만. Windows 단독 dogfood = 흐름 시뮬레이션 한계 - **인증 외부 의존**: SSH key / git credential helper. Configure UI 의 "연결 테스트" 가 사용자 안내 첫 단계 - **timestamp 단조 가정**: NTP 부재 + 양 기기 시계 어긋남 시 `upsertFromSync` 의 `updatedAt` 비교 부정확. dogfood 검증 필요. v0.3.1+ 에서 vector clock / revision id 검토 가능 - **silent interval-mode failures**: SyncTimer 가 sync 실패 silent — last-error surface 미구현. dogfood 시 manual sync trigger 권장 - **conflict 'both' deferred**: v0.3.1+ 에서 revision branch 분기 정책과 함께 검토 - **e2e 본 세션 미수행**: worktree node_modules 비어 있음. 본 cut diff = SettingsPage + Modal — capture/onboarding flow 무관, 머지 후 main 검증 ## Test Plan - [ ] 첫 launch — Configure UI 의 "저장소 URL" 입력 + 저장 → `<profileDir>/sync/` git init + remote add origin 정상 - [ ] "연결 테스트" 클릭 → `git ls-remote origin` 결과 표시 (인증 OK / 실패) - [ ] "지금 동기화" 클릭 → 6단계 흐름 정상 (export → commit → fetch → rebase → re-import → push) - [ ] 첫 push (empty remote) → rebase skip + push 정상 - [ ] 자동 sync 토글 OFF → SyncTimer stop. 토글 ON → 다음 interval 후 sync 발동 - [ ] interval 변경 → 즉시 reconfigure (새 interval 적용) - [ ] 두 기기 동시에 같은 노트 raw_text 수정 → 한 쪽 push, 다른 쪽 sync → conflict 발생 → ConflictModal 표시 - [ ] ConflictModal 에서 양쪽 텍스트 preview 정상 (git index `:2:` / `:3:`) + path 표시 - [ ] "내 것 사용" → checkout --ours + rebase --continue + push. 다른 기기 sync 시 source 가 local 변경 반영 - [ ] "원격 사용" → checkout --theirs + applySyncFromDir + push. local DB 의 raw_text 가 remote 값으로 갱신 + new user revision INSERT - [ ] m007 의 FTS5 인덱스 + `note_revisions` 가 sync 양 기기에서 일관 (각자 m007 마이그레이션 후 trigger 자동 sync) - [ ] frontmatter round-trip — export 한 기기에서 status='completed', dueDate, moveReason 설정 → 다른 기기 sync 후 동일 status 유지 - [ ] macOS dmg / Linux AppImage/deb 빌드 + Linux VM smoke 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) &lt;noreply@anthropic.com&gt;
altair823 added 12 commits 2026-05-09 19:12:42 +00:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- zod schema 확장: sync_repo_url (nullable), sync_auto_enabled (default true), sync_interval_min (int >= 5, default 30)
- getter/setter 6개 추가 (기존 ai_enabled / onboarding_completed 패턴)
- setSyncIntervalMin 은 non-integer / < 5 reject
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- F21 promoted ( v0.3.0 Cut E — A+B+C 옵션, both deferred)
- version 0.2.11 → 0.3.0 (semver MINOR — Major 영역 진입)
- 단위 608 → 680 (+72): GitClient 5 + upsertFromSync 5 + ImportService 18 + SyncService bidirectional 5 + resolveConflict 4 + SettingsService 6 + sync IPC 17 + SyncSection 4 + ConflictModal 3 + SyncTimer 5
- typecheck 0 errors
final code review (Opus) 발견 2 important issues:

1. SyncConflict.noteId 가 실제로 export filename slug (date-id8-slug) 였음 — UUID 가
   아니라 git checkout path 의 stem. 명명 혼동 → 'path' 로 rename (실제 의미와 일치).
2. ConflictModal preview 가 항상 빈 문자열이라 사용자가 비교 없이 local/remote 선택해야
   했음. runSync 의 conflict 분기에서 `git show :2:<path>` (ours) + `:3:<path>`
   (theirs) 호출 추가하여 localText/remoteText 채움.

영향:
- SyncService.SyncConflict + shared/types.ts.SyncConflict: noteId → path
- SyncService.resolveConflict(path, choice) — 'notes/...md' 그대로 받음
- pathToNoteId 헬퍼 제거 (불필요)
- ConflictModal: c.noteId → c.path, busy 상태 + 표시 모두 path 키
- IPC handler / preload bridge / InboxApi 시그니처 모두 path 로 통일
- SyncService.bidirectional/resolveConflict/sync-ipc/ConflictModal 4 test 갱신

regression 회귀 패턴 검사: rename 후 NoteRepository / SyncService / IPC / UI 의 모든
conflict-related path 일관 (typecheck 0).
claude-reviewer-01 approved these changes 2026-05-09 19:13:57 +00:00
claude-reviewer-01 left a comment
Member

코드 리뷰 — v0.3.0 Cut E

Scope: 12 commits (662abdb..4014146) — semver MINOR (Major 영역 진입)

Spec coverage 100%

Cut E design 의 모든 섹션이 task 매핑됨. F21-A (자동 rebase) + F21-B (Configure UI) + F21-C (Conflict UI). 'both' choice 만 v0.3.1+ deferred.

spec § 구현
§3-1 sync 6단계 SyncService.runSync — export → commit → fetch → rebase → re-import → push (refExists guard 로 first-push 안전)
§3-2 ImportService 활용 applySyncFromDir(dir)parsedToInputrepo.upsertFromSync (importNote 의 fork 정책 회피)
§3-3 GitClient 확장 fetch / rebaseOnto / rebaseAbort / hasUncommittedChanges / listConflicts + (Task 4 추가) refExists
§4 Configure UI SyncSection — URL 입력 + 저장 + 연결 테스트 + 자동 sync 토글 + interval
§5 Conflict UI ConflictModal — local/remote 2 choice (both deferred). path + localText/remoteText preview
§6 자동 주기 SyncTimer — start/stop/reconfigure (settings 변경 시 reconfigure)
§7 IPC 7 채널 (5 sync + 2 settings setter) — types + preload + handler 모두 일치
§8 테스트 +71 단위 (목표 27 초과 — sync 인프라 전수 검증)

코드 품질

Strengths

  • Single write path 강제 (Cut C/D 정책 → Cut E 보강): note_tags INSERT path 4곳 (updateAiResult / updateUserAiFields / importNote / upsertFromSync) 모두 rebuildFtsTagsForNote 호출. note_revisions INSERT path 4곳 (create / updateRawText / importNote / upsertFromSync INSERT branch) 모두 보장. Cut C/D 의 importNote 회귀 패턴이 Cut E 에서 사전 차단.
  • upsertFromSyncupdateRawText delegation: raw_text 변경 시 단일 transaction 안에서 Cut C 의 single write path 그대로 활용 — 새 user revision INSERT chain 보존. 새 INSERT path 추가 안 함.
  • 첫 push (empty remote) 안전: refExists('origin/main') guard (SyncService.ts:152) 가 rebase skip — 첫 push 의 "invalid upstream" 에러 회피.
  • Conflict preview 정직: rebase abort 전 git show :2:<path> (ours) + :3:<path> (theirs) 호출하여 localText/remoteText 채움 — 사용자가 비교 후 선택. 빈 문자열 placeholder 회피 (final review fix 4014146).
  • frontmatter round-trip 보강: Cut B (status) + Cut C (note_revisions) 도입 시 silent gap 됐던 5 필드 (status/status_changed_at/move_reason/due_date/due_date_source) 를 export/parse 모두에서 처리. F5 round-trip 도 동시에 fix.
  • constructor signature change 일관 적용: SyncService(profileDir, exportSvc, importSvc, now?) — main / 테스트 mock 모두 갱신. typecheck 0.

Final review follow-up 적용 (4014146)

initial final review (Opus, 1차) 에서 발견된 2 important issues:

  • SyncConflict.noteId 가 실제로 export filename slug (UUID 아님) — 명명 혼동
  • ConflictModal preview 가 항상 빈 문자열 — 사용자가 비교 없이 선택 (spec 의 side-by-side diff UX 미구현)

→ Fix:

  • SyncConflict.{noteId → path} rename + 모든 사용처 일관 (5 files: SyncService / shared/types / preload / settingsApi / ConflictModal)
  • runSync 의 conflict 분기에서 git show :2:<path> + :3:<path> 호출 → localText/remoteText 채움 → ConflictModal 가 정직한 비교 표시
  • 회귀 test 4 file 갱신 (bidirectional / resolveConflict / sync-ipc / ConflictModal)
  • pathToNoteId 헬퍼 제거 (의미 없는 path stem 추출 회피)

Architecture

  • 단위 분해 명확 (migration / repo / service / ipc / preload / store / component / timer 8계층 분리)
  • 책임 명확성 — upsertFromSync = sync 전용 in-place / importNote = export tree fork-on-conflict / applySyncFromDir = sourceDir loop / SyncService.sync = 6단계 orchestration / SyncTimer = thin scheduler / resolveConflict = per-path checkout
  • 일관성 — 기존 modal pattern (MoveStatusModal / RevisionHistoryModal / ConflictModal) 동일 overlay/X close button/aria-label

Risk 잔재 (final review 분석)

  • 다기기 환경 필수: dogfood 가치는 macOS + Windows 양 기기 sync 에서만. Windows 단독 dogfood = 흐름 시뮬레이션 한계
  • 인증 외부 의존: SSH key / git credential helper. Configure UI 의 "연결 테스트" 첫 진입점
  • timestamp 단조 가정: NTP 부재 시 잘못된 skip/update risk. v0.3.1+ vector clock / revision id 검토
  • silent interval-mode failures: SyncTimer 의 sync 실패 silent. last-error surface 미구현 — manual sync trigger 권장
  • conflict 'both' deferred: v0.3.1+ revision branch 분기 정책과 함께
  • SyncConflict/SyncStatus duplicated in SyncService.ts + shared/types.ts — known tech debt, structurally identical
  • interval upper bound 없음: setSyncIntervalMin 은 min 5 만 enforce. 525600 분 (1 년) 도 허용. dogfood 비현실적 case 라 무시
  • e2e 본 세션 미수행: worktree node_modules 비어 있음. 본 cut UI 변경 = SettingsPage SyncSection + ConflictModal — capture/onboarding flow 무관

Sub-review 트레일

  • Task 1 (GitClient) — Sonnet implementer + spec verbatim
  • Task 2 (upsertFromSync) — Sonnet implementer , 5 신규 test
  • Task 3 (applySyncFromDir + frontmatter) — Sonnet implementer , 18 test (export 7 + import 5 + applySyncFromDir 6)
  • Task 4 (SyncService bidirectional) — Sonnet implementer , refExists guard 추가 + 5 test
  • Task 5 (resolveConflict) — Sonnet implementer , 4 test
  • Task 6 (SettingsService sync) — direct edit , 6 test
  • Task 7 (IPC + types + preload) — Sonnet implementer , 17 IPC test
  • Tasks 8-9 (SyncSection + ConflictModal) — Sonnet batch implementer , 7 test (SyncSection 4 + ConflictModal 3)
  • Task 10 (SyncTimer) — Sonnet implementer , 5 test
  • Task 11 (release commit) — direct
  • Final code review (Opus, 1차) — ⚠ Approved with 2 follow-ups (noteId→path / conflict preview)
  • Final fix re-verification 679/680 (1 pre-existing flake unrelated to Cut E)

머지 권장

  1. PR #30 머지
  2. tag v0.3.0 + npm run dist:win → Windows exe 빌드
  3. Gitea release v0.3.0 + exe attach
  4. macOS host: dist:mac + dist:linux 후 dmg/AppImage/deb 추가 attach
  5. 메모리 정책 갱신 — single write path 강제 (4 path 검증 패턴) / sync source = path identifier / timestamp 단조 가정 / 'both' deferred
  6. 다기기 dogfood 필수 — macOS + Windows 양 기기 sync 1주 + conflict 빈도 / 인증 / 한국어 token 정확도 측정 → v0.3.1 plan (F24 vision + sync 보강)

Overall

Ready to merge. Spec 100% coverage, 608 → 679 unit (+71) + typecheck 0, m007 후 schema 호환, single write path invariant 4 path 전수 검증, v0.2.6 #10 / Cut B disabled / Cut C revision API / Cut D FTS5 모두 호환. final review 2 issues 모두 follow-up commit 으로 처리. semver MINOR 진입 — Cut F (v0.3.1) 진입 가능.

🤖 Reviewed by Claude Opus 4.7 (1M context) with Sonnet sub-agents per subagent-driven-development skill

## 코드 리뷰 — v0.3.0 Cut E **Scope**: 12 commits (662abdb..4014146) — semver MINOR (Major 영역 진입) ### Spec coverage ✅ 100% [Cut E design](docs/superpowers/specs/2026-05-09-v030-cut-e-design.md) 의 모든 섹션이 task 매핑됨. F21-A (자동 rebase) + F21-B (Configure UI) + F21-C (Conflict UI). 'both' choice 만 v0.3.1+ deferred. | spec § | 구현 | |---|---| | §3-1 sync 6단계 | `SyncService.runSync` — export → commit → fetch → rebase → re-import → push (refExists guard 로 first-push 안전) | | §3-2 ImportService 활용 | `applySyncFromDir(dir)` — `parsedToInput` → `repo.upsertFromSync` (importNote 의 fork 정책 회피) | | §3-3 GitClient 확장 | fetch / rebaseOnto / rebaseAbort / hasUncommittedChanges / listConflicts + (Task 4 추가) refExists | | §4 Configure UI | SyncSection — URL 입력 + 저장 + 연결 테스트 + 자동 sync 토글 + interval | | §5 Conflict UI | ConflictModal — local/remote 2 choice (both deferred). path + localText/remoteText preview | | §6 자동 주기 | SyncTimer — start/stop/reconfigure (settings 변경 시 reconfigure) | | §7 IPC | 7 채널 (5 sync + 2 settings setter) — types + preload + handler 모두 일치 | | §8 테스트 | +71 단위 (목표 27 초과 — sync 인프라 전수 검증) | ### 코드 품질 **Strengths** - **Single write path 강제 (Cut C/D 정책 → Cut E 보강)**: `note_tags` INSERT path 4곳 (`updateAiResult` / `updateUserAiFields` / `importNote` / **`upsertFromSync`**) 모두 `rebuildFtsTagsForNote` 호출. `note_revisions` INSERT path 4곳 (`create` / `updateRawText` / `importNote` / **`upsertFromSync` INSERT branch**) 모두 보장. Cut C/D 의 importNote 회귀 패턴이 Cut E 에서 사전 차단. - **`upsertFromSync` 의 `updateRawText` delegation**: raw_text 변경 시 단일 transaction 안에서 Cut C 의 single write path 그대로 활용 — 새 user revision INSERT chain 보존. 새 INSERT path 추가 안 함. - **첫 push (empty remote) 안전**: `refExists('origin/main')` guard (`SyncService.ts:152`) 가 rebase skip — 첫 push 의 "invalid upstream" 에러 회피. - **Conflict preview 정직**: rebase abort 전 `git show :2:<path>` (ours) + `:3:<path>` (theirs) 호출하여 localText/remoteText 채움 — 사용자가 비교 후 선택. 빈 문자열 placeholder 회피 (final review fix `4014146`). - **frontmatter round-trip 보강**: Cut B (status) + Cut C (note_revisions) 도입 시 silent gap 됐던 5 필드 (status/status_changed_at/move_reason/due_date/due_date_source) 를 export/parse 모두에서 처리. F5 round-trip 도 동시에 fix. - **constructor signature change 일관 적용**: `SyncService(profileDir, exportSvc, importSvc, now?)` — main / 테스트 mock 모두 갱신. typecheck 0. **Final review follow-up 적용** (`4014146`) initial final review (Opus, 1차) 에서 발견된 2 important issues: - ❌ `SyncConflict.noteId` 가 실제로 export filename slug (UUID 아님) — 명명 혼동 - ❌ ConflictModal preview 가 항상 빈 문자열 — 사용자가 비교 없이 선택 (spec 의 side-by-side diff UX 미구현) → Fix: - ✅ `SyncConflict.{noteId → path}` rename + 모든 사용처 일관 (5 files: SyncService / shared/types / preload / settingsApi / ConflictModal) - ✅ `runSync` 의 conflict 분기에서 `git show :2:<path>` + `:3:<path>` 호출 → localText/remoteText 채움 → ConflictModal 가 정직한 비교 표시 - ✅ 회귀 test 4 file 갱신 (bidirectional / resolveConflict / sync-ipc / ConflictModal) - ✅ `pathToNoteId` 헬퍼 제거 (의미 없는 path stem 추출 회피) ### Architecture - 단위 분해 명확 (migration / repo / service / ipc / preload / store / component / timer 8계층 분리) - 책임 명확성 — `upsertFromSync` = sync 전용 in-place / `importNote` = export tree fork-on-conflict / `applySyncFromDir` = sourceDir loop / `SyncService.sync` = 6단계 orchestration / `SyncTimer` = thin scheduler / `resolveConflict` = per-path checkout - 일관성 — 기존 modal pattern (`MoveStatusModal` / `RevisionHistoryModal` / `ConflictModal`) 동일 overlay/X close button/aria-label ### Risk 잔재 (final review 분석) - **다기기 환경 필수**: dogfood 가치는 macOS + Windows 양 기기 sync 에서만. Windows 단독 dogfood = 흐름 시뮬레이션 한계 - **인증 외부 의존**: SSH key / git credential helper. Configure UI 의 "연결 테스트" 첫 진입점 - **timestamp 단조 가정**: NTP 부재 시 잘못된 skip/update risk. v0.3.1+ vector clock / revision id 검토 - **silent interval-mode failures**: SyncTimer 의 sync 실패 silent. last-error surface 미구현 — manual sync trigger 권장 - **conflict 'both' deferred**: v0.3.1+ revision branch 분기 정책과 함께 - **`SyncConflict`/`SyncStatus` duplicated** in `SyncService.ts` + `shared/types.ts` — known tech debt, structurally identical - **interval upper bound 없음**: `setSyncIntervalMin` 은 min 5 만 enforce. 525600 분 (1 년) 도 허용. dogfood 비현실적 case 라 무시 - **e2e 본 세션 미수행**: worktree node_modules 비어 있음. 본 cut UI 변경 = SettingsPage SyncSection + ConflictModal — capture/onboarding flow 무관 ### Sub-review 트레일 - **Task 1 (GitClient)** — Sonnet implementer + spec verbatim ✅ - **Task 2 (upsertFromSync)** — Sonnet implementer ✅, 5 신규 test - **Task 3 (applySyncFromDir + frontmatter)** — Sonnet implementer ✅, 18 test (export 7 + import 5 + applySyncFromDir 6) - **Task 4 (SyncService bidirectional)** — Sonnet implementer ✅, refExists guard 추가 + 5 test - **Task 5 (resolveConflict)** — Sonnet implementer ✅, 4 test - **Task 6 (SettingsService sync)** — direct edit ✅, 6 test - **Task 7 (IPC + types + preload)** — Sonnet implementer ✅, 17 IPC test - **Tasks 8-9 (SyncSection + ConflictModal)** — Sonnet batch implementer ✅, 7 test (SyncSection 4 + ConflictModal 3) - **Task 10 (SyncTimer)** — Sonnet implementer ✅, 5 test - **Task 11 (release commit)** — direct - **Final code review (Opus, 1차)** — ⚠ Approved with 2 follow-ups (noteId→path / conflict preview) - **Final fix re-verification** — ✅ 679/680 (1 pre-existing flake unrelated to Cut E) ### 머지 권장 1. PR #30 머지 2. tag `v0.3.0` + `npm run dist:win` → Windows exe 빌드 3. Gitea release v0.3.0 + exe attach 4. macOS host: `dist:mac` + `dist:linux` 후 dmg/AppImage/deb 추가 attach 5. 메모리 정책 갱신 — single write path 강제 (4 path 검증 패턴) / sync source = path identifier / timestamp 단조 가정 / 'both' deferred 6. **다기기 dogfood 필수** — macOS + Windows 양 기기 sync 1주 + conflict 빈도 / 인증 / 한국어 token 정확도 측정 → v0.3.1 plan (F24 vision + sync 보강) ### Overall **Ready to merge.** Spec 100% coverage, 608 → 679 unit (+71) + typecheck 0, m007 후 schema 호환, single write path invariant 4 path 전수 검증, v0.2.6 #10 / Cut B disabled / Cut C revision API / Cut D FTS5 모두 호환. final review 2 issues 모두 follow-up commit 으로 처리. semver MINOR 진입 — Cut F (v0.3.1) 진입 가능. 🤖 Reviewed by Claude Opus 4.7 (1M context) with Sonnet sub-agents per subagent-driven-development skill
altair823 merged commit a54f134343 into main 2026-05-09 19:24:45 +00:00
altair823 deleted branch worktree-v030-cut-e-bidirectional-sync 2026-05-09 19:24:46 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/inkling#30