v0.3.0 Cut E — 양방향 git sync + Configure UI + Conflict resolution (F21) #30
Reference in New Issue
Block a user
Delete Branch "worktree-v030-cut-e-bidirectional-sync"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
v0.3.0 Cut E — F21 (다기기 git-based 동기화) 의 옵션 A+B+C 적용. semver MINOR (Major 영역 진입) — push-only
SyncService→ 양방향 6단계 흐름 + Configure UI + Conflict resolution.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.git ls-remote) + 자동 sync 토글 + interval input (default 30분, min 5).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 분기 정책 미정).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).ExportNote/ParsedNoteinterface +composeFrontmatter/parseExportNote/noteToExportNote모두 5 필드 (status / status_changed_at / move_reason / due_date / due_date_source) 추가.syncSvc.sync()호출. settings 변경 시reconfigure()(stop + start). interval mode 실패는 silent (다음 attempt / manual sync 가 처리).변경 내역 (12 commits)
Phase 0: 문서
662abdbdocs(plan) — Cut E plan + spec 정정 (단위 608, ImportService.run 활용, 'sync' enum 미도입, both deferred)Phase 1: schema/git
dba64c5Task 1 — GitClient: fetch / rebaseOnto / rebaseAbort / hasUncommittedChanges / listConflicts (+ refExists Task 4 추가)Phase 2: repo
bbfd0ccTask 2 —NoteRepository.upsertFromSync(sync 전용 3 분기 + single write path)Phase 3: import 인프라
9a1f0e2Task 3 —ImportService.applySyncFromDir+ frontmatter status/dueDate/moveReason round-tripPhase 4-5: sync 엔진
33588b0Task 4 —SyncService.sync양방향 6단계 + conflict 반환 +refExistsguard8436846Task 5 —SyncService.resolveConflict(local/remote 2 choice, both deferred)Phase 6: settings
62e68dcTask 6 — settings.sync_repo_url + sync_auto_enabled + sync_interval_minPhase 7: 와이어업
9e48624Task 7 — sync IPC + preload (configure / test / list-conflicts / resolve / status)Phase 8-9: UI
87c18a4Tasks 8-9 — SyncSection (Configure UI) + ConflictModal + SettingsPage mountPhase 10: timer
e3f6c71Task 10 — SyncTimer (자동 주기 + reconfigure)Phase 11: release
2ef4802chore(release) — version 0.2.11 → 0.3.0 + F21 promotedPhase 12: final review fix
4014146fix(v030) — SyncConflict noteId → path + populate localText/remoteText (final review)테스트 / 빌드
sync returns changed=false when DB unchanged on second run— pre-existing timing test)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 보존note_tagsINSERT path =updateAiResult/updateUserAiFields/importNote/upsertFromSync4곳, 모두rebuildFtsTagsForNote호출.note_revisionsINSERT path =create/updateRawText(Cut C delegate) /importNote/upsertFromSyncINSERT branch 4곳upsertFromSync의updatedAtlexicographic compare. 양 기기 시계 동기화 가정 (NTP 부재 시 잘못된 skip/update risk)<date>-<id8>-<slug>.md.git checkout --ours/--theirs가 path 받음 —SyncConflict.path가 정직한 명명'user'revision (의미상 사용자 의도 전파). m008 회피Risk 잔재 (final review)
upsertFromSync의updatedAt비교 부정확. dogfood 검증 필요. v0.3.1+ 에서 vector clock / revision id 검토 가능Test Plan
<profileDir>/sync/git init + remote add origin 정상git ls-remote origin결과 표시 (인증 OK / 실패):2:/:3:) + path 표시note_revisions가 sync 양 기기에서 일관 (각자 m007 마이그레이션 후 trigger 자동 sync)🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.7 (1M context) <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코드 리뷰 — 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.
SyncService.runSync— export → commit → fetch → rebase → re-import → push (refExists guard 로 first-push 안전)applySyncFromDir(dir)—parsedToInput→repo.upsertFromSync(importNote 의 fork 정책 회피)코드 품질
Strengths
note_tagsINSERT path 4곳 (updateAiResult/updateUserAiFields/importNote/upsertFromSync) 모두rebuildFtsTagsForNote호출.note_revisionsINSERT path 4곳 (create/updateRawText/importNote/upsertFromSyncINSERT branch) 모두 보장. Cut C/D 의 importNote 회귀 패턴이 Cut E 에서 사전 차단.upsertFromSync의updateRawTextdelegation: raw_text 변경 시 단일 transaction 안에서 Cut C 의 single write path 그대로 활용 — 새 user revision INSERT chain 보존. 새 INSERT path 추가 안 함.refExists('origin/main')guard (SyncService.ts:152) 가 rebase skip — 첫 push 의 "invalid upstream" 에러 회피.git show :2:<path>(ours) +:3:<path>(theirs) 호출하여 localText/remoteText 채움 — 사용자가 비교 후 선택. 빈 문자열 placeholder 회피 (final review fix4014146).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 아님) — 명명 혼동→ Fix:
SyncConflict.{noteId → path}rename + 모든 사용처 일관 (5 files: SyncService / shared/types / preload / settingsApi / ConflictModal)runSync의 conflict 분기에서git show :2:<path>+:3:<path>호출 → localText/remoteText 채움 → ConflictModal 가 정직한 비교 표시pathToNoteId헬퍼 제거 (의미 없는 path stem 추출 회피)Architecture
upsertFromSync= sync 전용 in-place /importNote= export tree fork-on-conflict /applySyncFromDir= sourceDir loop /SyncService.sync= 6단계 orchestration /SyncTimer= thin scheduler /resolveConflict= per-path checkoutMoveStatusModal/RevisionHistoryModal/ConflictModal) 동일 overlay/X close button/aria-labelRisk 잔재 (final review 분석)
SyncConflict/SyncStatusduplicated inSyncService.ts+shared/types.ts— known tech debt, structurally identicalsetSyncIntervalMin은 min 5 만 enforce. 525600 분 (1 년) 도 허용. dogfood 비현실적 case 라 무시Sub-review 트레일
머지 권장
v0.3.0+npm run dist:win→ Windows exe 빌드dist:mac+dist:linux후 dmg/AppImage/deb 추가 attachOverall
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