v0.2.7 release 후 dogfood 9건 누적 (F17~F25) 정리: - F17 휴지통 의미 분기 / F18 사유 입력 / F19 recall / F20 raw_text 가변 - F21 다기기 sync / F22 이미지 렌더링 (이미 v0.2.8 promoted) / F23 Ollama-less - F24 멀티모달 vision / F25 사이드바 + 저장소 추가: - v0.2.8+ roadmap: 7 cut 분할 (A~G), 12주 시간선, dependency graph - Cut A~G design specs (각 cut 별 design 결정 + schema + UI + 테스트 전략) - Cut A implementation plan (이미 v0.2.8 머지로 실행 완료, 참고 보존) PR #26 머지 후 main 에 doc commits rebase 안 되어 manual merge 진행: - F22 entry 는 origin/main 의 promoted 형태 우선 - 신규 9 파일 (specs/plan/roadmap) 은 origin/main 에 없는 파일 - "다음 항목 자리" 안내 F23 → F26 갱신 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.0 KiB
v0.3.0 — Cut E Design (다기기 git-based 양방향 sync)
작성일: 2026-05-09 선행 문서:
docs/superpowers/specs/2026-04-25-dogfood-feedback.md(F21)docs/superpowers/strategy/v028plus-roadmap.mdCut E
Cut 라벨: v0.3.0 — semver MINOR (새 인프라 — 양방향 sync + Configure UI). Major 영역 진입.
1. Cut 정체성
기존 push-only SyncService → 양방향 (pull + import + conflict resolution + Configure UI). 다기기 (Mac 업무 + Windows 개인) dogfood 가능.
2. 범위
| 항목 | 결정 |
|---|---|
| F21 옵션 A | git fetch && rebase 후 markdown → SQLite re-import. 자동 rebase default (충돌 시 fail + 사용자 prompt) |
| F21 옵션 B | 설정 페이지 안 "동기화 저장소" sub-section — URL 입력 + 인증 안내 + 마지막 sync 결과 |
| F21 옵션 C | conflict UI — 자동 rebase 실패 시 양쪽 비교 + 사용자 선택 |
| pull 시점 | 양쪽 — manual ("지금 동기화") + 자동 주기 (사용자 설정 가능 interval, default 30분) |
| revision 결합 (Cut C) | note_revisions 가 sync 대상 — 양 기기 rev 가 다른 chain 에 있으면 timestamp linear merge (옛 rev 가 sync source 로 inserted) |
3. SyncService 양방향화
3-1. 갱신된 sync() 흐름
async sync(opts: { interval?: boolean } = {}): Promise<SyncStatus> {
if (!(await this.isConfigured())) return { ok: false, reason: 'not_configured' };
const git = new GitClient(this.syncDir);
// 1. fetch
const fetchR = await git.fetch();
if (fetchR.exitCode !== 0) return { ok: false, reason: `fetch failed: ${fetchR.stderr}` };
// 2. local export (변경 감지 위해)
await this.exportSvc.export(this.syncDir, { includeMedia: true });
await git.addAll();
const localChanged = await git.hasUncommittedChanges();
// 3. local commit (있으면)
let localSha: string | null = null;
if (localChanged) {
const c = await git.commit(`chore(notes): sync ${this.now().toISOString()}`);
localSha = c.sha;
}
// 4. rebase
const rebaseR = await git.rebaseOnto('origin/main');
if (rebaseR.exitCode !== 0) {
// conflict — abort + 사용자에게 conflict UI 안내
await git.rebaseAbort();
return { ok: false, reason: 'conflict', conflicts: await this.listConflicts() };
}
// 5. re-import (rebase 후 markdown 변경 → SQLite 적용)
const imported = await this.importSvc.importAll(this.syncDir);
// 6. push
const pushR = await git.push();
if (pushR.exitCode !== 0) return { ok: false, reason: `push failed: ${pushR.stderr}` };
return { ok: true, changed: localChanged || imported.changedCount > 0, localSha, importedCount: imported.changedCount, pushed: true };
}
3-2. ImportService 활용
기존 ImportService (백업 복원 흐름) 가 markdown → SQLite 적재. sync 의 re-import 도 같은 service 활용:
class ImportService {
async importAll(dir: string): Promise<{ changedCount: number; conflicts: string[] }> {
// dir 하위의 모든 .md 파일 → frontmatter parse → notes UPSERT
// existing note 와 비교 — updated_at 더 최신이면 갱신, 아니면 skip
// raw_text 다른 경우 → note_revisions 에 INSERT (new rev, edited_by='sync')
}
}
revision linear merge 정책:
- 옛 rev (origin/main 의 rev_5) 가 local 에 없으면 → INSERT note_revisions (timestamp 기준 적절 위치)
- local rev 와 origin rev 가 동일 timestamp + 다른 raw_text → conflict (사용자 prompt)
- 일반적으로 다른 timestamp 면 timestamp 순 linear chain 으로 merge
3-3. GitClient 확장
class GitClient {
// 기존: run, isRepo, hasRemote, addAll, commit, push
// 신규
async fetch(): Promise<GitExecResult>;
async rebaseOnto(ref: string): Promise<GitExecResult>;
async rebaseAbort(): Promise<GitExecResult>;
async hasUncommittedChanges(): Promise<boolean>;
async listConflicts(): Promise<string[]>; // git diff --name-only --diff-filter=U
}
4. Configure UI (옵션 B)
설정 페이지 → 신규 sub-section "동기화 저장소":
[동기화 저장소]
저장소 URL: [git@gitea.example.com:user/inkling-notes.git]
[ 저장 ] [ 연결 테스트 ]
마지막 sync: 2026-05-09 14:32 (성공, 3건 가져옴, 2건 보냄)
다음 자동 sync: 2026-05-09 15:02
[ 자동 sync 사용 ]
interval: [30] 분
[ 지금 동기화 ] [ 충돌 해결... ]
저장소 URL 변경 → main 의 settings:configure-sync IPC 호출 → SyncService 가 <profileDir>/sync/ 에 git init + remote add origin (없으면). 인증 (SSH key / token) 은 사용자 OS 설정 (~/.ssh/ 또는 git credential helper) — Inkling 자체 인증 X, 안내 메시지만.
5. Conflict UI (옵션 C)
자동 rebase 실패 시 SyncService 가 { ok: false, reason: 'conflict', conflicts: [...] } 반환. 설정 페이지 의 "충돌 해결..." 버튼 활성화.
클릭 → modal:
충돌 N건
[note-id-1.md]
< 내 기기 > | < 다른 기기 >
본문 A | 본문 B
|
[ 내 것 사용 ] [ 원격 사용 ] [ 양쪽 보존 (옛 revision 으로) ]
선택:
- 내 것 사용: local 채택 (origin 변경 폐기)
- 원격 사용: origin 채택 (local 변경 → note_revisions 에 보존)
- 양쪽 보존: local + origin 모두 note_revisions 에 INSERT, latest = 사용자 선택 (또는 timestamp 더 최신)
확정 → SyncService.resolveConflict(noteId, choice) → git rebase --continue → push.
6. 자동 주기 sync
main process 가 settings.sync_interval_min (default 30) 마다 SyncService.sync({ interval: true }) 호출. interval=true 시 conflict 발생해도 silent (notification 만, 사용자가 다음 manual 또는 conflict UI 진입 시 처리).
settings: sync_auto_enabled: boolean (default true 단, configured 일 때만), sync_interval_min: number (default 30, min 5).
7. IPC
// 신규
'settings:configure-sync': (url: string) => Promise<{ ok: true } | { ok: false; reason: string }>
'settings:test-sync-connection': () => Promise<{ ok: true } | { ok: false; reason: string }> // git ls-remote
'sync:list-conflicts': () => Promise<Array<{ noteId: string; localText: string; remoteText: string }>>
'sync:resolve-conflict': (noteId: string, choice: 'local' | 'remote' | 'both') => Promise<{ ok: true }>
'sync:get-status': () => Promise<{ lastAt: string | null; lastResult: SyncStatus | null; nextAt: string | null }>
8. 테스트 전략
| 영역 | 단위 |
|---|---|
GitClient.fetch / rebaseOnto / rebaseAbort |
mock execFile + 결과 검증 |
SyncService.sync 양방향 |
mock GitClient + ImportService → 6 단계 흐름 |
| 자동 rebase 성공 | conflict 없는 시나리오 |
| 자동 rebase 실패 → abort | conflict 시 rebaseAbort + reason 반환 |
| ImportService.importAll | markdown → notes UPSERT + revision INSERT |
| revision merge | 양 chain → timestamp 순 linear |
| Configure UI | URL 입력 → IPC → git init/remote add |
| Conflict UI | 3 choice 별 sync 동작 |
| 자동 주기 sync | timer + interval=true mode |
목표: 단위 528 → 약 555 (+27), typecheck 0.
9. Risk
| Risk | 대응 |
|---|---|
| 인증 설정 실패 (사용자 SSH key 부재) | Configure UI 의 "연결 테스트" 버튼 — git ls-remote 결과 사용자에게 표시 |
| revision linear merge 정확도 | timestamp 단조 증가 가정 (양 기기 시계 동기화). NTP 부재 시 충돌 risk → 사용자 prompt |
| 자동 주기 sync 의 silent 충돌 누적 | interval mode 충돌 시 notification + 충돌 UI 자동 popup option |
| Cut C revision history 와 sync 결합 시 chain 분기 | 본 cut 의 정책: timestamp linear, branch 분기 미지원 (사용자 manual 결정으로 처리) |
10. v0.3.0 후
Cut F (v0.3.1) — F24 멀티모달 vision.
dogfood verify:
- Mac + Windows 양 기기 sync 1주 — 충돌 빈도 측정
- 자동 주기 sync 의 timing — battery / network 영향
- revision merge 정확도 (사용자 confirm 비율)