# task-2783+2 완료 보고 — PR #262 same-PR microfix (Gemini symlink HIGH 해소)

## Situation
PR #262(task-2783) head `1dc47e10`에 Gemini 재리뷰 **새 HIGH**(security-high):
`utils/replacement_pr_runner.py::transplant_expected_files`의 `target.write_text()`가
target이 symlink이면 **링크를 따라가 링크 대상(victim) 파일을 덮어씀**(symlink 자체를 regular file로 교체하지 않음).
회장/GPT 판정 = 정당한 지적, same-PR microfix로 해소.

## Complication
symlink 자체를 regular file로 교체해야 하나, repo **밖**을 가리키는 symlink는 삭제하면 안 됨(외부 파일 보호).
기존 A안 성과(정상 empty blob 허용 / GIT_SHOW_FAILED fail-closed / Guard1 repo-root escape fail)는 반드시 보존해야 함.

## Question
Guard1(TARGET_OUTSIDE_REPO_ROOT)을 유지하면서, repo 안으로 확인된 symlink target만 안전하게 regular file로 교체하려면?

## Answer
`transplant_expected_files` write 경로에 **Guard 2**를 삽입. Guard1 이후·write_text 직전 순서 보장.

### 처리 순서 (GPT verbatim 준수)
1. `resolved_target = target.resolve()`이 repo_root 밖이면 **`TARGET_OUTSIDE_REPO_ROOT` fail** (Guard1, 순서상 먼저 — 기존 유지).
2. repo 안으로 확인된 `target`이 **symlink이면 `target.unlink()` 후 regular file write** (Guard 2, 신규).
3. repo **밖**을 가리키는 symlink는 1번(Guard1)에서 먼저 fail → **삭제 안 함** (symlink.resolve()가 repo 밖 → relative_to 실패).
4. 일반 파일은 기존처럼 write.
5. 정상 empty blob 허용 / GIT_SHOW_FAILED fail-closed / repo-root escape fail **그대로 유지** (A안 성과 보존).

### Gemini symlink HIGH 해소 근거 (코드 순서)
`utils/replacement_pr_runner.py:194-212`
```
target = Path(cwd) / filepath
resolved_target = target.resolve()          # (Guard1) symlink는 최종 대상으로 resolve
resolved_target.relative_to(repo_root)       # repo 밖이면 TARGET_OUTSIDE_REPO_ROOT (외부 symlink 여기서 fail·미삭제)
content = sr.stdout or ""
target.parent.mkdir(parents=True, exist_ok=True)
if target.is_symlink():                       # (Guard2 신규) repo 안 확인된 symlink만
    target.unlink()                           # symlink 자체 제거 (victim 미접촉)
target.write_text(content, encoding="utf-8")  # regular file로 write
```
→ 내부 symlink는 unlink 후 regular file로 교체(victim 불변), 외부 symlink는 Guard1에서 fail(미삭제). HIGH 해소.

## 수정 파일 (정확히 2파일)
1. `utils/replacement_pr_runner.py` — Guard 2 삽입 (4줄 추가, 삭제 0)
2. `tests/regression/test_replacement_pr_runner_2510.py` — 회귀 테스트 T19/T20 추가 (68줄 추가)

## 회귀 테스트 (신규 2건)
- **T19 (내부 symlink)**: tmp repo 안 victim 생성 → target(expected path)을 victim 향하는 symlink로 생성 → transplant 실행 →
  ①target이 regular file로 교체 ②expected content가 target path에 write ③victim 내용 불변 검증. PASS.
- **T20 (외부 symlink)**: repo 밖 outside 파일 향하는 symlink → `TARGET_OUTSIDE_REPO_ROOT` fail + symlink 미삭제 + 외부파일 불변 검증. PASS.
- 기존 T17/T17b/T18 및 T01~T15 전부 유지.

## 검증 결과
1. **pytest**: `tests/regression/test_replacement_pr_runner_2510.py` → **24 passed / 0 failed** (0.16s, 격리 worktree 실행).
2. **canonical 2파일 size/sha 불변** (pytest 전/후 동일, 0바이트 미발생):
   - `utils/replacement_pr_runner.py`: BEFORE=AFTER `size=33557 sha256=95809c89b2f8ba90afb2de98b30d49ceb5d2f011fd6a3345256f907f8556c3b6`
   - `tests/regression/test_replacement_pr_runner_2510.py`: BEFORE=AFTER `size=24403 sha256=57ebdc51bde574d23322549feb9341c25af1aae550aeac9e5093f5dbf3f3d737`
   - 테스트는 tmp_path fixture로 격리 실행 → canonical 무접촉 확인.
3. **diff 2파일뿐**: `git diff --stat` → `test_...py | 68 ++`, `replacement_pr_runner.py | 4 ++` (2 files, 72 insertions). merge_queue_executor·real_merge_hooks·forbidden 무접촉.
4. WORKSPACE fallback(`cwd = repo_dir or str(WORKSPACE)`) 무변경 확인.

## L1 스모크테스트 결과
- 서버 재시작: 해당없음 (순수 유틸 함수 microfix, 서버 미관련)
- API 응답 확인: 해당없음 (API 아님)
- 스크린샷: 해당없음 (프론트 아님)
- **실동작 검증**: 격리 worktree pytest 24 PASS — T19가 실제 symlink 파일시스템 조작(symlink 생성→transplant→regular file 교체→victim 불변)을 실동작으로 검증. pytest 실행 전/후 canonical size/sha 불변으로 부작용 없음 증명.

## 머지 판단
- **머지 필요**: No (same-PR microfix — 신규 PR·merge 금지, merge_policy=none)
- **브랜치**: task/task-2783-dev3 (head `22bec5f5`, 이전 `1dc47e10`)
- **워크트리 경로**: /home/jay/workspace/.worktrees/task-2783-dev3
- **PR 갱신**: `git push origin task/task-2783-dev3` → PR #262 head 갱신 (1dc47e10..22bec5f5). 신규 PR 생성 안 함.
- **머지 의견**: Gemini symlink HIGH 해소 커밋을 PR #262 head에 추가 완료. Gemini 재리뷰 후 High 0건 확인 시 머지 대상. 본 task는 코드 수정·push까지만 수행(merge 금지 준수).

## 종료 검증 함정 인지
봇 자신의 finish-task는 canonical(미merge) 경로로 QC full-suite를 돌려 canonical 2파일을 truncate할 수 있음(정상 예상).
수정본 효과는 **worktree isolated pytest(24 PASS) + canonical size/sha 불변**으로 증명 완료. 완료해도 구현 후보(ACTIVE=false).

## 모델 사용 기록
- 엔키(백엔드): sonnet — Guard 2 코드 삽입 + T19/T20 회귀 테스트 작성. (정밀 로직 수정이라 haiku 미사용, sonnet 적정)
- 팀장(마르둑, Opus): 설계·검증·커밋·push만 수행 (직접 코딩 없음).

## 금지 준수 확인
신규 PR ✗ / merge ✗ / merge_queue_executor·real_merge_hooks 접촉 ✗ / WORKSPACE fallback 손대기 ✗ / ci.yml·ruleset ✗ / thread 임의 resolve ✗ / 후속 task dispatch ✗ — 전부 미수행(준수).

## 발견 이슈 및 해결
- Pyright 경고(test 파일 미사용 import, runner의 `branch` 미사용)는 **이번 수정 이전부터 존재하던 기존 사항**으로 이번 diff와 무관(추가 라인 아님). 범위 밖(2파일 제한·기존 로직 무변경 원칙)이라 미수정. 신규 삽입 코드는 경고 0건.
