Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
→ FileRegistry가 해당 파일의 SyncService를 조회
→ 서버가 원자적 쓰기 (tmp + rename)
→ content hash 업데이트 (에코 방지)
→ 해당 파일의 WS 클라이언트에 file-changed 브로드캐스트
→ PUT 클라이언트는 remoteUpdateUntilRef로 수신 무시 (에코 방지)
```

### 2.3 충돌 시나리오 (→ D-008: Last Write Wins)
Expand Down Expand Up @@ -353,7 +355,8 @@ Vync/ # nx monorepo (Drawnix 포크 기반)
│ │ ├── security.ts # validateFilePath + hostGuard (LFI/DNS rebinding 방지) [VYNC 추가: Phase 8]
│ │ ├── file-watcher.ts # chokidar 파일 감시 (unlink 이벤트 포함)
│ │ ├── sync-service.ts # 동기화 로직 (에코 방지 + 원자적 쓰기 + drain())
│ │ └── ws-handler.ts # WebSocket 메시지 핸들러 (파일 스코프 라우팅, ?file=)
│ │ ├── ws-handler.ts # WebSocket 메시지 핸들러 (파일 스코프 라우팅, ?file=)
│ │ └── __tests__/ # 서버 테스트 (hub-ws, discover, security, multi-file-e2e, put-broadcast 등)
│ └── cli/
│ ├── main.ts # CLI 진입점 (init/open/close/stop/diff 라우팅) [VYNC 수정: Phase 8, diff 추가]
│ ├── init.ts # vync init: 빈 .vync 파일 생성
Expand Down
124 changes: 124 additions & 0 deletions docs/ISSUES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Vync — 이슈 레지스트리

> 발견된 버그와 기술적 문제를 추적한다. 해결되면 상태를 `resolved`로 변경하고 해결 내용을 기록한다.
> 설계 결정은 [DECISIONS.md](./DECISIONS.md), 구현 계획은 [PLAN.md](./PLAN.md) 참조.

---

## 이슈 목록

| ID | 제목 | 심각도 | 상태 | 컴포넌트 | 발견일 |
|----|------|--------|------|----------|--------|
| I-001 | [Sub-agent `.lastread` Write 실패](#i-001) | minor | open | vync-translator | 2026-03-14 |
| I-002 | [PUT /api/sync가 WebSocket 브로드캐스트 안 함](#i-002) | major | resolved | server | 2026-03-14 |
| I-003 | [Electron 모드에서 `vync open` 시 브라우저 중복 열림](#i-003) | minor | resolved | CLI (open.ts) | 2026-03-14 |
| I-004 | [probePort()가 mode를 항상 'daemon'으로 덮어씀](#i-004) | minor | resolved | CLI (open.ts) | 2026-03-14 |

---

## 상태 정의

| 상태 | 설명 |
|------|------|
| `open` | 발견됨, 미착수 |
| `in-progress` | 수정 진행 중 |
| `resolved` | 해결 완료 (해결일 + 방법 기록) |
| `won't-fix` | 수정하지 않기로 결정 (사유 기록) |

## 심각도 정의

| 심각도 | 설명 |
|--------|------|
| `critical` | 데이터 손실 또는 핵심 기능 불가 |
| `major` | 기능 저하, 워크어라운드 존재 |
| `minor` | 불편하지만 기능에 영향 없음 |

---

## 상세

### I-001

**Sub-agent `.lastread` Write 실패**

심각도: `minor` · 상태: `open` · 발견일: 2026-03-14
컴포넌트: `vync-translator` sub-agent / diff pipeline

**현상**:
Sub-agent가 `.vync` 파일 수정 후 `.lastread` 스냅샷 파일을 직접 Write하려 할 때, Claude Code의 Write 도구 안전 장치에 의해 차단됨.

```
Write(/Users/presence/projects/Vync/.vync/roadmap.vync.lastread)
→ Error: File has not been read yet. Read it first before writing to it.
```

**근본 원인**:
1. **직접 원인**: Write 도구는 기존 파일을 덮어쓰려면 먼저 Read해야 하는 안전 장치가 있음. Sub-agent가 `.vync` 파일은 읽었지만 `.lastread` 파일은 읽지 않고 Write 시도.
2. **구조적 원인**: `.lastread` 스냅샷 갱신은 `vync diff` 명령이 `Snapshot updated`로 자동 처리하는 영역. Sub-agent가 이 역할을 직접 수행하려 한 것 자체가 역할 경계 위반.

**영향**:
- `.lastread` 스냅샷이 갱신되지 않아, 다음 `vync diff` 실행 시 이미 확인된 변경이 다시 보고될 수 있음
- `.vync` 파일 자체는 정상 수정됨 — 데이터 손실 없음

**워크어라운드**:
`vync diff <file>` 한 번 실행하면 스냅샷이 동기화됨.

**해결 방향**:
- `agents/vync-translator.md`에 `.lastread` 파일을 직접 조작하지 말라는 명시적 지침 추가
- 또는 MCP 서버 전환 시 스냅샷 관리를 Tool API로 캡슐화하여 구조적으로 방지

---

### I-002

**PUT /api/sync가 WebSocket 브로드캐스트 안 함**

심각도: `major` · 상태: `resolved` · 발견일: 2026-03-14 · 해결일: 2026-03-14
컴포넌트: `tools/server/server.ts` (PUT /api/sync 핸들러)

**현상**:
브라우저 A가 캔버스 편집 → PUT /api/sync → 서버가 디스크에 쓰기 → chokidar가 감지하지만 `isWriting=true`로 에코 방지됨 → 다른 클라이언트(B)에 브로드캐스트 안 됨. 멀티 탭/멀티 윈도우 환경에서 편집이 다른 클라이언트에 반영되지 않음.

**근본 원인**:
PUT 핸들러가 `sync.writeFile()` 후 `res.json({ ok: true })`만 반환. chokidar 경로는 에코 방지(isWriting=true + hash 일치)로 항상 억제됨. 결과적으로 PUT으로 들어온 변경이 어떤 WS 클라이언트에도 전달되지 않음.

**해결**:
PUT 핸들러에서 `sync.writeFile()` 후 `registry.broadcastToFile(filePath, { type: 'file-changed', filePath, data })` 추가. PUT 클라이언트는 `remoteUpdateUntilRef` 메커니즘으로 수신 무시 (에코 방지).

---

### I-003

**Electron 모드에서 `vync open` 시 브라우저 중복 열림**

심각도: `minor` · 상태: `resolved` · 발견일: 2026-03-14 · 해결일: 2026-03-14
컴포넌트: `tools/cli/open.ts` (vyncOpen)

**현상**:
Electron 서버가 실행 중일 때 `vync open <file2>` 호출 시 파일 등록 후 시스템 브라우저도 열림. Electron 내에서 Hub WS로 탭이 자동 추가되므로 브라우저 열기는 불필요.

**근본 원인**:
`vyncOpen()`에서 서버가 이미 실행 중일 때 `info?.mode`를 확인하지 않고 항상 `openBrowserWithFile()` 호출.

**해결**:
`info?.mode !== 'electron'` 조건 추가. Electron 모드이면 파일 등록만 하고 브라우저 열기 생략.

---

### I-004

**probePort()가 mode를 항상 'daemon'으로 덮어씀**

심각도: `minor` · 상태: `resolved` · 발견일: 2026-03-14 · 해결일: 2026-03-14
컴포넌트: `tools/cli/open.ts` (probePort)

**현상**:
Electron 서버 실행 중 PID 파일이 없거나 stale일 때, `probePort()`가 포트에서 서버를 발견하면 PID 파일을 `mode: 'daemon'`으로 항상 복구. 이로 인해 Electron 서버임에도 `mode: 'daemon'`으로 기록됨.

**근본 원인**:
`probePort()`의 recoveredInfo에서 `mode: 'daemon'`으로 하드코딩.

**해결**:
`readServerInfo()`로 기존 PID 파일의 mode를 읽어서 보존. 기존 PID 파일이 없으면 `'daemon'` 기본값 사용.

---
45 changes: 37 additions & 8 deletions docs/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@

## 현재 상태

**Phase**: Post-MVP 안정화 — 2026-03-12
**Phase**: Post-MVP 안정화 — 2026-03-14
- Phase 1~9 (MVP): 완료 (2026-03-07 ~ 2026-03-09)
- Diff Pipeline (D-015/D-016): 완료 (2026-03-11)
- Server Lifecycle Fix (PR #10): 완료 (2026-03-11)
- macOS 코드 서명 + 공증: 완료 (2026-03-11)
- Tab Bar "+" 버튼 수정 (PR #11): 구현 완료, PR 대기 (2026-03-12)
- Semantic Sync (D-017): 구현 완료, PR 대기 (2026-03-13) — `feat/semantic-sync` 브랜치
- Tab Bar "+" 버튼 수정 (PR #11): 완료 (2026-03-12, develop 병합)
- Semantic Sync (D-017/D-018): 완료 (PR #14, 2026-03-13, develop 병합)
- Plugin Path Fix (PR #15): 완료 ($VYNC_HOME 기반 경로, 2026-03-13, develop 병합)
- Asar Unpacked Path Fix: 완료 (정적 파일 경로 수정, `fcba037`)
- Fix: Electron/Web Sync + Duplicate Browser Opening: 완료 (2026-03-14, develop)
- PUT /api/sync 후 WebSocket 브로드캐스트 추가 (I-002)
- Electron 모드에서 `vync open` 시 브라우저 중복 열기 방지 (I-003)
- probePort() mode 보존 (I-004)
- 95개 테스트 PASS (신규 2개)
- **develop → main 병합 필요** (develop가 main 대비 20+ 커밋 ahead)

---

Expand Down Expand Up @@ -342,10 +350,9 @@
### 검증
- [x] TB.9 TypeScript 컴파일 확인
- [x] TB.10 전체 테스트 PASS (72개)
- [ ] TB.11 E2E 수동 검증 (6개 시나리오)
- [x] TB.11 E2E 수동 검증

### 남은 작업
- E2E 수동 검증 후 PR 생성 (fix/tab-add-button → develop)
**완료**: develop 병합 (`7b83ae8`), fix/tab-add-button 브랜치 삭제됨

---

Expand Down Expand Up @@ -373,8 +380,30 @@
- [x] 메인 세션이 확신 레벨별 행동 가이드 보유
- [x] 93개 전체 테스트 PASS (diff 31개 포함)

### 남은 작업
- PR 생성 (feat/semantic-sync → develop)
**PR**: #14 (feat/semantic-sync → develop, 2026-03-13), feat/semantic-sync 브랜치 삭제됨

---

## Plugin Path Fix (PR #15)

**목표**: 플러그인 스크립트 경로를 하드코딩(`~/.claude/skills/...`)에서 `$VYNC_HOME` 환경변수 기반으로 전환하여 마켓플레이스 설치 환경에서도 정상 동작하게 한다.
**브랜치**: `fix/plugin-path`
**계획**: `docs/plans/2026-03-13-plugin-path-fix.md`

- [x] PP.1 `skills/vync-editing/SKILL.md` — 5곳 `$VYNC_HOME` 전환
- [x] PP.2 `agents/vync-translator.md` — 7곳 `$VYNC_HOME` 전환 + fallback 섹션
- [x] PP.3 PoC 3/3 PASS (sub-agent $VYNC_HOME 접근, 스크립트 실행, Read fallback)
- [x] PP.4 E2E 검증 PASS (새 세션, `/vync create` 전체 흐름)
- [x] PP.5 플러그인 캐시 동기화 완료

**PR**: #15 (fix/plugin-path → develop, 2026-03-13)

---

## Asar Unpacked Path Fix

**목표**: Electron 패키징 시 정적 파일 경로를 `asar.unpacked` 기반으로 수정.
**커밋**: `fcba037` (fix(desktop): use asar.unpacked path for static assets)

---

Expand Down
13 changes: 9 additions & 4 deletions tools/cli/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,12 @@ async function probePort(): Promise<{
const body = await res.json();
if (body.version !== 2) return { running: false, info: null };

// Recover PID file from health response
// Recover PID file from health response, preserving existing mode if available
const existingInfo = await readServerInfo().catch(() => null);
const recoveredInfo: ServerInfo = {
version: 2,
pid: body.pid,
mode: 'daemon',
mode: existingInfo?.mode ?? 'daemon',
port: PORT,
};
await writeServerInfo(recoveredInfo);
Expand Down Expand Up @@ -394,10 +395,14 @@ export async function vyncOpen(
const port = info?.port ?? PORT;

if (running) {
// Hub mode: register file and open browser
// Hub mode: register file
console.log('[vync] Server running, registering file...');
await registerFile(port, resolved);
await openBrowserWithFile(port, resolved);
if (info?.mode !== 'electron') {
await openBrowserWithFile(port, resolved);
} else {
console.log('[vync] File registered. Electron will update via Hub WS.');
}
return;
}

Expand Down
Loading
Loading