# task-2478 보고서 — taskctl pr-open blast_radius 오류 fix

- 작성: 다그다(개발3팀장), 2026-05-07
- 팀: dev3-team (루, 모리건, 다그다)
- 브랜치: `task/task-2478-dev3`
- PR: https://github.com/Jeon-Jonghyuk/dev_workspace/pull/41

## 1. Situation
`taskctl pr-open` 자동 PR 생성 경로에서 `worktree_manager._get_blast_radius_summary`가 다파일 변경 시 `'list' object has no attribute 'get'` AttributeError를 일으켜 blast_radius 요약이 PR body에 누락되는 신뢰성 저하 발생 (task-2476 §인프라 이슈에서 보고).

## 2. Complication
- `scripts/ast_dependency_map.py:620`은 변경 파일 1개면 dict, 여러 개면 **list** 반환
- 또한 단일 dict도 `{"changed_file": ..., "blast_radius": {"direct_importers": [...]}}` 중첩 구조인데 기존 코드는 평탄 키(`data.get("direct_importers")`)로 접근
- 두 가지 결함:
  1. list 입력 → `.get()` AttributeError (캐치되어 silent failure)
  2. 단일 dict 입력 → `blast_radius` 중첩 누락으로 항상 빈 list 반환
- 결과: blast_radius 마크다운이 항상 의미 있는 데이터 없이 비어 있거나 silent fail

## 3. Question
list/dict/중첩/legacy 평탄 dict 모든 변형 입력에서 안전 동작하는 parser를 어떻게 구성하고, backward compat 유지하며 회귀 테스트로 보장할 것인가?

## 4. Answer (수정 내역)

### blast_radius 오류 재현 (회장 §6 #1)
```python
# AST 출력 (다파일 list)
[{"changed_file": "src/foo.py", "blast_radius": {...}},
 {"changed_file": "src/bar.py", "blast_radius": {...}}]

# 기존 코드(scripts/worktree_manager.py:455-457)
data = json.loads(ast_result.stdout)
direct_importers = data.get("direct_importers", [])  # ← AttributeError: 'list' object has no attribute 'get'
test_files = data.get("test_files", [])
```

### 원인 (회장 §6 #2)
- 코드 위치: `scripts/worktree_manager.py:455-457` (수정 전 SHA `75cb4734`)
- 타입 추론: `ast_dependency_map.analyze()` → `results: List[dict]` → `main()`에서 `output = results[0] if len(results) == 1 else results` (단일/다중 분기)
- 기존 parser는 단일 분기(dict) 가정 + 평탄 키 접근 가정 → 양쪽 모두 어긋남

### 수정 파일/라인 (회장 §6 #3) — parser만
1. **신규**: `utils/blast_radius_parser.py` (1676 bytes)
   - `parse_blast_radius(data: Any) -> tuple[list[str], list[str]]`
   - list/dict 분기 → `blast_radius` 중첩 우선, fallback으로 legacy 평탄 schema
   - 중복 제거(입력 순서 유지), None/비-list 안전 처리
2. **수정**: `scripts/worktree_manager.py`
   - 모듈 상단(line 27-30)에 `parse_blast_radius` import (`Path(__file__).resolve().parents[1]` workspace root sys.path 추가)
   - `_get_blast_radius_summary` 내부(현재 line 463-464)는 `parse_blast_radius(data)` 한 줄로 위임
   - Gemini medium 권고 수용: 함수 내 sys.path 조작 → 모듈 상단 1회 실행으로 이동
3. **신규 테스트**:
   - `tests/scripts/test_blast_radius_parser.py` (10 cases)
   - `tests/scripts/test_taskctl_pr_open_blast_radius.py` (4 cases)

### Backward compatibility 검증 (회장 §6 #4)
- `scripts/tests/test_ast_integration.py` 기존 15케이스 PASS 유지 (legacy 평탄 dict mock도 새 parser가 fallback으로 처리)
- 호출 시그니처 `_get_blast_radius_summary(project_path, branch, main_branch) -> str` 불변

### 단위 테스트 코드 + 실행 로그 (회장 §6 #5)
```bash
$ WORKSPACE_ROOT=/home/jay/workspace/.worktrees/task-2478-dev3 \
    python3 -m pytest tests/scripts/test_blast_radius_parser.py \
                      tests/scripts/test_taskctl_pr_open_blast_radius.py \
                      scripts/tests/test_ast_integration.py
============================== 29 passed in 0.16s ==============================
```

테스트 커버리지:
- list 입력 집계 (다파일) — 핵심 회귀
- list 중복 제거(순서 유지)
- dict + blast_radius 중첩 (실제 단일파일 출력 schema)
- legacy 평탄 dict (backward compat)
- 빈 list / 빈 dict
- malformed: blast_radius 값이 dict 아님 → legacy fallback
- list 내 dict 아닌 항목 skip (str/int/None)
- None / 비-list 내부 값 안전 처리

### dry-run / smoke test 결과 (회장 §6 #6)
실제 list 입력 smoke (mock subprocess) 실행:
```
===== blast_radius output =====
## Blast Radius
- 변경 파일: 2개 (src/foo.py, src/bar.py)
- 영향받는 파일: 2개 (src/baz.py, src/qux.py)
- 관련 테스트: 2개 (tests/test_foo.py, tests/test_bar.py)
PASS — list input no longer raises AttributeError
```

`taskctl.py pr-open --auto` 직접 dry-run은 task state machine 진입 조건(COMMITTED/RUNNING) 미충족으로 즉시 abort. 대신 worktree_manager._get_blast_radius_summary 직접 호출로 본질 동일 검증 완료(위 로그). PR 본문에 blast_radius가 누락되는 silent failure는 본 fix로 제거됨.

### PR 번호 + merge SHA (회장 §6 #7)
- PR #41: https://github.com/Jeon-Jonghyuk/dev_workspace/pull/41
- HEAD SHA: `841a51df` (Gemini medium 수용 커밋)
- merge SHA: **미머지 (base branch policy로 BLOCKED)**

### task-2477 chain 무영향 검증 (회장 §6 #8)
- `task/task-2477-*` 브랜치/PR 무영향 (독립 브랜치 `task/task-2478-dev3`에서만 작업)
- 수정 파일은 `task-2477` forbidden_paths(`server/conftest.py`, `server/tests/conftest.py`, `server/tests/test_main.py`)와 교차 0건

## 5. 머지 판단

- **머지 필요**: Yes (silent failure fix)
- **브랜치**: `task/task-2478-dev3` (HEAD `841a51df`, 4 commits)
- **워크트리 경로**: `/home/jay/workspace/.worktrees/task-2478-dev3`
- **머지 의견**: CI 11/11 SUCCESS, Gemini High 0건/Medium 1건 수용 완료. mergeStateStatus=BLOCKED (base branch policy `prohibits the merge`). 회장 절대기준 "admin override 금지"로 본 봇은 머지 강행 안 함. **아누(개발실장) 또는 회장 권한 머지 필요**.

## 6. CI 결과 (PR #41 latest run)
| Check | Status |
| --- | --- |
| cancel-kill-switch | SUCCESS |
| taskctl-state-guard (push) | SUCCESS |
| taskctl-state-guard (PR) | SUCCESS |
| qc-check | SUCCESS |
| hidden-path-audit | SUCCESS |
| lock-in-check | SUCCESS |
| merge-safety-check | SUCCESS |
| gemini-review-gate | SUCCESS |
| phase3-merge-gate | SUCCESS |
| ci/guard | SUCCESS |
| guard | SUCCESS |
| **합계** | **11/11 PASS** |

## 7. Gemini 리뷰 대응
- High severity: 0건
- Medium severity: 1건 — 함수 내부 sys.path 동적 조작 + import → 모듈 상단 이동 권고
  - **수용**: commit `841a51df`에서 모듈 상단으로 이동 (line 27-30). `noqa: E402` 1건만 남김(import 전 sys.path 조작 필요).
- Low/Comment: 1건 (요약 코멘트)

## 8. L1 스모크테스트 결과
- 서버 재시작: 해당없음 (subprocess 정제 작업, 서버 미관여)
- API 응답 확인: 해당없음 (worktree_manager 내부 함수)
- 스크린샷: 해당없음 (CLI 내부 로직)
- **실제 동작 검증**: list 입력 smoke (mock subprocess) — `## Blast Radius` 마크다운 정상 생성, AttributeError 미발생 (위 §4 dry-run 로그)

## 9. 발견 이슈 및 해결
| 이슈 | 상태 | 처리 |
| --- | --- | --- |
| 메인 workspace에 모리건이 별도 복사한 dirty 변경(staged) | ✅ 해결 | 메인 git restore + rm으로 정리, 워크트리만 단일 소스 truth로 유지 |
| Pyright `utils.blast_radius_parser` import 미해결 (3건) | ✅ 해결 | `# type: ignore[import-not-found]` + 모듈 상단 sys.path 조작 |
| pytest `ModuleNotFoundError: No module named 'utils.blast_radius_parser'` (워크트리에서) | ✅ 해결 | `tests/conftest.py`가 `WORKSPACE_ROOT` env(default 메인 workspace)로 sys.path 주입 → `WORKSPACE_ROOT=<워크트리>` 지정으로 회피. 머지 후엔 메인에도 utils 파일이 들어가 자연 해소 |
| `_tf` Pyright unused 진단 | ✅ 해결 | `_, _ = ...` 단일 underscore로 변경 |
| `taskctl pr-open --auto` task state machine 미진입 | 🟡 우회 | task lifecycle COMMITTED/RUNNING state 진입을 위한 setup이 별도 필요. 본 task 본질(parser fix)은 단위/통합 테스트 + smoke test로 검증 완료 |
| Gemini review 5분 자동 트리거 미동작 (1차) | ✅ 해결 | PR `/gemini review` 코멘트로 수동 트리거 + 새 push 시 정상 작동 |
| mergeStateStatus=BLOCKED (base branch policy) | 🚫 범위 외 | 본 task가 admin override를 못함(회장 절대기준). 아누/회장 권한 머지 필요. CI/리뷰 게이트는 모두 PASS |

## 10. 회장 완료 조건 (5건) 대비표
| # | 조건 | 상태 |
| --- | --- | --- |
| 1 | blast_radius list 입력에서 오류 미발생 | ✅ (4 회귀 테스트 + smoke test PASS) |
| 2 | 기존 dict 입력도 PASS | ✅ (test_ast_integration 15/15 PASS, dict + blast_radius 중첩 + legacy 평탄 모두) |
| 3 | `taskctl pr-open` dry-run PASS | 🟡 (taskctl state machine 진입 setup 별도 필요. parser 본질은 worktree_manager 직접 smoke로 검증) |
| 4 | 단위 테스트 PASS | ✅ (29/29 PASS — 신규 14 + 기존 15) |
| 5 | PR 생성 및 CI PASS | ✅ (PR #41, CI 11/11 PASS) / 머지 자체는 base policy로 차단 |

## 11. 모델 사용 기록
- 다그다(팀장): Opus 4.7 — 설계/분배/검토/통합/Pyright 진단 fix(직접 수정, 단순 회피 코드)
- 루(백엔드): Sonnet 4.6 — parser 신설 + worktree_manager 위임
- 모리건(테스터): Sonnet 4.6 — 단위/통합 테스트 14케이스
- haiku 미사용

## 12. 변경 파일 목록 (PR #41 diff)
- `utils/blast_radius_parser.py` (신규, 38 lines)
- `scripts/worktree_manager.py` (modified, 8 lines diff)
- `tests/scripts/test_blast_radius_parser.py` (신규, 96 lines)
- `tests/scripts/test_taskctl_pr_open_blast_radius.py` (신규, 91 lines)

## 13. 커밋 SHA
1. `89f95616` — 루: blast_radius parser 유틸 신설
2. `b98842c2` — 루: worktree_manager → blast_radius_parser 위임
3. `2cacc968` — 모리건: 단위/통합 테스트 추가
4. `5175b60e` — 다그다: Pyright 진단 fix (sys.path 워크트리 root + type ignore + _ unused)
5. `5bf0006e` — main sync merge
6. `841a51df` — 다그다: parse_blast_radius import 모듈 상단 이동 (Gemini medium 수용)

## 14. 후속 권고
- 아누 또는 회장이 PR #41 머지 (base policy 차단 해소). 본 task 본질은 fix 완료 + 검증 완료.
- task-2477 chain 머지 차단 무 (독립 브랜치).

## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회

