Files
inkling/docs/superpowers/specs/2026-05-09-v030-cut-e-design.md
altair823 7d2b8c95ec docs(v028+): F17~F25 dogfood + roadmap + Cut A~G specs + Cut A plan
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>
2026-05-09 15:09:02 +09:00

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.md Cut 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:

  1. Mac + Windows 양 기기 sync 1주 — 충돌 빈도 측정
  2. 자동 주기 sync 의 timing — battery / network 영향
  3. revision merge 정확도 (사용자 confirm 비율)