# v2.13 gate forbidden/expected matching 구조 재설계 packet (260617)

상태: `PR236_R2_HOLD_INLINE_HIGH2_MATCHING_AXIS_REPEAT_ACTIVE_FALSE` · ACTIVE=false · merge 금지

## 1. 배경 — 같은 축 2회차 (미세패치 중단)
- r1(PR#235): forbidden op/path 매칭 `fp in haystack` **case-sensitive** → HIGH
- r2(PR#236): case-insensitive normalize로 고침 → 그러나 **substring `in` 매칭 방식 자체**가 결함으로 HIGH **재발**
- HIGH 2 (둘 다 `dispatch/v2_13_gate.py:179`, unresolved, resolved/stale 아님):
  - forbidden **operations** substring false positive/negative
  - forbidden **paths** substring false positive/negative
- 회장 doctrine 발동: **같은 forbidden/expected matching 축 2회차 → 미세패치 중단, 구조 분리/정밀 재설계**.

## 2. 근본 결함 (왜 미세패치로 안 닫히나)
현재(L173-180):
```python
if forbidden_paths:
    norm_haystack = _normalize(haystack)   # haystack = diff + tool_calls 전체를 1개 문자열로
    for fp in forbidden_paths:
        if fp and _normalize(fp) in norm_haystack:   # substring 매칭
            touched.add("forbidden_path"); break
```
구조적 문제 — case-insensitivity와 무관:
1. **경계 무시 substring**: forbidden `rm` → "form"/"warm"/"warmup"에 매칭(false positive). forbidden path `api` → "rapid"/"capital"에 매칭.
2. **diff 전체를 1 문자열로 평탄화** → 실제 touched file path가 아니라 코드 본문 텍스트·주석·문자열 리터럴까지 매칭 대상(false positive).
3. **경로 형식 차이로 false negative**: `./dispatch/x.py` vs `dispatch/x.py` vs `a/dispatch/x.py`(git diff prefix) 정규화 불일치.
4. forbidden **operation**과 forbidden **path**가 동일 substring 로직 공유 → 두 축 모두 같은 결함.

→ normalize/lower 추가, 키워드 보강 같은 **미세패치로는 substring 매칭의 false pos/neg가 구조적으로 남음**.

## 3. 구조 재설계 (정밀 매칭, 단일 책임 분리)
### 3.1 forbidden_path 축 — diff에서 실제 파일경로 추출 후 glob 매칭
- diff 텍스트 substring 금지. `+++ b/<path>` / `--- a/<path>` 헤더 파싱(또는 tool_calls의 file path 필드)으로 **touched_files 집합** 정확 추출.
- forbidden_paths를 **fnmatch/glob 패턴**으로 touched_files 각각에 매칭(`**/*.service` 등 기존 forbidden 패턴 호환).
- 경로 정규화: leading `./`·`a/`·`b/` 제거, posix 구분자 통일 후 비교. substring 아닌 **경로 컴포넌트 단위**.

### 3.2 forbidden_operation 축 — 토큰/명령 경계 매칭
- tool_calls/diff를 **word-boundary 토큰화**(`\b` regex) 또는 명령 파싱(shlex) 후 forbidden op를 **정확 토큰 매칭**(set membership). substring `in` 금지.
- 위험 명령(systemctl enable 등)은 기존 `_AXIS_PATTERNS` regex(이미 word-aware)와 일관되게 처리 — forbidden_op도 같은 regex/토큰 방식으로 통일.

### 3.3 expected_files 비교 (MED #2, L472) 동일 원리
- `f not in expected_files` 직접 문자열 비교 → 같은 경로 정규화 함수 통과 후 set 비교. 두 축이 **공통 normalize_path() 1개** 사용.

### 3.4 단일 책임 분리
- 매칭 로직을 `detect_axes`에서 분리: `_match_forbidden_paths(touched_files, forbidden_paths)` / `_match_forbidden_ops(tokens, forbidden_ops)` / `normalize_path(p)` 헬퍼로.
- ★ **7파일 내부 함수 분리로 가능한지 vs 새 모듈 필요한지 = 회장 결정 사항** (r2 금지 "새 모듈 추가"와 충돌 가능 → 별도 승인 범위 명시 필요).

## 4. 테스트 (재설계 시 필수)
- false positive: forbidden `rm`이 "form"/"warmup" 미매칭 / forbidden path `api`가 "capital" 미매칭.
- false negative: `./dispatch/x.py`·`a/dispatch/x.py`·`dispatch/x.py` 동일 취급, glob `**/*.service` 매칭.
- 경로 컴포넌트 경계: `dispatch/x` forbidden이 `dispatch/xyz.py` 오매칭 안 함.
- expected_files 정규화 일치.
- 기존 122 회귀 유지.

## 5. 회장 결정 요청
- **(B-1)** 7파일 내부 함수 분리로 정밀 재설계(새 파일 0, `v2_13_gate.py` 내 헬퍼) — r2 "새 모듈 금지" 유지 가능
- **(B-2)** 별도 매칭 모듈 신설(`dispatch/path_match.py` 등) — "새 모듈 추가 금지" 예외 승인 필요
- **(C)** 보류
- MED 5 처리: HIGH와 같은 매칭 축(L472 expected_files·L137 dead-code·L32 redundant lower·git subprocess perf x2)은 재설계 시 함께 닫음. perf 2건(git subprocess)은 module-level 1회 resolve 캐싱으로 동반 해결.

## 6. 안전 (현재)
merge 0(BLOCKED) · admin override 0 · code change 0 · r3 미착수 · canonical 8420a763 · flag OFF · cron 0 · **ACTIVE=false** · C(event strategy) 미착수.
