# task-2757+2 — v2.13 gate forbidden/expected matching 구조 재설계 (B-1, 7파일 내부 함수 분리)

## ★ 회장 결정 (B-1 승인 — 2026-06-17)
PR #236 HIGH 2(`dispatch/v2_13_gate.py:179` substring 매칭) = 같은 matching 축 **2회차** → 미세패치 중단, **구조 재설계**. **B-1만 승인**(7파일 내부 함수 분리). **B-2 새 모듈 신설 불허**. 상태 `PR236_TO_R3_MATCHING_STRUCTURAL_REDESIGN_APPROVED_ACTIVE_FALSE`. ACTIVE=false.

## base / branch / PR
- base = clean origin/main `8420a763`. **fresh r3 branch `task/task-2757+2`**. 참조 = `origin/task/task-2757+1`(8e831279) 7파일.
- **PR #236 추가 push 금지**. branch push only(ANU가 fresh PR 생성·#236 close 통제). isolated worktree 진행.

## 핵심 재설계 (substring `in` 매칭 제거 — dispatch/v2_13_gate.py 내부 헬퍼 분리)
현재 L173-180: `if fp and _normalize(fp) in norm_haystack:` substring 매칭 → false pos/neg(forbidden `rm`이 "form"에 매칭). **substring `in` 매칭 전면 금지.**

### 1. forbidden_path 축 — 실제 touched_files 추출 후 glob 매칭
- diff 전체 문자열 substring 금지. diff의 `+++ b/<path>`/`--- a/<path>` 헤더(또는 tool_calls의 file path 필드)에서 **touched_files 집합 정확 추출**.
- `normalize_path()` 통과 후 forbidden_paths를 **fnmatch/glob**(`**/*.service` 등 기존 패턴 호환)로 touched_files 각각 매칭.

### 2. normalize_path() 헬퍼 (gate.py 내부)
- leading `./`·`a/`·`b/`(git diff prefix) 제거 · 중복 slash 축약 · OS separator → posix(`/`) 통일 · 소문자화는 경로엔 적용하지 말 것(대소문자 구분 파일시스템 — 경로는 형식만 정규화). 경로 **컴포넌트 단위** 비교(substring 아님).

### 3. forbidden_operation 축 — token/word-boundary exact 매칭
- 전체 문자열 substring 금지. tool_calls/diff를 **word-boundary 토큰화**(`\b` regex) 또는 shlex 명령 파싱 후 forbidden op를 **exact token / 명령 경계 매칭**. 기존 `_AXIS_PATTERNS` regex(word-aware)와 일관.

### 4. expected_files 비교 (MED #2, L472)
- `f not in expected_files` 직접 비교 → **같은 normalize_path() 통과 후 set membership**.

### 5. 단일 책임 분리 (7파일 내부, 새 파일 0)
- `_match_forbidden_paths(touched_files, forbidden_paths)` / `_match_forbidden_ops(tokens, forbidden_ops)` / `normalize_path(p)` 헬퍼로 `detect_axes`에서 분리. **전부 dispatch/v2_13_gate.py 내부 함수**.

## Medium 5 처리
- matching 축(L472 expected_files·L137 dead-code if/else·L32 redundant lower)은 재설계 시 **함께 닫음**.
- git subprocess perf x2(correction_budget.py:36 / migrate:37 `_resolve_root` 매 호출 git rev-parse) → **module-level 1회 cache**(함수 결과 캐싱)로 7파일 내부 동반 해결. 범위 커지면 보류 보고.

## 필수 테스트 (각각 추가)
- forbidden `rm`이 "form"/"warmup"에 **오매칭 안 함**
- forbidden path `api`가 "capital"/"rapid"에 **오매칭 안 함**
- `./dispatch/x.py`·`a/dispatch/x.py`·`b/dispatch/x.py`·`dispatch/x.py` **동일 취급**
- glob `**/*.service` 매칭
- `dispatch/x`가 `dispatch/xyz.py`에 **오매칭 안 함**(컴포넌트 경계)
- expected_files normalize set 비교
- 기존 122 테스트 유지

## 수정 범위 = 7파일 내부만
dispatch/v2_13_gate.py · dispatch/control_db_guard.py · scripts/correction_budget.py · scripts/promotion_guard.py · scripts/migrate_capability_matrix_v213.py · memory/schemas/goal_contract_schema.json · tests/regression/test_v213_phase1_gate_2757.py

## 금지 (위반 시 STOP_REPORT)
merge · admin override · ACTIVE=true · systemd enable/install · callback/wake launch · C(event strategy) · external probe/ANU-Control daemon 범위 확장 · 7파일 밖 수정 · **새 파일/새 모듈 추가** · substring `in` 매칭 잔존 · PR 생성/push to main.

## 중단 조건 (STOP_REPORT)
7파일 밖 수정 필요 · 새 파일 필요 · 같은 matching 축 HIGH 재발 · credential/permission/systemd/production/callback/wake/merge 축 터치 필요 · 구조 재설계가 7파일 내부로 안 닫힘.

## 산출물 / callback
`task/task-2757+2` branch commit + push(PR 0). 종료 전 ANU normal callback 필수 — ANU key `c119085addb0f8b7`(self-key 금지·UTF-8 wc -c ≤3900 bytes·envelope만: task_id/branch/head/result_path/sha256/collector_role=ANU/one-line).

## allowed_resources
```yaml
allowed_resources:
  paths:
    - "dispatch/v2_13_gate.py"
    - "dispatch/control_db_guard.py"
    - "scripts/correction_budget.py"
    - "scripts/promotion_guard.py"
    - "scripts/migrate_capability_matrix_v213.py"
    - "memory/schemas/goal_contract_schema.json"
    - "tests/regression/test_v213_phase1_gate_2757.py"
  forbidden_paths: ["**/*.service","**/*.path","dispatch/anu_pickup_driver.py","dispatch/anu_result_pickup_runner.py",".github/**"]
  commands: ["pytest","python3 -m py_compile","git"]
  merge_policy: "none"
  ttl_hours: 12
```
