bibimbap/docs/development/verification-strategies.md

309 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Verification Strategies Registry
`verification-advisor` 가 읽는 단일 레지스트리. 이 파일에 등록된 전략만 검증 대상이 된다. 전략 추가/수정 시 이 파일만 변경하면 에이전트 구조(tier) 는 건드릴 필요 없다.
## 검증 사다리 (L1 / L2 / L3)
프로젝트의 검증은 3단으로 나뉜다. 코드 변경의 성격에 따라 **적용할 최소 L 레벨** 이 결정된다.
| 레벨 | 대상 | 신뢰 범위 |
|---|---|---|
| **L1** | 단위·회귀 (타입체크 + 단위/회귀 테스트) | "내가 고친 라인이 깨지지 않음" + "과거 버그가 재발하지 않음" |
| **L2** | 원격/외부 의존 계약 (live contract 테스트) | "외부 서비스 스키마·필드가 우리 기대와 정합" |
| **L3** | End-to-end (실제 런타임·DB·외부 서비스 전부 도는 시나리오) | "사용자 시나리오가 실제 스택에서 끝까지 동작" |
### 버그 범주 → 적용 L 레벨 (예시 · 프로젝트 맞춤)
프로젝트 특성에 맞게 아래 표를 채운다.
| 버그/변경 범주 | 의무 레벨 |
|---|---|
| 외부 API 스키마/응답 파싱 수정 | L1 + L2 |
| 외부 서비스 설정 상수 변경 | L1 + L2 |
| 인증/인가/롤 플로우 수정 | L1 + L2 + L3 |
| 순수 도메인 로직 (외부 의존 없음) | L1 |
| MyBatis 매퍼 신규/SQL alias·집계 뷰 정의·변경 (DB-방언 계약) | L1 + L2 (dev DB contract) |
| 인프라 설정 (container/env/compose) | L1 + 수동 스모크 |
| HTTP 상태코드/예외 매핑 변경 (404/403 등, 전역 `@ExceptionHandler` 경유) | L1 + 런타임 스모크 |
**회귀 테스트 의무**: 버그 수정 커밋은 해당 버그를 재현하는 테스트를 같이 포함한다. revert 시 테스트가 실패하고, 수정 후엔 통과해야 한다.
**신규 컨트롤러/매퍼 의존 변경 시 full test 의무**: 신규 컨트롤러를 추가하거나 컨트롤러의 매퍼 의존을 늘리면 implementation 단계에서 `test-compile` 만으로 끝내지 말고 반드시 full `./mvnw -o test` 를 실행한다. `BibimbapApplicationTests` 는 MyBatis/DataSource autoconfigure 가 exclude 된 컨텍스트라 컨트롤러가 주입하는 매퍼마다 `@MockBean` 을 수동 등록해야 하며, 누락 시 `contextLoads``NoSuchBeanDefinitionException` 으로 실패한다. 이 회귀는 `test-compile` 로는 탐지되지 않는다(W3-2 세션 20260618 실증).
**mock-vs-reality: DB-방언 계약은 L1 으로 못 잡는다**: 컨트롤러/애플리케이션 단위테스트가 MyBatis 매퍼를 `@MockBean` 으로 대체하면 "내가 부른 메서드가 불렸나"만 보고 "실제 Postgres 가 그 SQL 을 우리 기대대로 해석하나"는 검증하지 못한다. L1 통과율이 높을수록 DB-통합 계약 공백이 오히려 숨는다. W3-2 세션 20260622 에서 L1 43/43 GREEN 을 통과한 채 누출된 실버그 2종:
- **SQL alias 케이스 폴딩**: 따옴표 없는 `AS gameId` 는 Postgres 가 소문자(`gameid`)로 폴딩 → MyBatis `Map` 키가 소문자 → 컨트롤러의 camelCase `get("reviewCount")` 가 null → 집계 summary 전면 무력. camelCase 키가 필요하면 alias 를 **반드시 큰따옴표**(`AS "gameId"`)로 감싼다.
- **집계 뷰 1:N LEFT JOIN fan-out**: 부모(리뷰)에 자식(축 평점) 직접 JOIN 시 `COUNT(*)`/`AVG` 가 자식 행수만큼 왜곡(자식 0행일 땐 잠복, 입력 순간 발현). 집계는 자식을 서브쿼리에서 선집계 후 LEFT JOIN 한다.
→ 매퍼 메서드 신규/시그니처·SQL 변경, 집계 뷰 정의·변경 시 L1 GREEN 으로 끝내지 말고 dev DB 연동 L2 contract 로 (a) 매퍼 반환 `Map` 키가 컨트롤러 조회 키와 정합, (b) 뷰 집계가 샘플 데이터 손계산과 일치, 를 가드한다. (현재 L2 harness 미구축 — 신설이 open item.)
### 실행 수단
프로젝트 루트에 통합 검증 스크립트를 둘 것을 권장한다 (예: `scripts/verify.sh`, `make verify`, `cargo xtask verify`). 스크립트는 L1 → L2 → 로그 스캔 순차 실행을 담당.
- L2 를 조건부로 비활성화하는 환경 변수를 제공 (원격 꺼진 CI 대비). 예: `SKIP_LIVE_CONTRACT=1`.
- 원격 테스트의 seed/live URL 미지정 시 L2 는 자동 skip 처리되도록 구성.
## 사용 규약
- `verification-advisor` 는 호출 시 변경 scope 를 받아 매칭되는 전략만 실행한다.
- 전략 개수에 따라 tier 가 자동 결정된다:
- 1개 → advisor 가 직접 실행 (tier 2)
- 2개 이상 → `worker:` 필드가 있는 전략은 해당 worker spawn (tier 3), 나머지는 advisor 순차 실행
- 신규 worker 도입은 [agent-team-protocol.md §9](./agent-team-protocol.md) 의 확장 트리거 참조.
### 집합 전수 AC 실행 지침
design.md 의 "검증 포인트" 에 다음 형식의 AC 가 있으면 "집합 전수 체크" 유형으로 분류한다.
```
AC-N: <집합명> 전수 N건 ... (grep -c ... == N)
```
실행 방법:
1. AC 에 명시된 `grep -c` 명령을 그대로 실행한다.
2. 반환값이 AC 에 명시된 기대 수와 다르면 **blocker FAIL** 로 판정한다.
3. 실패 시 출력: 실제 매치 수 + 누락된 항목을 diff 또는 목록으로 제시.
집합 전수 AC 는 특성상 "부분 통과"가 없다 — 기대 수에서 1 이라도 어긋나면 전체 FAIL. 규약 근거는 프로토콜 §4.3 + `design-advisor.md` "집합 전수 체크 AC 패턴" 섹션.
## 전략 (템플릿)
아래는 YAML 스켈레톤이다. 프로젝트 기술 스택에 맞게 `cmd` 를 교체한다 (`pnpm` / `yarn` / `npm` / `cargo` / `go test` / `pytest` / `mvn test` 등).
```yaml
strategies:
# ── L1 ────────────────────────────────────────
- id: typecheck
cmd: <프로젝트 타입체크 명령> # e.g. pnpm typecheck / cargo check / mypy .
scope: all
timeout_s: 180
failure_severity: blocker
- id: unit
cmd: <프로젝트 단위 테스트 명령> # e.g. pnpm test / cargo test / pytest
scope: <src glob> # e.g. src/**
timeout_s: 600
failure_severity: blocker
# ── L2 ────────────────────────────────────────
- id: contract-<external>
cmd: <계약 테스트 명령>
scope:
- <외부 의존이 집중된 경로>
timeout_s: 60
failure_severity: blocker
preconditions:
- "<외부 서비스 기동 확인 방법>"
- "<필요 환경 변수>"
note: |
원격 미기동 또는 seed 미지정 시 runtime 에서 skip 처리.
blocker 로 두는 이유는 "원격이 떠 있는데 계약 위반" 이 사용자 장애로 직결되기 때문.
# ── 통합 ──────────────────────────────────────
- id: verify-all
cmd: <통합 검증 스크립트> # e.g. pnpm verify / make verify
scope: all
timeout_s: 900
failure_severity: blocker
note: "L1 + L2 + 로그 스캔 통합. ATP task 세션 종료 전 의무."
```
## 필드 정의
| 필드 | 의미 |
|---|---|
| `id` | 전략 식별자 (unique) |
| `cmd` | 실행 명령 |
| `scope` | glob. 이 패턴에 변경이 걸칠 때만 실행 |
| `timeout_s` | 초 단위 타임아웃 |
| `failure_severity` | `blocker` (실패 시 전체 검증 실패) \| `warning` (기록만) |
| `worker` (optional) | spawn 할 worker 이름. 미지정 시 advisor 직접 실행 |
## 확장 시
- 새 전략 추가: 위 YAML 블록에 항목 추가.
- Worker 분리가 필요한 수준에 도달 (브라우저 테스트, 장시간 E2E 등): `worker:` 필드 붙이고 해당 worker 파일 신설 + `verification-advisor.md` 의 tools 에 `Agent` 추가.
- 기준은 [agent-team-protocol.md §9 확장 트리거 레지스트리](./agent-team-protocol.md#9-확장-트리거-레지스트리).
## 설계·테스트 단계 체크리스트 (구조적 교훈)
세션 회고에서 수용된 재현성 있는 교훈을 규칙형으로 기재한다. 이 섹션은 누적 append-only.
### 영향맵에 SSR 호출지점 포함
매퍼 메서드 시그니처·data 클래스 필드 등 **심볼을 변경할 때**, design 단계 파일 영향맵은 API 컨트롤러뿐 아니라 그 심볼을 호출하는 **SSR/뷰모델 지점**(예: Spring MVC `@Controller` 의 뷰 렌더 메서드)도 `rg` 전수 확인으로 포함해야 한다. 누락 시 implementation 단계 컴파일 깨짐.
> 근거: W3-2 고도화 세션(20260622) — `GameController.gameDetail` SSR 호출지점을 설계 영향맵에서 누락 → 컴파일 깨짐, implementation-advisor 직접 보정.
### 계약 강화 시 기존 fixture 전수 감사
입력 검증 계약을 강화(최소 길이·필수 필드 추가)하면, 신규 테스트 케이스 추가만으로 부족하다. **기존 테스트 fixture 전수**가 새 계약에 정합하는지 감사하는 단계를 W-TEST 체크리스트에 명시해야 한다.
감사 절차:
1. 새로 도입된 검증 계약 목록화 (최소길이·필수필드·enum 범위 등).
2. 기존 테스트 fixture(요청 본문·파라미터) 전수 스캔 — 신규 계약 조건 충족 여부 확인.
3. 미충족 fixture 갱신 (행위 assertion 보존 전제).
4. 거부 경로 테스트의 fixture 도 신규 계약을 충족하는 값으로 올려 실제 거부 사유(상위 분기)까지 도달함을 보장.
> 근거: W3-2 고도화 세션(20260622) — B2(본문 10자)·6축 필수 계약 도입 시 기존 fixture 8건이 미갱신되어 L1 RED. 컨트롤러는 무버그.
### freeze/동결 분류는 근거 문서 확인 선행
어떤 변경을 "동결 영역 해제·고위험 게이트"로 분류하기 전에, 동결 범위를 정의한 **근거 문서(ADR·roadmap·work-log)를 줄 번호까지 직접 확인**한다. 표면적 유사성("평점 집계" 등)만으로 freeze 인접 추론 금지.
절차:
1. 동결 선언 근거 문서를 실제로 열어 동결 범위 정의를 줄 번호로 확인.
2. 변경 대상 테이블/뷰/심볼이 그 범위에 **명시적으로** 포함되는지 판단.
3. 포함 확인 시만 §6 게이트 표기.
> 근거: W3-2 고도화 세션(20260622) — `game_review_stats` 집계뷰가 phantom 고위험 게이트로 오분류 → `roadmap:63/201/202` 직접 확인으로 일반 DDL 정정.
### (긍정 패턴) frontend-design 스킬 fork 위임
production-grade UI(SVG·a11y·다중 JS 인터랙션 포함)를 구현할 때, `frontend-design` 스킬을 **fork(컨텍스트 상속)로 서브에이전트에 위임**하면 스킬 호출 + 단일파일 폴리시 + 프리뷰 render-verify 를 컨텍스트 오염 없이 수행 가능하다. 검증된 패턴.
조건:
- L1 전체 GREEN 확인 후 진입.
- 단일파일 폴리시: JSP 1파일 안에서 완결(신규 파일 0).
- 가드레일 명시: 기존 JS 로직 보존 / a11y / BE API 계약 무변경 / 외부 JS 라이브러리·CDN 도입 금지.
- 산출물에 프리뷰 HTML 포함(`artifacts/`).
> 근거: W3-2 고도화 세션(20260622) — 육각형 SVG 레이더·6축 radiogroup·C1~C6 를 fork 위임으로 단일 JSP 파일 승격 + L1 43/43 GREEN 유지.
### 신규 `*Test.java` 는 "(검증)" 소유태그여도 구현 산출물
design 파일 영향맵에서 신규 테스트 파일이 owner 칸에 `(검증)` 으로 표시되더라도, **테스트 작성은 implementation 단계의 산출물**이다. `verification-advisor` 는 테스트를 **실행만** 하며 Write 권한이 없다. `(검증)` 은 "검증 관련 파일"이라는 용도 라벨일 뿐 작성 주체가 아니다. implementation 단계에서 파일 영향맵의 신규 `*Test.java` 를 전수 작성해야 하며, 빠뜨리면 verification 단계에서 시나리오 AC(VP) 가 "미커버"로 떨어진다(테스트가 없으니 실행할 게 없음).
> 근거: W2 세션(20260624) — W2-2 에서 impl-advisor 가 설계 파일영향맵의 `JamJudgeAdminControllerTest`/`JamRoleGateTest` `(검증)` 태그를 "verification-advisor 가 작성"으로 오독 → 테스트 0건 산출. 별도 fix 라운드로 24테스트 보완 후 통과.
### 매퍼 alias 케이스폴딩 기준 = Map resultType / 집계 SELECT (VIEW 여부 아님)
`mock-vs-reality` 의 BUG-2(alias 케이스폴딩) 정밀화: camelCase alias 를 큰따옴표로 인용해야 하는 기준은 "집계 VIEW 인가"가 **아니라 매퍼 반환이 `Map`(또는 키 이름 의존 집계)인가** 이다. Postgres 는 비인용 `AS gameId` 를 소문자(`gameid`)로 폴딩하므로, `resultType=Map` 이면 `row.get("gameId")` 가 null 이 된다. 반면 **POJO 직접 매핑 매퍼는 MyBatis 가 case-insensitive 로 매핑**하므로 비인용 alias 라도 무해하다. 따라서: **Map resultType·집계 SELECT 매퍼는 `AS "camelCase"` 인용 필수, POJO 직접매핑은 비인용 허용**.
> 근거: W2 세션(20260624) — W2-1 5매퍼(POJO) 비인용 alias 67건 무해 확인 → W2-5 에서 `JamVotesMapper.listCountsByJam`(Map resultType) 은 인용 필수로 정정(설계 concern 은 "VIEW만 인용"으로 부정확). L2 contract 에서 비인용 대조군 폴딩(`gameid`/`votecount`) 재현으로 입증. W2-4/6 의 집계 매퍼도 동일 적용.
### 신규 컨트롤러가 호출하는 공유 헬퍼는 정의 존재를 grounding 으로 선확인
신규 컨트롤러가 기존 컨트롤러의 패턴(예: `response(HttpStatus, String)` 응답 헬퍼)을 따를 때, 그 헬퍼가 **상속/공유되는지 아니면 클래스마다 재정의해야 하는지**를 implementation 전에 `rg` 로 확인한다. bibimbap 컨트롤러는 공통 베이스 클래스가 없어 각 컨트롤러가 헬퍼를 자체 정의해야 한다. 호출만 하고 정의를 빠뜨리면 `test-compile` 로는 늦게 잡히고(또는 다른 컴파일 에러에 가려), full `./mvnw test` 의 컴파일 단계에서 BUILD FAILURE 로 발현한다.
> 근거: W2 세션(20260624) — W2-1 `JamAdminController` 가 `response(HttpStatus,String)` 44회 호출하나 정의 누락(`JamController` 는 보유) → L1 컴파일 24 errors. backward 보정으로 헬퍼 1개 추가 후 통과.
### 보안 거부 테스트는 정상경로 성공과 쌍으로 차별 입증 (vacuous 회피)
보안 방어(SSRF·인증 게이트 등)의 "거부" 단위테스트가 `assertTrue(result.isEmpty())` 류만 검사하면, 방어 로직이 깨져 **항상 empty 를 반환**해도 테스트가 통과한다(vacuous PASS — 거부를 입증하지 못함). 특히 graceful `catch(Exception)→empty` 패턴은 정상경로 예외(예: 잘못된 헤더 설정)를 삼켜 정상 fetch 실패를 숨긴다. → **정상 입력 성공(present) 테스트와 차단 입력 empty 테스트를 쌍으로** 두어, 성공테스트가 GREEN 이어야 차단테스트의 empty 가 "차단되어 empty"임이 구별 입증된다.
> 근거: W3-3 세션(20260629) — `SsrfSafeFetcher` 가 `Host` restricted-header 로 모든 fetch 가 예외→empty. SSRF 거부 테스트 17건이 전부 vacuous PASS, 정상경로 테스트만 RED 로 노출. adversarial 감사가 근본원인 진단.
### 보안핵심 기능은 verification + adversarial 코드감사 병렬
SSRF·zip-slip·경로 boundary·sanitize 같은 보안핵심은 verification-advisor(테스트 실행 판정)만으로 부족하다. 작성자 테스트가 못 짠 우회벡터(IP 대체인코딩·IPv6 변형·DNS rebinding·redirect 우회·심링크 TOCTOU·LIKE 와일드카드)는 **코드를 직접 추적하는 adversarial 감사**가 잡는다. 두 검증을 병렬로 걸고, 감사가 BYPASS/CRITICAL 을 내면 backward.
> 근거: W3-3 SSRF(감사가 Host-header vacuous + CGNAT 100.64/10 미차단 포착 — verification 단독으로는 무력 배포할 뻔) / W3-5 zip-slip(감사 SOUND — 심링크 TOCTOU 가 ZipInputStream 구조상 불가임을 코드추적으로 확정).
### 표시면이 선행 기능 컨트롤러에 의존하면 scope-fence 예외 선언
기능 A 의 UI 표시가 선행 기능 B 의 컨트롤러/뷰(예: 목록·카드 SSR)에 배선돼야 할 때, "B 산출물 접근 금지" scope-fence 를 그대로 두면 A 의 표시 AC 가 미완으로 남는다. orchestrator 는 표시면 의존을 사전 식별해 **해당 컨트롤러를 A 의 명시적 편집 허용 대상으로 fence 예외 선언**하거나, 표시 AC 를 의도적 deferral 로 분리·문서화한다.
> 근거: W4 세션(20260629) — 배지 표시 AC(프로필·게임카드)가 WebMvcController/SearchController(W3-1/3-4) SSR 에 의존하나 scope-fence 로 차단 → 리뷰작성자 칩만 구현, 프로필·게임카드는 deferral 로 분리. **후속 세션(20260629-142115)에서 그 deferral 을 독립 트랙으로 처리(컨트롤러 매퍼주입 + index.jsp 칩렌더, 5파일 352/352 GREEN) → 완전 해소.** fence 예외 선언과 후행 독립 트랙 분리가 **동등하게 유효한 두 경로**임이 실증됨 — 선행 기능과 충돌 자원이 없으면 후행 분리가 롤백 경계도 깔끔하다.
### 설계 트레이드오프 concern 은 1차 결론까지 못박기
설계가 "구현에서 A 또는 B 가 필요할 수 있다"식 트레이드오프를 concern 으로 열어두면, 1차 구현이 검증 안 된 분기를 고를 수 있다. design 산출 시 **트레이드오프를 1차 권장안 + 조건부 전환 기준까지 선결정**으로 좁혀 첫 구현이 정답 분기를 잡게 한다.
> 근거: W3-3 SSRF — 설계 concern 이 "IP핀닝 또는 connect후 peer검증" 을 열어둠 → 1차가 IP핀닝(Host override) 선택, HTTPS SNI 깨짐 + restricted-header 미검증. fix1 에서 option-b(hostname-connect)로 전환.
### needs_user_verification 은 "미완료 목록" 이 아니라 "결정 분기" 단위로 구조화
세션 종료 시 `needs_user_verification` 을 단순 잔여 작업 나열로 적으면, 다음 세션이 각 항목마다 "어떻게 할까요" 재질의로 시작한다. 대신 각 항목을 **사용자가 한 번에 고를 수 있는 결정 분기**(예: 직접 적용 / 지금 구현 / 배포 후 이월 / 추가 하드닝)로 미리 구조화하면, 다음 세션 착수 시 결정이 1라운드에 수렴하고 재질의가 0이 된다. 항목마다 "기본 가정값 + 영향 범위" 를 병기한다(§2.2 open_questions 규약과 정합).
> 근거: 세션 20260629-142115 — 직전 세션이 needs 4건을 결정 분기로 명시 → 사용자 4건 일괄 결정, 재질의 0, 모든 분기 첫 라운드 수렴.
### 런타임/환경 의존 보장은 L1(값 확인)과 L3(실반영) 레이어로 분리
설정값이 "코드에서 세팅되는가" 와 "런타임에 실제 반영되는가" 가 다른 레이어인 경우(예: `Security.setProperty("networkaddress.cache.ttl")``InetAddressCachePolicy` lazy init 경합으로 best-effort), 단위검증으로 **값 설정만** 확인하고 **실제 런타임 반영은 L3 스모크**로 분리 배정한다. best-effort 한계와 결정적 정본(예: JVM `$JAVA_HOME/conf/security/java.security`)을 코드 주석·체크리스트에 함께 명시해, "값=설정됨" 을 "동작=보장됨" 으로 오인하지 않게 한다.
> 근거: 세션 20260629-142115 b1 — `SsrfSafeFetcher.initDnsCachePolicy()` 단위검증은 `Security.getProperty=="30"` 만 확인(L1), 실 rebinding 방어 반영은 배포 후 체크리스트(L3)로 이월. 런타임 미반영 시 JVM 레벨 격상 경로 명시.
---
### MyBatis + PostgreSQL nullable 파라미터 명시 캐스트 규약
MyBatis annotation SQL(`@Select`)에서 nullable 파라미터를 `#{p} IS NULL` 조건으로 쓸 때, PostgreSQL은 prepared statement `$N`의 타입을 추론하지 못해 `PSQLException: could not determine data type of parameter $1` → 500이 발생한다. 컴파일·L1 단위테스트(MockBean)로는 탐지되지 않는다.
**규약:**
```sql
-- ❌ 잘못된 패턴 — null 전달 시 PSQLException
AND (#{categoryId} IS NULL OR p.category_id = #{categoryId})
-- ✅ 올바른 패턴 — 명시 캐스트
AND (#{categoryId}::bigint IS NULL OR p.category_id = #{categoryId}::bigint)
```
nullable 파라미터 타입별 캐스트:
| Java 타입 | PostgreSQL 캐스트 |
|---|---|
| `Long` / `Integer` | `::bigint` / `::int` |
| `OffsetDateTime` / `LocalDateTime` | `::timestamptz` / `::timestamp` |
| `String` | `::text` |
**대안**: `<if test="p != null">` 동적 XML 분기로 null 케이스를 조건 자체에서 제거(JamsMapper/GamesMapper keyset 패턴). 신규 keyset 페이징 매퍼 작성 시 이 패턴을 우선 권장한다.
**신규 매퍼 코드리뷰 체크**: `rg '#{[^}]+}\s+IS\s+NULL' src/main/java --type java` 로 타입 캐스트 없는 IS NULL 조건을 전수 확인한다.
> 근거: 세션 20260630-105459 — `PostsMapper.listPublishedKeyset` `#{categoryId} IS NULL` 캐스트 누락 → `GET /posts` 500.
---
## 프로토콜 개선 권고 (외부 번들 — 미적용)
아래 항목은 ATP 플러그인 번들(`~/.claude` 전역) 대상이다. 본 프로젝트 파일에서 직접 수정하지 않고 기록만 한다.
- **design-advisor 체크리스트**: §1 파일 영향맵 작성 규약에 "SSR 컨트롤러·뷰모델 호출지점 포함" 항목 추가 필요.
- **W-TEST worker 지시**: 컨트롤러 검증 계약 강화 시 "기존 fixture 전수 계약 정합 감사" 단계를 의무 체크리스트 항목으로 포함 필요.
- **agent-team-protocol §6 게이트 분류**: "근거 문서 확인 없이 freeze 인접 = 고위험 추론 금지" 조항 추가 권고.
- **design-advisor 파일영향맵 표기**: 신규 `*Test.java` 의 owner 를 `(검증)` 용도 라벨과 분리해 `owner=implementation` 으로 명시 필요(단계 경계 오독 유발 — 구조적). 근거: W2 세션(20260624) W2-2 테스트 미작성 갭.
- **implementation-advisor 체크리스트**: 신규 컨트롤러가 호출하는 공유 헬퍼/심볼은 정의 존재를 grounding(`rg`)으로 선확인하는 조항 추가(test-compile 비신뢰). 근거: W2 세션(20260624) W2-1 `response()` 헬퍼 누락 컴파일 FAIL.
- **검증 레지스트리 "버그 범주→L 레벨" 표**: 보안핵심(SSRF·zip-slip·경로 boundary·sanitize) 행 추가 — "L1+L2 + adversarial 코드감사 병렬" 명시. 근거: W3-3 세션(20260629) SSRF vacuous 포착.
- **verification-advisor 체크리스트**: 보안 거부 단위테스트는 정상경로 성공(present) 테스트와 쌍으로 차별 입증(distinguishing assertion) 확인 조항. 근거: W3-3 vacuous PASS.
- **orchestrator scope-fence 조항(구조적)**: 단일기능 표시면이 선행기능 컨트롤러에 의존할 때 cross-feature display wiring fence 예외 선언 또는 deferral 분리. 근거: W4 배지 표시 AC 갭.
- **design-advisor 조항**: 트레이드오프 concern 은 1차 권장안 + 조건부 전환 기준까지 선결정으로 좁힘. 근거: W3-3 SSRF IP핀닝 오선택.
- **§4.4 item5 를 *선행 게이트* 로 강화**: 현재 item5(가시성·표현·레이아웃 축 누락 점검)는 옵션 *작성 후* 사후 점검이라, "비율/가시성 떨어진다" 같은 **모호 어휘를 옵션 설계 이전에 단일 축으로 협소화**하면 가드가 작동할 시점을 이미 지난다. 모호 시각 결함 어휘 수신 시 옵션 축 확정 *전에* 다축 스캔(레이아웃비율·종횡비·대비가시성·타이포 최소 4축)을 의무화하는 선행 게이트로 승격 권고. 근거: 세션 20260630-175023 — "비율"을 카드 종횡비 단일 축으로 조기 협소화 → 사용자가 "검색바도 혼자 짧다"로 레이아웃 비율 축 직접 추가.
### HTTP 상태/예외 매핑 변경은 런타임 스모크 의무 (L1 통과로 충분치 않음)
컨트롤러가 던지는 예외의 HTTP 상태(404/403 등)를 바꾸거나 신규 예외를 도입할 때, 전역 `@RestControllerAdvice`/`@ExceptionHandler(Exception.class)` 가 더 구체적 핸들러보다 먼저 잡아 의도한 상태를 **삼킬 수 있다**(예: `ResponseStatusException(404)` 이 광역 Exception 핸들러에서 500으로 변환). 컴파일·단위(L1)는 "예외를 던진다"까지만 검증하고 실제 HTTP 응답 코드는 미검증 → L1 GREEN 으로 끝내지 말고 **런타임 스모크(`curl -w '%{http_code}'`)** 로 실제 상태코드를 가드한다. 회귀 가드는 (a) 컨트롤러가 올바른 예외를 던지는 단위테스트 + (b) advice 핸들러가 상태를 보존하는 검증, 둘 다 둔다.
> 근거: B2·B4·FE 세션(20260629) — `gameDetail` 404 전환이 mvn test 361 PASS 후에도 런타임 500 반환(전역 Exception 핸들러가 ResponseStatusException 가림). 브라우저 스모크에서 발견 → advice 에 `@ExceptionHandler(ResponseStatusException)` 추가로 해소.
### multi-worker advisor 산출은 orchestrator git diff 교차검증
implementation-advisor 가 여러 code-writer worker 로 파일을 분산 수정한 뒤 반환하는 완료 summary 는 worker 상태 추적 혼선으로 **실제 변경과 불일치**할 수 있다("Worker C만 완료" 보고했으나 실제 4파일 적용 등). orchestrator 는 advisor summary 를 그대로 신뢰하지 말고 `git diff --stat` / 대상 파일 grep 으로 **실변경을 교차검증**한 뒤 다음 단계로 진행한다.
> 근거: B2·B4·FE 세션(20260629) — FE implementation-advisor 가 불완전 summary 반환, git diff 로 4파일 적용 확인 후 진행. profile 미변경은 "입력 컨트롤 없음=정상"으로 판별.
### graphify 스캔 범위 — JSP/CSS/JS 정적 자산 미포함
graphify(`/graphify src/ docs/`)는 `src/main/webapp/WEB-INF/views/*.jsp`, `src/main/webapp/css/`, `src/main/webapp/js/` 등 정적 자산을 스캔 범위에 포함하지 않는다. 프론트엔드 분석 태스크에서 graphify-lookup-advisor가 miss를 반환하는 것은 예상 결과다 — research-advisor 직접 탐색으로 대체한다.
> 근거: 세션 20260630-113258 — graphify-lookup miss 즉시 확인, research-advisor로 전환해 JSP 26개 전수 탐색 성공.
### JSP 변경은 WAR 빌드 PASS만으로 불충분 — 런타임 스모크 의무
이 프로젝트는 JSP 사전컴파일이 미설정(Tomcat이 첫 요청 시 런타임 컴파일)이라, `./mvnw package` exit 0 이어도 JSP EL 표현식·taglib 선언 오류는 **런타임에만 발현**한다. WAR 빌드는 JSP를 파일로 패키징만 하고 컴파일하지 않으므로 L1 GREEN 은 "JSP 구문 유효" 보증이 아니다. JSP 파일이 변경된 모든 세션은 verification 단계에 **런타임 스모크**(앱 기동/재기동 후 변경 JSP 경로 `curl -w '%{http_code}'` 200 확인)를 명시한다. 특히 외부 디자인 산출물의 마크업을 이식할 때 stray taglib/EL 이 딸려오기 쉬우므로 implementation 체크리스트에 grep 전수 확인을 포함한다: `rg '<%@\s*taglib' src/main/webapp/WEB-INF/views/` (이 프로젝트 뷰는 JSTL 미사용·scriptlet 전략 — taglib 출현 자체가 적신호). §273(예외매핑) 의 일반화 버전이다. JSP 사전컴파일 플러그인(jspc) 도입은 별도 open item.
> 근거: 세션 20260630-160000 — 비주얼 리디자인 이식 중 errer.jsp에 디자인서 딸려온 구 javax JSTL taglib(`http://java.sun.com/jsp/jstl/core`)이 stray 유입 → mvn package PASS·다른 페이지 200 이나 `/error` 만 런타임 500(JasperException, Jakarta 미해소). L2 스모크에서 포착 → 미사용 taglib 제거로 200 (fix 360078a).
### 정적 include 파편(.jspf)은 자체 `pageEncoding` 필수 — 부모 인코딩 미상속
`<%@ include file="x.jspf" %>`(정적/translation-time include)로 삽입되는 파편은 **부모 JSP의 `pageEncoding`을 상속하지 않는다.** JSP 스펙상 정적 include 세그먼트는 파일별로 인코딩이 독립 결정되며, 파편에 page 지시자가 없으면 기본 ISO-8859-1로 읽힌다. UTF-8 한글 바이트를 가진 파편이 부모(UTF-8)에 include 돼도 Jasper가 Latin-1로 읽어 **mojibake**(`ì ì§ ë...`)가 된다. 본문 텍스트가 있는 .jspf는 첫 줄에 `<%@ page pageEncoding="UTF-8" %>`를 둔다(같은 값이면 부모와 충돌 없음). 런타임 스모크에서만 발현하므로 브라우저 육안 확인이 필요하다. (참고: `<jsp:include>` 런타임 include는 별도 translation unit이라 자체 contentType으로 정상 — theme-init/header/footer가 그 경로.)
> 근거: 세션 20260630-160000 후속 브라우저 리뷰 — posts-empty/recruit-empty.jspf(UTF-8)가 정적 include 되며 빈상태 한글 전부 mojibake. 빌드 PASS·페이지 200이라 빌드/문자열검증으론 미포착, 브라우저에서 발견 → 각 파편에 pageEncoding 추가로 해소 (fix a4d163d).
### 모호한 시각 결함 어휘는 단일 결정축 협소화 전 다축 스캔 선행
"비율이 떨어진다 / 가시성이 떨어진다" 같은 **모호한 시각 결함 어휘**는 결함의 위치를 한정하지 않는다. 이를 곧장 단일 결정축(예: "비율" → 카드 종횡비)으로 좁혀 `AskUserQuestion` 을 던지면, 같은 어휘가 가리키던 다른 축(레이아웃 비율·정렬·대비·표현 총량)이 옵션 공간에서 누락된다. 모호 시각 어휘 수신 시 옵션 축을 확정하기 **전에** 다축 스캔(최소 레이아웃비율·종횡비·대비가시성·타이포간격 4축)을 선행한다. 스크린샷이 있으면 관찰 1 에이전트 → 축별 병렬 평가 → 종합의 multi-axis 위임(`ui-multiaxis-eval` 패턴)이 협소화 맹점을 구조적으로 메운다. UI/UX 변경 검증은 §291 런타임 스모크를 **라이트/다크 양 테마 × 영향 화면 전수** 육안으로 수행한다(토큰 대비·종횡비는 테마별로 다르게 발현).
> 근거: 세션 20260630-175023 — 사용자 지적 "비율/가시성" 2건을 카드 종횡비 단일 축으로 협소화 → 사용자가 "검색바도 혼자 짧다"로 레이아웃 비율 축 직접 추가. 이후 multi-axis 위임 평가(6 에이전트)가 13건 결함으로 복원, 라이트/다크 양화면 스모크로 검증. 변경 상세: [changes/2026-06-30-ui-multiaxis-ratio-visibility-fix.md](../changes/2026-06-30-ui-multiaxis-ratio-visibility-fix.md).