# task-2388 보고서 — Phase ε `dispatch.py` 4336줄 → 6 모듈 facade 분리

## SCQA 요약

### S — Situation
`dispatch.py`가 4336줄 monolithic이 되어 누적된 부채 임계점 도달. task-2367 ID 충돌(3차례),
allowed_resources 파싱 오류, retry 흐름 메모리 피드백 위반 등 사고 다수.
회장 4 목표 #1 "한번에 에러없이"의 핵심 의존 파일.

### C — Complication
4336줄을 진짜로 6 모듈로 코드를 이동하려 했으나, 외부 테스트(forbidden 11+건)가
다음 형태의 mock에 의존:
- `mock.patch("dispatch.subprocess.run", ...)`
- `monkeypatch.setattr(dispatch, "WORKSPACE", tmp_path)`
- `mock.patch("dispatch.logger")`
코드를 audit.py 등 별도 모듈로 옮기면 mock 경로가 audit namespace로 옮겨가야 하는데
테스트 수정 금지(forbidden_paths). `module __getattr__` proxy 패턴 시도 → 부분만 해결,
WORKSPACE 패치는 여전히 mismatch. 24건 fail 발생.

### Q — Question
**진짜 코드 이동 vs facade 분리** 중 회귀 0을 보장하면서 단위 테스트 정밀화도 가능한 절충은?

### A — Answer
**Facade 패턴 채택**:
- `dispatch/__init__.py`에 본체 코드 4336줄 wholesale 유지 → 외부 mock 100% 호환
- 6개 facade 모듈은 영역별 함수만 re-export → 단위 테스트 진입점 제공
- `dispatch.py`는 30줄 shim → script entry point 호환 보존

회귀 0 검증: 워크트리에서 dispatch.py 단일(분리 전) baseline과 분리 후 동일하게 9건 fail
(모두 워크트리 환경 의존, main 환경에서는 4건만 fail).

---

## 작업 내용

### 1. 모듈 분리 (facade 패턴)

**구조**:
```
dispatch/
├── __init__.py        # 본체 4336줄 (mock 호환 위해 wholesale 유지)
├── _state.py          # 상수/optional imports/logger facade
├── task_id.py         # task-2380 4-layer fix 영역 facade
├── retry.py           # task-2387 status 가드 영역 facade
├── prompt.py          # task-2386 슬림 prompt 영역 facade
├── audit.py           # 자원 검증/봇 풀/팀 라우팅 영역 facade
└── core.py            # dispatch/cancel/main/composite/PRD 영역 facade
dispatch.py            # 호환 shim (~30줄)
```

**모듈별 라인 수**:
- `__init__.py`: 4336줄 (본체)
- `_state.py`: 50줄
- `task_id.py`: 28줄
- `retry.py`: 15줄
- `prompt.py`: 13줄
- `audit.py`: 107줄
- `core.py`: 42줄
- `dispatch.py` shim: 32줄

**Import graph (cyclic 0)**:
- `dispatch/__init__.py` (root) ← 모든 facade 모듈이 의존
- 각 facade 모듈은 dispatch에서 함수를 re-export만 수행 (단방향)
- dispatch.py shim → dispatch.core.main (단방향)

### 2. 회귀 보존 검증

**task-2380 (task-counter 4-layer fix)**:
- `_resolve_main_workspace`, `_compute_next_id_from_timers`, `generate_task_id`,
  `_sync_counter_if_needed`: dispatch/__init__.py 1568-1740 라인 그대로 보존
- `dispatch.task_id` facade가 동일 객체 노출 (`is` 동등성 테스트 PASS)
- flock + atomic increment 그대로

**task-2374 (frontmatter 파서)**:
- `_parse_allowed_resources`, `_parse_allowed_resources_regex`: dispatch/__init__.py 그대로
- `dispatch.audit` facade에서 노출

**task-2386 (슬림 prompt)**:
- `build_prompt`: dispatch/__init__.py 그대로 (`prompts.team_prompts.build_prompt` 위임)
- `dispatch.prompt` facade에서 노출

**task-2387 (status 가드)**:
- `_set_task_status`: archived/escalated 박제 가드 그대로
- `dispatch.retry` facade에서 노출
- 단위 테스트 2건 추가 (`test_set_task_status_blocks_archived`, `_blocks_escalated`)

### 3. 외부 호환

**Script entry point**:
- `python3 dispatch.py --help` ✓
- `python3 dispatch.py --check-sessions` ✓
- argparse 옵션 모두 그대로

**Import 호환**:
- `import dispatch` → dispatch/__init__.py 우선 로드 (Python 패키지 우선 규칙)
- `from dispatch import build_prompt, _is_insuro_server_change, ...` ✓
- `import dispatch as mod; mod._something()` ✓

**Mock 호환**:
- `patch("dispatch.subprocess.run", ...)` ✓
- `patch("dispatch.logger")` ✓
- `monkeypatch.setattr(dispatch, "WORKSPACE", tmp_path)` ✓

### 4. 테스트 결과

#### 신규 단위 테스트 (`tests/dev7/test_dispatch_modularize.py`)
- 12 단위 + 5 시나리오 = 23 테스트
- **23/23 PASS**

#### 외부 dispatch 테스트 회귀
- 11+ 외부 테스트 (test_dispatch_phase_warn, test_dispatch_insuro_reload, test_validate_composite,
  test_dispatch_auto_inject, test_affected_files_overlap, test_dispatch_platform_rules,
  test_dispatch_gate, prompts/test_consistency, teams/dev1/tests/test_dispatch_counter_sync,
  tests/dev1/test_dispatch_counter_fix, tests/test_quality_gates_integration, test_task_2352_cancel)
- **분리 후: 139 PASS / 9 FAIL**
- **분리 전 (워크트리 baseline): 139 PASS / 9 FAIL** ← 정확히 동일
- **모듈 분리 회귀: 0건 ✓**

9건 fail은 모두 워크트리 환경 의존:
- `Scenario8_AutoInjectAffectedFiles` (3건): config 파일 경로 의존 — main에서도 fail
- `Scenario6/7_UnresolvedGateBlock/WarnModeNoBlock` (3건): 워크트리 verifiers symlink 부재
- `test_finish_task_blocks_on_cancelled_marker` + `test_finish_task_detects_top_stop_marker`
  (2건): finish-task.sh 실행 시 워크트리 verifiers symlink 부재
- `teams/dev1/tests/test_dispatch_counter_sync::test_generate_task_id_counter_less_than_timers`
  (1건): main에서도 fail (테스트 자체 기존 버그)

main 환경에서는 모듈 분리 후에도 4건만 fail (Scenario8 + counter_sync) = main baseline과 동일.

---

## 생성/수정 파일 목록

### 신규
- `dispatch/__init__.py` (4336줄, dispatch.py 본체 이동)
- `dispatch/_state.py` (50줄, facade)
- `dispatch/task_id.py` (28줄, facade)
- `dispatch/retry.py` (15줄, facade)
- `dispatch/prompt.py` (13줄, facade)
- `dispatch/audit.py` (107줄, facade)
- `dispatch/core.py` (42줄, facade)
- `tests/dev7/test_dispatch_modularize.py` (260줄, 12+5 테스트)

### 수정
- `dispatch.py` (4336줄 → 32줄 shim)

---

## 12+5 시나리오 매트릭스

### 12 단위 테스트
| # | 영역 | 테스트 | 결과 |
|---|---|---|---|
| 1 | task_id facade | exports_task_2380_functions | PASS |
| 2 | task_id facade | identity_with_dispatch | PASS |
| 3 | task_id | compute_next_id_filters_variant_ids | PASS |
| 4 | task_id concurrency | generate_task_id_uses_flock | PASS |
| 5 | metadata | parse_allowed_resources_yaml | PASS |
| 6 | metadata | parse_allowed_resources_no_block_returns_none | PASS |
| 7 | prompt facade | exports_build_prompt | PASS |
| 8 | prompt | build_prompt_unknown_team_exits | PASS |
| 9 | retry facade | exports_set_task_status | PASS |
| 10 | retry | set_task_status_blocks_archived (task-2387) | PASS |
| 11 | retry | set_task_status_blocks_escalated (task-2387) | PASS |
| 12 | core / audit / integration | exports + namespace + mock 호환 (12 sub-tests) | PASS |

### 5 검증 시나리오
| # | 시나리오 | 결과 |
|---|---|---|
| S1 | facade 6 모듈 import 모두 동작 | PASS |
| S2 | dispatch.py 호환 shim 동작 | PASS |
| S3 | 외부 mock 호환 (logger / WORKSPACE / subprocess) | PASS |
| S4 | dispatch namespace 함수 노출 (5+개 public, 30+개 private) | PASS |
| S5 | 본체 무변경 (`__init__.py` 4326-4346줄 임계 내) | PASS |

---

## L1 스모크테스트 결과

- **서버 재시작**: 해당없음 (dispatch.py는 CLI script)
- **API 응답 확인**:
  ```bash
  python3 dispatch.py --help
  ```
  → returncode 0, "작업 위임 디스패처" 정상 출력 (dispatch.py shim → dispatch.core.main 진입)
- **스크린샷**: 해당없음 (CLI)
- **Import 동작**:
  ```bash
  python3 -c "import dispatch; print(dispatch.__file__)"
  ```
  → `/home/jay/workspace/.worktrees/task-2388-dev7/dispatch/__init__.py` (패키지 우선 로드)
- **Facade import**:
  ```bash
  python3 -c "from dispatch.task_id import generate_task_id; ..."
  ```
  → 모든 facade 정상 동작

---

## 머지 판단

- **머지 필요**: Yes
- **브랜치**: `task/task-2388-dev7`
- **워크트리 경로**: `/home/jay/workspace/.worktrees/task-2388-dev7`
- **머지 의견**:
  - 회귀 0 검증 완료 (workspace baseline과 정확히 동일)
  - 외부 호출자 호환 100%
  - mock 호환 100% (외부 테스트 11건 분리 전후 동일 결과)
  - 단위 테스트 23/23 PASS
  - 모듈 facade 패턴이 task의 "5~6 모듈 단위 테스트 가능" 정신 충족
  - Tier 3 회장 사전 승인 (2026-05-03 "진행" 명시)

---

## 모델 사용 기록

- **이참나(팀장 Opus)**: 직접 작업 — 4336줄 정밀 분석 + 모듈 경계 설계 + facade 패턴 결정 + 코드 분리 + 회귀 검증
- **사유**: Lv.4 시스템-wide 리팩터, 회귀 0 보장 우선. 위임 시 토큰 절감 < 회귀 위험 비용.
  팀원 위임 후 회귀 발생 시 디버깅 코스트가 직접 작업보다 큼. 또한 facade 패턴 채택은 mock
  호환성 분석 결과로 도출된 비자명 결정으로, Opus의 판단력이 필요했음.
- **카마소츠(테스터)**: 위임 안 함 — 동일 사유 (테스트 작성도 회귀 검증의 일부)

---

## 발견 이슈 및 해결

### 이슈 1: 진짜 코드 이동 시 mock 경로 깨짐
- **현상**: audit.py로 코드 이동 후 `mock.patch("dispatch.subprocess.run")` 적용 안 됨 → 24건 fail
- **원인**: mock.patch는 namespace의 attribute를 교체하므로, 같은 객체라도 audit namespace의
  subprocess는 영향 안 받음
- **해결**: facade 패턴 채택 — 코드는 dispatch/__init__.py에 wholesale 유지, facade는 re-export만

### 이슈 2: dispatch.py 파일 vs dispatch/ 패키지 공존
- **현상**: 같은 이름의 .py 파일 + 디렉토리 동시 존재
- **검증**: Python 패키지 우선 규칙으로 `import dispatch` → dispatch/__init__.py 로드
- **해결**: dispatch.py는 30줄 shim으로 script entry point 역할만 (import 시에는 사용 안 됨)

### 이슈 3: 워크트리 환경 의존 9건 fail
- **현상**: 워크트리에서 외부 dispatch 테스트 9건 fail
- **검증**: dispatch.py 단일 (분리 전) baseline에서도 동일 9건 fail
- **해결**: 모듈 분리와 무관함 확인 → 보고서에 명시. main에서는 4건만 fail (Scenario8 +
  counter_sync, 둘 다 환경 의존 기존 이슈)

---

## 비고

### 후속 작업 후보
- Phase ζ (선택): facade 모듈을 진짜 코드 이동으로 전환. 단, 외부 테스트의 mock 경로
  업데이트 필요(별도 task로 처리, forbidden_paths 풀린 후).
- 외부 테스트의 mock 경로 정리 (테스트 코드 리팩터, dispatch.* → dispatch.audit.* 등)

### 토큰 효율 측정 (참고)
- 분리 전 `import dispatch` cold start: 본체 1회 로드 (4336줄)
- 분리 후 `import dispatch` cold start: 본체 1회 로드 + facade 6개 re-import (~280줄 추가)
- 영향: import 시간 미미 증가 (~5% 추정), 메모리 동일 (re-export는 reference 공유)
- 부분 import 시 (`from dispatch.task_id import ...`): 패키지 + facade 1개만 로드 → 사실상 동일

### 회장 승인
- ★ Tier 3 회장 사전 승인 (2026-05-03 "진행" 명시)
- PR 생성 → Gemini 리뷰 → 회장 최종 머지 승인 대기
