# P1 설계 packet v2 — WORKSPACE fallback truncate 근절 (설계만·코드 dispatch 금지)

상태명: `WORKSPACE_FALLBACK_TRUNCATE_P1_DESIGN_V2_ACTIVE_FALSE`
작성: ANU 직접 (2026-07-02 KST). **설계 v2 — 코드 dispatch/PR/merge/ruleset·ci.yml/callback·finish-task/2782·2781 전부 금지.** 근거: 회장 GPT(설계 packet 방향성 수용 85~90점·**code dispatch pending design v2**·4 필수 보완). 선행 v1: `p1_workspace_fallback_design_packet_260702.md`. main=e9e5b34d.

## ★ v2 핵심 교정 (GPT 지적): "repo_dir 필수화"만으론 부족
`repo_dir=None` 금지만으로는 누군가 `ReplacementPRRunner(repo_dir=str(WORKSPACE))`로 우회하면 **여전히 canonical에 write**. 진짜 목표 = **replacement/transplant write 경로가 canonical WORKSPACE에 직접 write 못 하게** 하는 것.

## 1. safe repo_dir invariant (P1 핵심 정의)
단일 헬퍼 `_resolve_safe_repo_dir(repo_dir, *, allow_workspace=False) -> str` 도입, 모든 replacement write/mutate 경로가 통과:
- `repo_dir is None` → **`MISSING_REPO_DIR` fail-closed**.
- `Path(repo_dir).resolve() == WORKSPACE.resolve()` → **`WORKSPACE_WRITE_FORBIDDEN` fail-closed** (명시돼도 canonical이면 거부).
- (선택) `WORKSPACE.resolve() in Path(repo_dir).resolve().parents` 등 canonical 하위/동일 판정 강화 검토(단순 동일부터).
- 통과 시 그 repo_dir 반환.
- **예외**: `allow_workspace=True`는 **CLI 수동 모드에서만**(replacement_pr_runner.py:723 CLI가 명시적으로 opt-in). **automation/merge_queue/real-merge dry-run 경로는 allow_workspace 절대 금지**.
- ★ **fail 방식 = RuntimeError raise**(§6 회귀 보존 이유).

## 2. fallback 사이트 5곳 — 이번 P1 적용 범위 (확정)
전부 `cwd = repo_dir or str(WORKSPACE)`:
- **185 `transplant_expected_files`** — 파일 write(write_text/mkdir) = **THE truncate 경로**. → invariant 적용 **필수**.
- **231 `commit_local`** — git commit(canonical에 커밋 생성 위험). → 적용 **필수**.
- **250 `push_branch`** — git push(canonical 브랜치 push 위험). → 적용 **필수**.
- **290 `precheck_local_replacement_diff`** — git diff(read-only, 단 canonical 기준 오판). → 적용 **권장**.
- **316 `assert_clean_working_tree`** — git status(read-only 체크). → 적용 **권장**.
- **결정**: **5곳 전부 동일 safe repo_dir invariant로 묶는다**(write/mutate 3 필수 + read 2 일관성). 이유: execute() 한 흐름에서 self.repo_dir을 공유하므로 부분만 고치면 남는 위험(commit/push가 canonical에)이 큼. 66번(`_run` 기본 cwd)은 범용 헬퍼라 이번 범위 제외(호출자가 cwd 명시) — **남는 위험 명시**: `_run` 직접 호출 시 canonical 가능하나 replacement write 경로 아님.

## 3. merge_queue_executor.py:1745 repo_dir 출처 (2안 + 권고)
현황: W2 경로(1255~1291) `ctx.replacement_runner.execute()`. runner=1745서 **repo_dir 없이** 생성. ctx에 **기존 안전 sandbox path 없음**. real-merge는 **dormant(ACTIVE=false)**.
- **옵션 (b) 권장 — 무주입 fail-closed**: 1745 **미변경**(repo_dir 없음 유지) → execute()가 §1 invariant로 `MISSING_REPO_DIR` **raise** → W2 `except`(1291)가 잡아 **기존 fallthrough(DIFF_CONTAMINATION_REPLACEMENT) 보존**. 즉 dormant W2가 **canonical write 대신 안전 fail-closed + 기존 동작**. **merge_queue_executor.py 수정 불필요 가능** → scope 축소·회귀 최소.
- **옵션 (a) — tmp clone 주입**: W2를 실기능화하려면 1745(또는 W2 setup)가 **tmp clone dir**(예: `git worktree add`/`git clone --local`)을 repo_dir로 주입 + **cleanup 책임 = merge_queue(provisioner)**. 단 real-merge 활성화 안건이므로 P1 범위 밖.
- **권고**: **(b)** — P1은 "canonical 파괴 차단"이 목표. dormant W2는 fail-closed로 충분. tmp clone provisioning은 **real-merge 활성화 시 별도**. → **이번 P1 expected_files에서 merge_queue_executor.py는 (b) 채택 시 무접촉 가능**(단 W2 fail-closed 회귀 테스트는 필요 → §5).

## 4. expected_files 후보 (v2)
- `utils/replacement_pr_runner.py` (safe repo_dir invariant + 5 사이트 + CLI:723 allow_workspace opt-in) — **필수**
- `tests/regression/test_replacement_pr_runner_2510.py` (신규 회귀) — **필수**
- `utils/merge_queue_executor.py` — **(b) 채택 시 무접촉** / (a) 채택 시만 포함. **기본 무접촉 권고**.
- → 옵션 (b)면 expected_files 2파일로 **PR#262급 크기**(범위 재축소). (a)면 3파일+real-merge 축.

## 5. 테스트 계획 (v2, GPT 요구 반영)
1. `repo_dir=None` transplant/commit/push → `MISSING_REPO_DIR` fail + canonical 무접촉.
2. **`repo_dir=canonical WORKSPACE` → `WORKSPACE_WRITE_FORBIDDEN` fail** (명시 우회 차단) + canonical 무접촉.
3. `repo_dir=tmp/sandbox` → 정상 동작(transplant/commit/push).
4. **merge_queue W2 경로**: replacement_runner(repo_dir 없음)로 execute → invariant raise → W2 except → **기존 fallthrough(DIFF_CONTAMINATION_REPLACEMENT) 보존**(회귀 0, canonical 무접촉).
5. 정상 empty-file PR(rc0+empty, tmp repo_dir) → 허용 유지(EMPTY_OVERWRITE 제거 보존).
6. 기존 T17/T17b/T18/T19/T20(symlink/GIT_SHOW_FAILED/TARGET_OUTSIDE) 회귀 0.
7. CLI allow_workspace opt-in만 WORKSPACE 허용(자동화 경로는 불가) 검증.
8. fresh worktree: canonical 2파일 size/sha 전후 불변.

## 6. 기존 guard/회귀 보존 (v2)
- symlink guard / GIT_SHOW_FAILED / TARGET_OUTSIDE_REPO_ROOT **순서·동작 불변**(P1은 cwd 결정 앞단만 강화).
- rc0+empty 정상 허용 보존.
- ★ **invariant fail = raise**(return success=False 아님): W2 `except`가 잡아 기존 fallthrough 보존 → dormant real-merge 경로 **회귀 0**. (success=False면 W2가 Critical escalation 분기(1275)로 빠져 동작 변경됨 → 금지.)

## 7. dormant real-merge 영향 검증
- 옵션 (b): W2 fail-closed → except fallthrough. real 실행/활성화 **절대 0**(mock/tmp 테스트만). 활성화 시 tmp clone provisioning은 별도 안건.

## 8. 실패 시 STOP 조건
- invariant가 예상외 호출자(추가 grep 누락)를 깨면 STOP_REPORT.
- 옵션 (b)인데 W2 except fallthrough가 회귀 테스트서 재현 안 되면 STOP_REPORT(옵션 a 재검토).
- merge_queue 수정이 필요해지면(=(a) 강제) scope 확장이므로 STOP_REPORT 후 재승인.
- PR#262 이후 main truncate 재현은 **root-cause confirmed로 과장 금지**, P1 실증 근거로만.

## 9. 예상 diff / 위험도
- 옵션 (b): replacement_pr_runner.py(invariant 헬퍼 + 5 사이트 치환 + CLI opt-in, ~40~60줄) + test(회귀 8케이스). **merge_queue 무접촉** → **PR#262급 범위·위험 중하**. 별도 PR.
- 옵션 (a): + merge_queue_executor.py(tmp clone provision+cleanup) → 위험 중상(real-merge 축·Gemini 신규 finding). **비권고(활성화 안건과 결합)**.

## 보고 순서 / 다음
1. (본 v2) 설계 packet
2. 예상 diff/expected_files(§4·9)
3. 위험도·STOP(§8·9)
→ **회장/GPT 승인 후에만** P1 코드 task dispatch. task-2782/2781은 그 이후.

## 금지 (현 단계)
P1 코드 dispatch·신규 PR·merge·ruleset/ci.yml·callback/finish-task 축·real-merge 활성화·2782/2781 — 전부 미실행. **설계 v2까지만.**
