30 KiB
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)로 폴딩 → MyBatisMap키가 소문자 → 컨트롤러의 camelCaseget("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 의 확장 트리거 참조.
집합 전수 AC 실행 지침
design.md 의 "검증 포인트" 에 다음 형식의 AC 가 있으면 "집합 전수 체크" 유형으로 분류한다.
AC-N: <집합명> 전수 N건 ... (grep -c ... == N)
실행 방법:
- AC 에 명시된
grep -c명령을 그대로 실행한다. - 반환값이 AC 에 명시된 기대 수와 다르면 blocker FAIL 로 판정한다.
- 실패 시 출력: 실제 매치 수 + 누락된 항목을 diff 또는 목록으로 제시.
집합 전수 AC 는 특성상 "부분 통과"가 없다 — 기대 수에서 1 이라도 어긋나면 전체 FAIL. 규약 근거는 프로토콜 §4.3 + design-advisor.md "집합 전수 체크 AC 패턴" 섹션.
전략 (템플릿)
아래는 YAML 스켈레톤이다. 프로젝트 기술 스택에 맞게 cmd 를 교체한다 (pnpm / yarn / npm / cargo / go test / pytest / mvn test 등).
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 확장 트리거 레지스트리.
설계·테스트 단계 체크리스트 (구조적 교훈)
세션 회고에서 수용된 재현성 있는 교훈을 규칙형으로 기재한다. 이 섹션은 누적 append-only.
영향맵에 SSR 호출지점 포함
매퍼 메서드 시그니처·data 클래스 필드 등 심볼을 변경할 때, design 단계 파일 영향맵은 API 컨트롤러뿐 아니라 그 심볼을 호출하는 SSR/뷰모델 지점(예: Spring MVC @Controller 의 뷰 렌더 메서드)도 rg 전수 확인으로 포함해야 한다. 누락 시 implementation 단계 컴파일 깨짐.
근거: W3-2 고도화 세션(20260622) —
GameController.gameDetailSSR 호출지점을 설계 영향맵에서 누락 → 컴파일 깨짐, implementation-advisor 직접 보정.
계약 강화 시 기존 fixture 전수 감사
입력 검증 계약을 강화(최소 길이·필수 필드 추가)하면, 신규 테스트 케이스 추가만으로 부족하다. 기존 테스트 fixture 전수가 새 계약에 정합하는지 감사하는 단계를 W-TEST 체크리스트에 명시해야 한다.
감사 절차:
- 새로 도입된 검증 계약 목록화 (최소길이·필수필드·enum 범위 등).
- 기존 테스트 fixture(요청 본문·파라미터) 전수 스캔 — 신규 계약 조건 충족 여부 확인.
- 미충족 fixture 갱신 (행위 assertion 보존 전제).
- 거부 경로 테스트의 fixture 도 신규 계약을 충족하는 값으로 올려 실제 거부 사유(상위 분기)까지 도달함을 보장.
근거: W3-2 고도화 세션(20260622) — B2(본문 10자)·6축 필수 계약 도입 시 기존 fixture 8건이 미갱신되어 L1 RED. 컨트롤러는 무버그.
freeze/동결 분류는 근거 문서 확인 선행
어떤 변경을 "동결 영역 해제·고위험 게이트"로 분류하기 전에, 동결 범위를 정의한 근거 문서(ADR·roadmap·work-log)를 줄 번호까지 직접 확인한다. 표면적 유사성("평점 집계" 등)만으로 freeze 인접 추론 금지.
절차:
- 동결 선언 근거 문서를 실제로 열어 동결 범위 정의를 줄 번호로 확인.
- 변경 대상 테이블/뷰/심볼이 그 범위에 명시적으로 포함되는지 판단.
- 포함 확인 시만 §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가Hostrestricted-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)로는 탐지되지 않는다.
규약:
-- ❌ 잘못된 패턴 — 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 /posts500.
프로토콜 개선 권고 (외부 번들 — 미적용)
아래 항목은 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-1response()헬퍼 누락 컴파일 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) —
gameDetail404 전환이 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 (fix360078a).
정적 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건 결함으로 복원, 라이트/다크 양화면 스모크로 검증.