feat(skill): route fixes, delivery report, composition diversity, platform refresh

- route table: extension route for products with an existing visual
  language, feedback row, derived state tokens stay local
- completion: blocking 0 plus reported unverified; hard-gate surface is
  the touched scope; delivery report starts with what and how to open
- design.md created at step 0 with a skeleton, fingerprint line and
  change log; product-surface rubric reading; direction delegation
  states the chosen card and reason
- layout: hero families, nav forms, footer forms; art-direction page
  convergence 6 axes; genre sections; image treatments; placeholder
  conventions; multi-screen order; Tailwind theme mapping
- platform: scroll-driven examples, anchor positioning, canvas OT,
  three/R3F pins, shadcn style prefixes, Safari 26 notes, subgrid
- design-gate: four result states, exit codes, cwd module resolution,
  --url/--out-dir/--strict, oklch contrast, dead selector word match
- Korean font budget split from the Latin baseline
This commit is contained in:
Yun Chan 2026-10-06 16:55:38 +09:00
parent 9bcb45cbd3
commit 88c11682b1
51 changed files with 5834 additions and 2259 deletions

View file

@ -4,41 +4,79 @@
> **완료는 기능·프로젝트 계약·렌더 시각 평가가 함께 확인된 상태다. 사용자를 QA 로 쓰지 않는다.**
"고쳤다"는 말의 근거는 검증 리포트의 수치와 실제 렌더 평가다. 게이트가 실패 항목을 내면 4단계로 돌아가 고치고, 변경한 영역과 연결된 회귀 위험을 다시 검사한다. 프로젝트가 정한 전체 검증 계약은 그대로 수행한다. 이 루프(구현 → 게이트 → 실패 → 수정 → 재게이트)가 designpaca의 기본 동작이며, 기능·접근성 자동 검사와 시각 평가(위계·브랜드·타입·구도·이미지·리듬)는 별도 결과로 보고한다.
"고쳤다"는 말의 근거는 검증 리포트의 수치와 실제 렌더 평가다. 기능·접근성 자동 검사와 시각 평가는 별도 결과로 보고한다. 시각 평가의 축과 판정 기준은 [critique.md](critique.md) §2-1 시각 비평 루브릭을 쓴다.
## 왜 필요한가 — 실제 사고 목록
각 검사가 어떤 실제 사고에서 왔는지(사고 목록)와 테스트를 새로 쓸 때의 게이트 규칙 G1~G36 전문은 [audit-gate-rules.md](audit-gate-rules.md)에 있다. 게이트를 확장하거나 사용자 보고 결함을 해부할 때만 연다.
이 게이트의 각 검사는 사용자 보고로야 발견됐던 결함에서 왔다. 어느 검사가 어느 사고를 잡는지가 설계 근거다.
## 이 문서를 읽는 법
| 사고 (실제) | 검사 |
| 상황 | 읽을 곳 |
|---|---|
| CSS 닫는 중괄호 누락 — 이후 규칙 절반이 죽은 채 렌더 | stylelint(구문) + 중괄호 균형 |
| 브랜드가 모바일에서 1글자 폭으로 수축, 세로로 쌓임 | 수축 탐지(짧은 라벨 n줄 이상) + 브랜드 줄바꿈(텍스트 블록 각각 1줄) |
| 기기 글꼴 확대(안드로이드 큰 글꼴 1.3×)에서 제목 부서짐 | 스케일×폭 h1 행렬 |
| 본문 보조색이 배경 대비 4.4:1 (AA 미달) | 전 요소 대비 스캔(블렌드 계산) |
| CSS `color-mix()`의 computed `color(srgb …)`를 0~255 rgb로 오독해 정상 대비를 1:1로 거짓 실패 | CSS Color 4 `color(srgb 0~1)` 파싱 + 알파 합성 RED fixture |
| `.sr-only`·`aria-hidden` 장식·빈 overlay의 범위를 보이는 셀/버튼 글자로 합산해 0px 여백 거짓 실패 | text node 단위 실측 + 비가시 조상 제외. 화면에 보이는 글자만 인셋 판정 |
| 조사만 하고 안 고친 영역(관심 과목 등) | 전 뷰 순회 — 검사는 '내가 고친 것'이 아니라 전체 표면 |
| 대시보드 26명 ↔ 명단 12명 숫자 모순 | 콘텐츠 정합(요약 = 재계산 비교) |
| aria-label 을 div/ul 에 오용(보조기술 무시) | html-validate + axe |
| dl 안 `<p>`, role="grid" 자식 구문 위반 | html-validate + axe |
| 계산 로직 오류(행 배열을 값으로 셈 → 0/3) | 단위 테스트(브라우저 밖 경계값) |
| 같은 데이터 속성이 여러 뷰에 존재해 선택자 오작동 | 하니스 규칙: 선택자는 컨테이너 스코프 |
| 혼합 span 그리드의 자동 배치가 시간 라벨을 흩어놓음(같은 열 폭 40/78 혼재, Δ 불규칙) | **리듬·트랙 불변식** — 같은 부모·같은 클래스 형제 중 (a) 숫자/시간 라벨(등폭 의도)과 (b) 빈 격자 셀(트랙 균일 의도)은 폭이 균일해야 하고, 한 축 정렬·크기 균일 형제는 등간격이어야 한다. 한국어 라벨은 글자수가 같아도 내용 폭차가 자연스러우므로 폭 검사 대상에서 뺀다(오탐 방지). 근본 수칙: 혼합 span 그리드는 자동 배치를 믿지 않고 좌표를 명시하라 |
| 프리셋 전환(v7)에서 브랜드 표면이 누락 — theme-color·파비콘이 종이 시대 값으로 11버전 생존 | **theme-color 정합**(메타 값 ↔ 실제 배경색 비교) + 파비콘·og:image 존재. 토큰 마이그레이션은 브랜드 표면 3종을 갱신하기 전까지 끝나지 않는다 |
| 컴포넌트 제거 시 CSS/JS 잔존 — 미사용 선택자 12종·이중 계산 블록(첫 블록이 즉시 덮임) | **죽은 선택자 검사**(CSS 클래스 ↔ HTML·JS 텍스트 사용률 대조). 제거는 HTML·CSS·JS 삼위일체 |
| 크롬에 컨트롤 1개 추가 → 그 줄의 최소폭 합 초과(320px +18px) | 오버플로 전 폭 검사(기존) + 규칙: 크롤에 컨트롤을 더하면 최소폭 합을 가장 좁은 폭에서 다시 잰다 |
| 같은 명시도의 숨김 규칙이 기본 규칙보다 앞에 있어 짐 — 모바일에서 숨기려던 kbd 안내가 렌더됨(6% 시각 diff) | 시각 회귀 + 규칙: **숨김 오버라이드는 기본 규칙보다 파일 뒤쪽에** 두거나 컴포넌트 규칙 근처의 미디어쿼리로 |
| 탐색 차터가 결과를 단언하지 않아 "9288점" 입력이 조용히 통과(값을 덧붙인 타이핑) | 하니스 규칙 8(검사는 단언)·9(number 입력 조작) |
| 모바일 라운드 — 중앙 모달이 썸존 밖, 백 키가 앱을 통째로 종료, safe area 무시, 입력 확대 | L6 시나리오(백·시트·터치타깃 스윕) + `references/mobile-app-ux.md` 표준 — **OS 관습은 조사 없이 바꾸지 않는다** |
| 전역 `* { margin: 0 }`가 native dialog의 UA 중앙 여백을 지워 좌상단에 고정, 짧은 화면의 패널은 고정 크롬에 눌려 내부 scroll이 0px | dialog의 `fixed/inset/margin:auto`를 명시하고 390·1440·2560 중심 오차, viewport rect, `html` scroll lock, short-height scroll owner를 hard fail로 측정. 모달 행동은 [WAI-ARIA APG](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)를 따른다 |
| 2560px 분할 히어로에서 사진이 1400px 이상으로 팽창하고 카피 열은 200px 안팎으로 붕괴 | hero·copy·media·후속 정보 레일의 rect 계약. stage/media/copy/workbench 토큰을 분리하고 media 상한·copy 최소폭·정렬을 hard fail로 측정 |
| 탭 전환 스크롤 잔류 — 짧은 뷰에서 클램프되어 헤더 반쯤 잘림, 뷰마다 크롬 위치 제각각 | 뷰 전환 `scrollTo(0,0)` 단언(M5) + mobile-app-ux.md |
| 크럼을 헤더로 내보내 빈약한 헤더 — 계측 전부 통과했지만 사용자에게 '깨져 보인다' | 헤더 sticky·브랜드+액션 구조 단언 + **시각 무게 평가(눈)** — mobile-app-ux.md |
| 왼쪽 인셋 리듬 불일치(제목 16 vs 앱바·카드 24) — 오버플로 검사는 오른쪽만 보므로 통과 | M6 왼쪽 인셋 스위트(텍스트 시작점 ≥12px·같은 축 ±10px) + preflight §0-E |
| 모바일에서 절대 배치 지도 자식의 부모를 `position: static`으로 바꿈 — offsetParent가 BODY가 되어 격자·핀이 히어로 전체를 덮음. 기하 게이트 8/8 통과 | **상태별 실제 캡처를 열어 보는 시각 E2E** + absolute/fixed 자식의 containing block 단언(하니스 규칙 19) |
| 새 관리자·가맹 서브페이지가 생겼지만 `public/work/*/index.html`만 검사 — 새 HTML 오류 8건이 검증 밖 | 정적 입력을 `public/work/**/*.html`·`**/*.css`로 재귀화 + 검사 대상 파일 수 단언(하니스 규칙 20) |
| 마케팅 h1의 의도적 2줄 때문에 범용 줄 수 규칙을 강제할 위험 | 페이지별 읽기 폭·문구·글꼴 확대의 계약을 기록하고 375·390 × 글꼴 확대에서 실제 줄 수와 가독성을 판정. 줄 수를 맞추려고 글자 크기를 축소하지 않음 |
| 실패를 몇 번까지 고치는지, 언제 멈추고 사용자에게 묻는지 | 루프와 재작업 상한 |
| 도구가 없어 검사를 못 했을 때 보고하는 법, 완료 보고 첫 줄 | 미검증은 통과가 아니다 |
| 매 감사에서 지킬 게이트 규칙 요약 | 매 감사에서 지키는 규칙 |
| 검사 계층의 구성 | 7계층 |
| 접근성 자동 감사·axe | 접근성 감사 루프 |
| 터치 타깃 24px·44px 판정 | 터치 타깃 — 두 층 판정 |
| 캡처 조건·모션 완료·잘림·폰트 측정 | 렌더 측정 계약 |
| 게이트를 돌리는 명령·옵션·종료 코드, 프로젝트를 바꾸지 않는 실행 | 게이트 실행 |
| 어느 검사가 어느 폭에서 재는지, 게이트가 재지 않는 것 | 검사별 측정 폭 |
| `gate.config.json` 설정 키 | 설정 키 |
| 게이트를 프로젝트에 심기(사용자가 동의했을 때만) | 설치 |
| 집계 리포트 형식 | 리포트 양식 |
| 새 검사 작성, 사고 이력, 사용자 보고 해부 | [audit-gate-rules.md](audit-gate-rules.md) |
## 루프와 재작업 상한
기본 루프는 `구현 → 게이트 → 실패 → 수정 → 재게이트`다. 실패 항목을 고치면 고친 영역과 연결된 회귀 위험을 다시 검사하고, 프로젝트가 정한 전체 검증 계약은 그대로 수행한다. 이 루프에는 상한이 있다. 자율 실행 모드(자동 승인·목표 모드 등)도 상한을 면제하지 않는다.
1. **같은 결함은 수정 시도 3회까지다.** "같은 결함"은 [critique.md](critique.md) §6 병합 기준으로 묶은 하나의 finding(같은 근본 원인)이다. 시도 1회는 `수정 → 같은 검사 재실행` 한 번이다. 수정이 렌더에 닿지 않았다면(G11: diff 수치가 소수점까지 같다) 그 회차는 시도가 아니라 적용 실패다. 적용 경로(캐시·캐스케이드·파일)부터 확인한다.
2. **3회째도 실패하면 4회째 수정을 하지 않는다.** 멈추고 사용자에게 보고한 뒤 턴을 끝낸다. 보고에는 finding(critique §3 형식), 원인 가설, 시도한 수정 3개와 각 회차의 측정값, 남은 위험, 그리고 아래 선택지를 담는다. 질문 수단은 [SKILL.md 인터뷰 게이트](../SKILL.md)의 규칙 3과 [harness.md](harness.md)를 따른다.
- (가) **구조를 되돌린다** — 같은 자리를 계속 깁지 않고, 결함을 만든 구조·토큰·방향 결정으로 돌아간다(3단계 또는 2단계).
- (나) **필요한 것을 받는다** — 실제 값·자산·권한·실기기·도구처럼 에이전트가 가질 수 없는 것을 사용자에게 요청한다.
- (다) **알려진 결함으로 남긴다** — 사용자가 고른 경우에만, 위험과 함께 `design.md` 미확정 목록에 적는다. 하드 게이트 결함은 이 선택으로도 "충족"이 되지 않는다. 상태는 `차단(사용자 수용)`으로 남는다.
3. **고칠 수 없는 외부 요인은 시도 횟수를 쓰지 않는다.** 서드파티 iframe·임베드의 위반, 하네스에 없는 도구가 필요한 검사는 첫 확인에서 `외부`로 분류하고 2번의 보고 형식으로 바로 알린다.
4. **방향(Direction) finding은 4단계가 아니라 2단계로 되돌아간다.** 같은 상한(3회)을 쓴다. 사용자의 취향 불만이 반복될 때의 절차는 [feedback-loop.md](feedback-loop.md)를 따른다. 그 문서의 "같은 불만 3회면 방향 단계로 복귀"와 이 상한은 같은 원칙이다. 국소 수정을 반복해 방향 결함을 덮지 않는다.
## 미검증은 통과가 아니다
도구·권한·기기가 없어 실행하지 못한 검사는 FAIL도 PASS도 아닌 **미검증(Not verified)**이다.
- 하드 게이트 항목이 미검증이면 그 항목을 충족으로 세지 않는다. 실기기 검증(하드 게이트 6)처럼 대부분의 하네스에서 할 수 없는 항목도 마찬가지다.
- 하드 게이트에 미검증이 하나라도 있으면 완료 보고에 "모두 통과", "완료", "검증 끝" 같은 말을 쓰지 않는다. 대신 "하드 게이트 미검증 M건 — 검증하려면 <도구·기기·사용자 확인>이 필요하다"를 적는다.
- 미검증을 줄이려고 검사를 대충 흉내 내지 않는다. 코드만 읽고 렌더 결과를 단정하는 것은 증거 방향성 위반이다(critique §9).
### 완료 보고 첫 줄 — 정본
검증 결과를 보고하는 줄은 이 문자열 하나다. 리뷰 보고에서는 첫 줄에, 구현 경로의 납품 보고에서는 "검증" 줄에 쓴다([critique.md](critique.md) §13·§13-b). 다른 문서는 이 문자열을 복제하지 않고 이 절을 가리킨다.
```
하드 게이트: 충족 N / 미검증 M / 차단 K · 방향: 열린 Direction d건 · 시각 루브릭: <점수>
```
- `N`·`M`·`K`는 [preflight.md](preflight.md)의 하드 게이트 행을 센 수다. 게이트 도구의 집계 줄(`충족 n / 미검증 m / 실패 k`)은 자동 검사 항목 수라서 그대로 옮겨 적지 않는다. 게이트가 미검증·실패로 낸 항목을 해당 행에 반영한 뒤 센다.
- `<점수>`에는 [critique.md](critique.md) §2-1 루브릭의 `합계 x/y, 최약 축 <축>(<축 점수>)`를 적는다. 시각 평가가 요청 범위 밖이면 `범위 밖`, 렌더를 볼 수 없었으면 `미검증`이라고 적는다.
- 영어로 보고할 때는 `Hard gates: N met / M not verified / K blocking · Direction: d open · Visual rubric: <score>`로 쓴다. 용어 대응: 충족 = met, 미검증 = not verified, 미점검 = not reviewed, 차단 = Blocking, 방향 = Direction, 중요 = Important, 다듬기 = Polish. 실행마다 다른 말로 옮기면 다음 실행이 `design.md`에서 이 줄을 찾지 못한다.
- 미검증이 사람·실기기가 있어야만 확인되는 항목뿐이면 "에이전트가 확인할 수 있는 범위는 전부 충족, 사람 확인 필요 M건"이라고 덧붙인다. 도구가 있었는데 돌리지 않은 미검증과 구분하기 위해서다.
- 프로젝트 계약 대조 결과는 첫 줄에 넣지 않는다. 계약을 대조한 경로에서는 둘째 줄에 `프로젝트 계약: 충족 n / 미검증 m / 위반 k`로 따로 적는다.
**납품 판정.** 차단 0건이면 납품할 수 있고, 미검증은 항목과 이유를 명시 보고한다(충족으로 세지 않는다). 미검증이 남은 납품은 "검증 미완료 납품"이라고 밝히고, 항목마다 검증에 필요한 도구·기기·사용자 확인을 적는다. 차단이 1건이라도 있으면 납품이 아니다. 사용자가 위험을 듣고 수용한 차단(위 루프 2의 (다))은 `K`에 넣지 않고 첫 줄 뒤에 `차단(사용자 수용) k′건`으로 따로 적으며, 수용 문장을 `design.md`에 남긴 경우에만 납품한다. 열린 Direction(`d > 0`)은 납품 전에 2단계로 돌아가 닫는다(위 루프 4).
## 매 감사에서 지키는 규칙
게이트 규칙 전문은 [audit-gate-rules.md](audit-gate-rules.md) §2다. 아래는 확장 여부와 무관하게 매번 지킨다.
- **G7** 게이트를 통과해도 리포트에 "무엇을 몇으로 확인했나"를 남긴다. "ALL PASS"만 쓰지 않는다.
- **G8** 검사는 기대값과 비교해 단언한다. 상태를 출력만 하는 것은 검사가 아니다.
- **G11** 수정 뒤에도 같은 diff 수치가 반복되면 수정을 반복하지 말고 적용 자체를 의심한다.
- **G15** 시각 기준(baseline) 갱신은 무엇이 의도적으로 바뀌었는지 한 줄씩 나열한 뒤에만 한다.
- **G16** 사용자 보고 스크린샷은 비전 요약이 아니라 픽셀 측정으로 해부하고, 원인을 수치로 확정한다.
- **G17** 새 검사는 수정 전 코드에서 RED를 재현한 뒤 GREEN을 확인한다.
- **G18** 시각 E2E는 기본 화면만 찍지 않는다. 캡처를 실제로 열어 판정하고, 저장 사실만으로 PASS하지 않는다.
- **G22** 사용자 승인과 검증 결과는 다른 상태다. 승인됨/검증됨을 따로 적는다.
- **G36** 증거 방향성을 지킨다. 시각 증거로 코드 finding을, 코드로 시각 finding을 단정하지 않는다.
사용자가 결함을 보고하면 방어하지 않는다. G16으로 원인을 수치화하고, 그 결함을 잡는 검사를 G17 순서로 게이트에 더한다.
## 7계층
@ -46,28 +84,28 @@
| 계층 | 도구 | 비고 |
|---|---|---|
| L0 정적 | stylelint + html-validate | 문법·구문 위반을 브라우저 켜기 전에 |
| L0 정적 | `design-gate.mjs` — 죽은 선택자·토큰 위생(기본 켜짐), stylelint + html-validate(`checks.static`, 기본 꺼짐) | 문법·구문 위반과 죽은 코드를 브라우저 켜기 전에. 브라우저가 없어도 이 계층의 결과는 남는다 |
| L1 단위 | 순수 계산 로직 추출(calc.js) + 경계값 테스트 | 프로젝트별 작성 — 0명/만석/초과/빈배열. **입력 검증·신청 가능 같은 '판정'(가능/불가+사유)도 여기 둔다** — UI 는 판정을 문장으로 번역만 |
| L2 불변식 | `design-gate.mjs` — 오버플로·제목 존재/가시성·수축·대비·SEO/meta·죽은 선택자·토큰 위생·theme-color 정합. 명시한 양의 `h1MaxLines`만 줄 수 계약으로 검사 | 전 뷰 × 전 폭 |
| L3 시각 회귀 | 기준 화면 pixelmatch diff (≥0.1% 실패) + 핵심 상태 캡처 | 갱신은 검증된 배포 후 `--update-baseline`. 캡처 존재와 사람의 시각 판정은 별개다 |
| L2 불변식 | `design-gate.mjs` — 오버플로·제목 존재/가시성·수축·리듬·트랙·대비·SEO/meta·theme-color 정합·글꼴 확대 h1 행렬·브랜드 줄바꿈·터치 타깃·왼쪽 인셋·axe·런타임 오류. 명시한 양의 `h1MaxLines`만 줄 수 계약으로 검사 | 전 폭(`widths`)을 도는 것은 오버플로뿐이다. 나머지는 검사마다 정해진 폭에서 잰다 — 아래 "검사별 측정 폭" |
| L3 시각 회귀 | 기준 화면 pixelmatch diff (≥0.1% 실패). 게이트는 페이지 기본 화면을 390·1440에서만 찍는다. 뷰·핵심 상태 캡처는 프로젝트 E2E로 보강한다 | 갱신은 검증된 배포 후 `--update-baseline`(G15). 기준이 없어 새로 만든 실행은 비교가 아니므로 미검증이다. 캡처 존재와 사람의 시각 판정은 별개다 |
| L4 WebKit | Playwright webkit | 사파리 엔진 렌더 차이 |
| L5 탐색 | 키보드 완전 통과 + 사용자 여정 차터 | 프로젝트별 작성 — 마우스 금지 |
| L6 시나리오 E2E | 실사용자 여정 재현 — 페르소나의 하루·장기 과업 포함 | 프로젝트별 작성. **기대값 단언 필수**(하니스 규칙 8), 백 키·시트·양방향 흐름은 `mobile-app-ux.md` 패턴 기준. 실측: RED 1,742 단언 중 318 실패가 신규 기능에 정확히 집중 — 시나리오를 먼저 쓰면 미구현이 수치로 드러난다 |
| L6 시나리오 E2E | 실사용자 여정 재현 — 페르소나의 하루·장기 과업 포함 | 프로젝트별 작성. **기대값 단언 필수**(G8), 백 키·시트·양방향 흐름은 [mobile-app-ux.md](mobile-app-ux.md) 패턴 기준. 시나리오를 먼저 쓰면 미구현이 수치로 드러난다 |
## 접근성 감사 루프
**하드 게이트.** 접근성 감사는 자동 점수 하나로 끝나지 않는다. Chrome DevTools MCP가 있으면 `lighthouse_audit`(accessibility 카테고리)로 실패 노드를 모으고 `take_snapshot`으로 접근성 트리(이름·역할·상태·랜드마크·헤딩)를 확인한다. MCP가 없으면 `npx lighthouse --only-categories=accessibility` 또는 `npx @axe-core/cli <url>`로 대체한다. 실패 노드로 대상 컴포넌트를 국소화하고, 수정한 뒤 **동일 감사를 재실행**해야 통과다 — 4단계로 돌아가 고치고 5단계에서 재검증하는 이 문서의 기본 루프와 같은 정신이다. 자동 점수 100은 WCAG 준수와 같지 않고, 낮은 점수가 이슈 증거를 대체하지도 않는다. 판정 원칙·시맨틱·키보드·라이브 리전 같은 세부는 [accessibility.md](accessibility.md)를 따른다 [SKILL-WEB-A11Y].
**하드 게이트.** 접근성 감사는 자동 점수 하나로 끝나지 않는다. 브라우저 자동화 도구에 Lighthouse 감사와 접근성 트리 스냅샷 기능이 있으면 그것으로 실패 노드를 모으고 접근성 트리(이름·역할·상태·랜드마크·헤딩)를 확인한다. 하네스별 도구 이름은 [harness.md](harness.md) §6에 있다. 그런 도구가 없으면 `npx lighthouse --only-categories=accessibility` 또는 `npx @axe-core/cli <url>`로 대체한다. 실패 노드로 대상 컴포넌트를 국소화하고, 수정한 뒤 **동일 감사를 재실행**해야 통과다. 자동 점수 100은 WCAG 준수와 같지 않고, 낮은 점수가 이슈 증거를 대체하지도 않는다. 판정 원칙·시맨틱·키보드·라이브 리전 같은 세부는 [accessibility.md](accessibility.md)를 따른다 [SKILL-WEB-A11Y].
**선택 axe 훅.** 이 문서 상단(왜 필요한가)의 axe 관련 검사(aria-label을 div/ul에 오용, dl 안 `<p>`·role="grid" 자식 구문 위반)는 axe-core 가 있을 때만 돈다. `tools/design-gate.mjs` 는 `checks.axe: "auto"`(기본값)일 때 프로젝트 cwd 에서 `axe-core` 를 해석할 수 있으면 뷰마다 WCAG 2.x A·AA 태그로 axe 를 실행해 위반을 실패로 보고하고, 해석할 수 없으면 `SKIP — 미검증` 으로 보고한다(`npm i -D axe-core` 로 켠다). 게이트 밖에서는 브라우저 MCP 나 `npx @axe-core/cli <url>` 로 같은 검사를 돌린다. 어느 쪽도 없으면 해당 체크를 실패로 적지 않고 **미검증(Not verified)**으로 보고한다 — 통과로 추정하지 않는다. 상태값의 뜻은 [리포트 양식](#리포트-양식)을 따른다.
**선택 axe 훅.** axe 관련 검사(aria-label을 div/ul에 오용, dl 안 `<p>`·role="grid" 자식 구문 위반 등)는 axe-core 가 있을 때만 돈다. `tools/design-gate.mjs` 는 `checks.axe: "auto"`(기본값)일 때 `axe-core` 를 실행한 디렉터리(cwd) → 게이트 스크립트 위치 순으로 찾는다. 찾으면 뷰마다 WCAG 2.x A·AA 태그로 axe 를 실행해 위반을 실패로 보고하고, 못 찾으면 `미검증` 줄로 보고한다(통과로 세지 않는다). 프로젝트에 `axe-core` 를 설치하는 것은 아래 "설치"의 동의 대상이고, 리뷰 경로에서는 설치하지 않고 미검증으로 남긴다. 게이트의 axe 는 `best-practice` 태그 규칙(제목 단계 건너뜀, `<main>` 하나, dialog 이름)을 돌리지 않는다. 게이트 밖에서는 브라우저 도구나 `npx @axe-core/cli <url>` 로 같은 검사를 돌린다. 어느 쪽도 없으면 해당 체크를 실패로 적지 않고 **미검증(Not verified)**으로 보고한다 — 통과로 추정하지 않는다.
## 터치 타깃 — 두 층 판정
L2의 터치 타깃 검사는 컨트롤 하나의 실측 `min(width, height)`을 두 문턱과 비교한다. `thresholds.inlineTargetMin`(기본 24px)은 본문 흐름 속 인라인 텍스트 링크에 적용되는 문턱이고, `thresholds.tapTargetMin`(기본 44px)은 버튼·입력·role 컨트롤 등 인라인이 아닌 컨트롤에 적용되는 문턱이다. 두 문턱은 서로 다른 판정 위계를 진다.
L2의 터치 타깃 검사는 컨트롤 하나의 실측 `min(width, height)`을 두 문턱과 비교한다. `thresholds.inlineTargetMin`(기본 24px)은 본문 흐름 속 인라인 텍스트 링크에 적용되는 문턱이고, `thresholds.tapTargetMin`(기본 44px)은 버튼·입력·role 컨트롤 등 인라인이 아닌 컨트롤에 적용되는 문턱이다. 두 문턱은 서로 다른 판정 위계를 진다. 기준의 정본은 [accessibility.md](accessibility.md) §8이다.
- **하드 게이트 — WCAG 2.5.8 층.** 인라인이 아닌 대상 중 `min(width, height) < 24px` 인 것은 곧바로 실패가 아니다. 게이트가 간격 예외를 계산한다 — 각 대상의 바운딩박스 중심에 24px 지름 원을 두었을 때, 그 원이 다른 대상의 박스나 다른 미달 대상의 원과 겹치지 않으면 예외로 통과한다. 겹치면 `WCAG 2.5.8:` 로 표시된 실패다. 문장 속 인라인 링크는 WCAG 의 Inline 예외라 이 층에서 검사하지 않는다. Equivalent(같은 기능의 다른 컨트롤)·User Agent Control·Essential 예외는 게이트가 자동으로 판정하지 못하므로, 해당한다고 보면 근거를 design.md 에 적고 수동 평가로 남긴다 [WCAG-TARGET].
- **프로젝트 계약 층.** 컨트롤 `< thresholds.tapTargetMin`(기본 44px, HIG 44pt·M3 48dp 관례)과 인라인 링크 `< thresholds.inlineTargetMin`(기본 24px)은 WCAG 위반이 아니라 계약 미달이다. `tapTargetPolicy: "contract"`(기본값, 이전 동작과 같다)이면 `계약 44px:` 로 표시해 실패로 보고하고, `"wcag"` 이면 통과 detail 에 "계약 참고"로만 남긴다. 모바일 앱 수준을 요구하지 않는 프로젝트(예: 랜딩 페이지)는 design.md 에 근거를 적고 `"wcag"` 로 바꾸거나 문턱을 조정한다.
- **하드 게이트 — WCAG 2.5.8 층.** 인라인이 아닌 대상 중 `min(width, height) < 24px` 인 것은 곧바로 실패가 아니다. 게이트가 간격 예외를 계산한다 — 각 대상의 바운딩박스 중심에 24px 지름 원을 두었을 때, 그 원이 다른 대상의 박스나 다른 미달 대상의 원과 겹치지 않으면 예외로 통과한다. 겹치면 `WCAG 2.5.8:` 로 표시된 실패다. 문장 속 인라인 링크는 WCAG 의 Inline 예외라 이 층에서 검사하지 않는다. 게이트가 "문장 속"으로 보는 것은 computed `display` 가 `inline` 이고 같은 줄 흐름에 자신 밖의 글자가 있는 요소, 그리고 `tapTargetsInline` 선택자에 매칭되는 요소뿐이다. 내비·푸터에 홀로 선 링크와 아이콘 링크는 `<a href>` 여도 이 층에서 검사한다. Equivalent(같은 기능의 다른 컨트롤)·User Agent Control·Essential 예외는 게이트가 자동으로 판정하지 못하므로, 해당한다고 보면 근거를 design.md 에 적고 수동 평가로 남긴다 [WCAG-TARGET].
- **프로젝트 계약 층.** 컨트롤 `< thresholds.tapTargetMin`(기본 44px, HIG 44pt·M3 48dp 관례)과 링크·인라인 요소 `< thresholds.inlineTargetMin`(기본 24px)은 WCAG 위반이 아니라 계약 미달이다. 이 층에서 24px 문턱을 쓰는 것은 `<a href>` 링크, computed `display: inline` 요소, `tapTargetsInline` 매칭 요소다(리포트에 `(링크)`·`(인라인)`으로 표시한다). 나머지 컨트롤은 44px 문턱을 쓴다. `tapTargetPolicy: "contract"`(기본값, 이전 동작과 같다)이면 `계약 44px:` 로 표시해 실패로 보고하고, `"wcag"` 이면 통과 detail 에 "계약 참고"로만 남긴다. 모바일 앱 수준을 요구하지 않는 프로젝트(예: 랜딩 페이지)는 design.md 에 근거를 적고 `"wcag"` 로 바꾸거나 문턱을 조정한다.
인라인 판정 자체(요소가 `display:inline`인지, `<a href>`인지, `tapTargetsInline` 선택자에 매칭되는지)는 이 두 층 구분과 별개로 그대로 쓴다.
대상에서 빼는 것: `display: none`·`visibility: hidden` 요소, 그리고 시각적으로 숨긴 요소(2px 이하 상자를 `clip`·`clip-path`·`overflow`로 가린 스킵 링크·스크린리더 전용 입력). `label` 이 감싸거나 `for` 로 연결한 체크박스·라디오는 입력 상자가 아니라 `label` 영역으로 잰다.
## 렌더 측정 계약
@ -108,21 +146,107 @@ artifact 한 건에는 빌드·revision, viewport·zoom·스크롤·상태, 기
| 같은 폭, 다른 높이의 피사체 크롭 | 같은 폭의 이미지 장면을 짧은 viewport와 긴 viewport에서 header 역할·스크롤 위치를 같게 맞춘다 | 피사체와 행동의 안전 영역이 두 높이에서 의도대로 남는지 비교한다 | 폭만 기록해 짧은 화면에서 잘린 얼굴·문구를 놓친다 |
| 의도 변경 없는 낡은 expected 값 | 렌더가 달라졌지만 디자인 의도 변경 기록이 없는 시각 회귀를 만든다 | 기존 기대값을 유지하고 제품 결함 또는 측정기 오류를 분류한다 | diff를 없애려고 expected·baseline만 무단 갱신한다 |
## 게이트 실행
**기본은 프로젝트를 바꾸지 않고 돌리는 것이다.** 어느 경로든 게이트 설정·기준 화면·리포트는 먼저 프로젝트 밖 임시 디렉터리(하네스의 스크래치나 OS 임시 디렉터리)에 둔다. 프로젝트에 설정·devDependency·스크립트를 심는 것은 사용자가 동의했을 때만 한다(아래 "설치").
```
node <스킬>/tools/design-gate.mjs --url <주소> --out-dir <프로젝트 밖 임시 경로> # 설정 파일 없이. 프로젝트에 아무것도 쓰지 않는다
node <스킬>/tools/design-gate.mjs --init --config <임시 경로>/gate.config.json # 뷰·예산·문턱을 정해야 하면 임시 설정을 만든다
node <스킬>/tools/design-gate.mjs --config <임시 경로>/gate.config.json --out-dir <임시 경로>
```
- `--url` 만 주고 `--out-dir` 을 빼면 리포트 파일도 쓰지 않는다. `--url` 은 여러 번 줄 수 있다.
- 설정의 상대 `pages[].path` 는 실행한 디렉터리(cwd) 기준이다. 임시 설정에는 절대 경로나 `pages[].url` 을 적는다. 프레임워크 프로젝트는 빌드 결과를 띄운 주소(`url`)를 검사한다. `url` 만 준 페이지는 파일을 읽는 검사(죽은 선택자·토큰 위생)가 미검증으로 남으므로, 둘 다 재려면 `path` 와 `url` 을 같이 준다.
- 그 밖의 옵션: `--no-report`(리포트 파일 없음), `--strict`(미검증이 있으면 exit 3), `--static-only`(브라우저 없이 L0만), `--init`(파일이 있으면 덮어쓰지 않고 exit 2, `--force` 일 때만 덮어쓴다), `--update-baseline`(G15). 값 옵션은 `--키 값`·`--키=값` 둘 다 받는다. 전체 목록은 `--help` 가 정본이다.
**결과 상태는 넷이다.** `PASS`(쟀고 충족), `FAIL`(쟀고 미달), `미검증`(도구·입력이 없어 못 쟀다), `OFF`(설정으로 끈 검사, 집계에서 뺀다). 리포트 머리말은 실패가 있으면 "n건 실패", 실패 없이 미검증이 있으면 "검증 미완료", 둘 다 없을 때만 "전 항목 통과"이고, 집계 줄은 `충족 N / 미검증 M / 실패 K` 다.
| 종료 코드 | 뜻 | 루프에서 할 일 |
|---|---|---|
| 0 | 실패 0건 | 미검증 줄이 있으면 보고에 옮긴다. 0이 "전부 통과"는 아니다 |
| 1 | 실패 1건 이상 | 제품 결함으로 보고 고친다(재작업 상한 적용) |
| 2 | 사용법·설정 오류 | 인자·설정을 고친다. 재작업 횟수를 쓰지 않는다 |
| 3 | `--strict` 에서 미검증이 남았다 | 환경 문제다. 도구를 갖추거나 미검증으로 보고한다 |
**선택 도구는 실행한 디렉터리(cwd) → 게이트 스크립트 위치 순으로 찾는다.** `puppeteer-core`(브라우저 계층 전체), `axe-core`, `pixelmatch`·`pngjs`(시각 회귀), `playwright`(WebKit)가 그렇다. 스킬이 사용자 홈에 설치돼 있어도 프로젝트 디렉터리에서 실행하면 프로젝트의 `node_modules` 를 쓴다. 브라우저는 `CHROME_PATH` → 설정 `chromePath` → Chrome·Chromium·Edge 기본 경로 → `PATH` 순으로 찾는다. `puppeteer-core` 나 브라우저가 없으면 게이트는 죽지 않고 L0 결과를 남긴 뒤 브라우저 계층을 미검증 한 줄로 보고한다. `designpaca tools` 가 설치하는 playwright 만으로는 브라우저 계층이 돌지 않는다. 하네스에 브라우저 자동화 도구가 있으면([harness.md](harness.md) §6) 그 도구로 같은 항목을 재고, 게이트 때문에 `puppeteer-core` 를 설치하지 않는다.
### 검사별 측정 폭
`widths` 전부를 도는 것은 가로 오버플로뿐이다. "게이트 통과"가 무엇을 쟀다는 뜻인지는 검사마다 다르다.
| 검사 | `checks` 키 | 재는 폭(뷰포트) | 뷰 | 못 재면 |
|---|---|---|---|---|
| 가로 오버플로 | 항상 | `widths` 전부 × 높이 900 | 전 뷰 | — |
| 제목 존재·가시성, `h1MaxLines` | 항상 | 1440 | 전 뷰 | 선택자 없음은 실패 |
| 수축 / 리듬·트랙 | `stack` / `rhythm` | 1440 | 전 뷰 | — |
| 대비 | `contrast` | 1440, 기본 외형 하나 | 전 뷰 | 이미지·그라디언트 배경 위 글자는 "대비 미검증 n요소" |
| SEO/meta·theme-color | `meta` | 1440 | 기본 화면 | — |
| 글꼴 확대 h1 행렬 | `scaleMatrix` | 375·390 × `fontScales` | 기본 화면 | — |
| 브랜드 줄바꿈 | `wrap`(`brand` 가 있을 때) | 390 | 기본 화면 | 선택자 없음은 실패 |
| 터치 타깃 | `tapTargets` | 390 | 전 뷰 | — |
| 왼쪽 인셋(최소 시작점) | `leftInset` | 390 | 전 뷰 | 텍스트 없음은 미검증 |
| axe | `axe` | 1440 | 전 뷰 | `axe-core` 없음은 미검증 |
| 런타임(스크립트 예외·없는 로컬 파일·깨진 이미지) | `runtime` | 위 실행 전체 | — | — |
| 죽은 선택자 / 토큰 위생 | `deadCss` / `cssHygiene` | 파일(브라우저 불필요) | — | `path` 나 로컬 CSS 가 없으면 미검증 |
| stylelint·html-validate | `static`(기본 꺼짐) | 파일 | — | 도구 없음은 미검증 |
| 시각 회귀 | `visual`(기본 꺼짐) | 390·1440 | 기본 화면 | 기준 없음은 미검증 |
| WebKit 오버플로 | `webkit`(기본 꺼짐) | 390 | 기본 화면 | `playwright` 없음은 미검증 |
**게이트가 재지 않는 것.** 게이트가 통과해도 아래는 확인된 것이 아니다. 해당 preflight 행은 직접 재거나 미검증으로 보고한다.
- 1440 이외 폭의 대비, 다크 등 다른 외형의 대비(A3).
- 1920·2560의 구성비 — 측정 폭, 분할 히어로의 카피·미디어 rect(W5~W7). 그 폭에서는 가로 스크롤만 본다.
- 320 폭의 왼쪽 인셋과 "같은 축 ±10px"(M-E).
- 반지름 리터럴 1~2종. 3종 이상(`thresholds.radiusLiterals`)이거나 페이지 예산을 넘을 때만 실패다. `0`·`50%`·키워드·알약 값은 세지 않는다.
- 인라인 `<style>`·런타임 주입 CSS 의 죽은 선택자·토큰 위생. `<link rel="stylesheet">` 로 연결된 로컬 파일과 그 `@import` 만 읽는다. 다른 파일은 `staticContract` 로 지정한다.
- 키보드 도달·포커스 표시, 감소 모션, 성능 예산, 상태 완결성, 뷰 전환 뒤 스크롤 위치. 프로젝트 E2E(L5·L6)와 수동 확인의 몫이다.
뷰를 설정에 적었으면 게이트는 전환을 단언한다. `[viewAttribute="뷰 이름"]` 버튼을 찾지 못하거나 `viewAttribute` 없이 뷰 이름만 적으면 그 뷰는 `뷰 전환` 실패 한 줄로 끝난다(G8).
### 설정 키
| 키 | 기본값 | 뜻 |
|---|---|---|
| `pages[].name` | `page1`… | 리포트에 찍히는 이름 |
| `pages[].path` / `pages[].url` | — | 로컬 HTML / 떠 있는 주소. 하나는 필수다. 같이 주면 `url` 을 렌더하고 `path` 로 파일 검사를 한다 |
| `pages[].h1`, `pages[].h1MaxLines` | `"h1"`, 없음 | 제목 선택자(다중 뷰 앱은 활성 뷰의 제목으로 좁힌다), 줄 수 계약(양의 정수일 때만 검사) |
| `pages[].brand` | 없음 | 브랜드 선택자. 있을 때만 줄바꿈을 잰다 |
| `pages[].viewAttribute`, `pages[].views` | 없음 | 뷰 전환 버튼의 속성 이름과 뷰 이름 목록. 전환이 없으면 적지 않는다 |
| `pages[].staticContract` | 없음 | `selectorFiles`(죽은 선택자를 볼 CSS), `selectorDocuments`(대조할 HTML·JS), `hygieneFiles`(리터럴을 셀 CSS), `tokenFiles`(토큰 권위 파일), `budgets.colorLiterals`·`budgets.radiusLiterals` |
| `widths` | 320·390·768·1440·1920·2560 | 가로 오버플로를 재는 폭. [preflight.md](preflight.md) W1의 기본 매트릭스와 같다 |
| `fontScales` | 1·1.3 | 글꼴 확대 배율 |
| `checks` | 위 표 | 검사별 켜기·끄기. `axe` 는 `"auto"` 또는 `false` |
| `thresholds` | `contrastNormal` 4.5, `contrastLarge` 3, `stackLines` 3, `radiusLiterals` 3, `tapTargetMin` 44, `inlineTargetMin` 24, `leftInsetMin` 12, `visualDiffPct` 0.1 | 문턱 |
| `tapTargetPolicy`, `tapTargetsInline` | `"contract"`, 없음 | `"wcag"` 이면 WCAG 2.5.8 실패만 실패다. 인라인 링크로 볼 선택자 목록 |
| `deadCssIgnore` | `["is-"]` | 동적으로 조립하는 클래스 접두사 |
| `load` | `waitUntil: "networkidle0"`, `timeoutMs: 30000` | 로드 대기. 폴링·SSE 가 있는 페이지는 `waitUntil` 을 `"load"` 로 바꾼다 |
| `chromePath`, `chromeArgs` | 없음 | 브라우저 실행 파일, 추가 실행 인자(예: `["--no-sandbox"]`) |
| `strict` | `false` | `true` 면 `--strict` 와 같다 |
죽은 선택자의 사용 판정은 낱말 일치다. `"card-" + kind` 처럼 조립하는 클래스는 `deadCssIgnore` 에 접두사를 적는다. 오탐이 난다고 검사를 끄지 말고 `staticContract` 와 `deadCssIgnore` 로 계약을 적는다.
## 설치 (프로젝트에 게이트 심기)
**사용자가 고른 경우에만 한다.** `gate.config.json`·devDependency·`verify` 스크립트를 프로젝트에 남기려면 무엇이 생기는지 한 줄로 말하고 동의를 받는다. 새 프로젝트를 만드는 전체 경로에서도 같다. 동의가 없으면 위 "게이트 실행"으로 끝낸다.
```
node <스킬>/tools/design-gate.mjs --init # gate.config.json 초안
# pages[].views 를 실제 뷰 목록으로, viewAttribute 를 data-view 등으로
npm i -D puppeteer-core # CHROME_PATH 또는 자동 탐색
package.json: "verify": "node <스킬>/tools/design-gate.mjs"
배포는 항상: npm run verify && build && deploy # verify 실패(exit 1)면 배포 불가
node <스킬>/tools/design-gate.mjs --init # gate.config.json 초안. 이미 있으면 덮어쓰지 않는다
<패키지 매니저> add -D puppeteer-core # 브라우저는 CHROME_PATH 또는 자동 탐색
package.json: "verify": "node tools/design-gate.mjs" # verify 가 exit 1 이면 배포하지 않는다
```
L1·L5 는 프로젝트 안에 `tools/unit/*.test.mjs`, `tools/exploratory.mjs` 로 보강하고 verify 체인에 묶는다. 근거와 완성 예는 designpaca 저장소 `apps/site/tools/` 가 정본 샘플이다.
- **패키지 매니저는 프로젝트의 락파일로 정한다**(`pnpm-lock.yaml`·`yarn.lock`·`bun.lock`·`package-lock.json`). 락파일이 없으면 사용자에게 묻는다. 다른 매니저로 설치해 두 번째 락파일을 만들지 않는다.
- **스크립트에 스킬 설치 경로를 박지 않는다.** `<스킬>` 자리의 절대 경로를 `package.json` 에 넣으면 다른 기기·CI에서 깨진다. 게이트를 `tools/` 로 복사해 상대 경로로 부르고, 복사한 버전을 `design.md` 에 적는다.
- `verify` 가 미검증까지 막아야 하면 `--strict` 를 붙이거나 설정에 `"strict": true` 를 적는다.
**리뷰 경로에서는 이 절을 실행하지 않는다** — 리뷰는 읽기 전용이고 동의를 구하지도 않는다([change-review.md](change-review.md) §1-1). 위 "게이트 실행"의 `--url`·`--out-dir` 로만 돌린다.
L1·L5 는 프로젝트 안에 `tools/unit/*.test.mjs`, `tools/exploratory.mjs` 로 보강하고 verify 체인에 묶는다. 완성 예는 designpaca 저장소의 `apps/site/tools/` 다(저장소 전용, 설치본에는 없다).
## SEO / meta 감사 (design-gate 내장)
조사 기준(2026): title 10~65자·유일, description 60~165자, viewport, `html lang`, charset, canonical(프로덕션), og:title/description/image/url, twitter:card, favicon, h1 유일, img alt 전부, JSON-LD 파싱. 데모·로컬 페이지는 canonical/robots 없음을 허용한다 — **검사는 맥락을 존중하되 나열 근거는 리포트에 남긴다.**
게이트가 단언하는 것: title(10~65자), description(60~165자), viewport, `html lang`, charset, og:title, og:image, twitter:card, favicon, h1 하나, img 의 `alt` 속성, JSON-LD 파싱, theme-color 와 실제 배경색의 일치. `og:description`·`og:url`·canonical·robots 와 페이지 사이의 title 유일성은 단언하지 않는다 — 프로덕션이면 직접 확인한다. SEO 가 범위 밖인 국소 수정·앱 내부 화면은 `checks.meta` 를 끄고 그 사실을 보고에 적는다.
## OS · 브라우저 특성 — 검사에 반영할 것
@ -131,54 +255,19 @@ L1·L5 는 프로젝트 안에 `tools/unit/*.test.mjs`, `tools/exploratory.mjs`
- **안드로이드**: 시스템 글꼴 확대는 제목·컨트롤을 수축시킬 수 있다. gate의 `fontScales`와 computed root 스케일은 회귀 신호일 뿐, px 고정 서체의 실제 OS 확대를 보장하지 않는다. 실제 접근성 확대와 WCAG 200% zoom은 별도 브라우저·기기 검증으로 보고한다.
- **폰트 스왑**: `font-display: swap` 의 CLS — 폴백 메트릭 보정(typography.md §4) 없이 검사 통과를 선언하지 않는다.
## 하니스 규칙 (테스트를 만드는 테스트)
1. **선택자는 스코프필수** — 같은 data-* 가 다른 뷰에 있으면 숨은 요소를 잡는다(`#as-table [data-assign]` 처럼).
2. **page.evaluate 클로저 금지** — Node 스코프 함수를 페이지 안에서 부르지 않는다. 측정 코드는 문자열로 주입.
3. **줄 수 측정의 오탐 두 종류** — 폰트 메트릭 top 차이(±5px 허용오차), 인라인 아이콘과 텍스트의 top 차이(텍스트 노드만 분리 측정). line box와 실제 글리프 잘림의 구분은 [렌더 측정 계약](#렌더-측정-계약)을 따른다.
4. **CSS 수정 직후 검증은 캐시 차단**(`setCacheEnabled(false)`) + 실제 로드된 `?v=` 확인.
5. **비전(모델) 검수는 '여부'가 아니라 '지점 평가'에만** — 여부는 계측으로.
6. **모달 닫힘 직후 포커스 강탈** — 뷰 전환+타깃 포커스는 한 evaluate 로, settle 후 유실 재포커스.
7. **게이트를 통과해도 리포트에 수치를 남긴다** — "ALL PASS"가 아니라 "무엇을 몇으로 확인했나".
8. **검사는 단언한다** — `pass("차터", st)` 처럼 상태를 상세로만 찍는 건 검사가 아니다. 실측 사고: 점수 입력에 "88"을 `type()` 하면 기존 값에 **덧붙어** "9288"이 되었고, 차터는 상태 텍스트만 인쇄해 통과했다. 기대값과 비교해 pass/fail 을 내라.
9. **number 입력은 선택-덮어쓰기가 안 된다** — 트리플 클릭 선택이 무시된다. 값을 비우고 타이핑하거나 `el.value = x; dispatchEvent(new Event("input"))` 로 설정한다.
10. **등장 모션과 캡처 상태를 맞춘다** — 모션 지속시간은 사용자 경험의 목적·입력 반응·감소 모션 계약으로 정한다. 시각 회귀는 그 값을 줄이라고 요구하지 않고, 실제 완료를 관측한 뒤 캡처한다. 임의 sleep으로 진행 프레임을 통과시키지 않으며 실제 모션 검수는 [렌더 측정 계약](#렌더-측정-계약)을 따른다.
11. **같은 diff 수치가 반복되면 패치가 적용되지 않은 것** — 수정하고도 시각 diff %가 소수점까지 동일하면 수정이 렌더에 닿지 않았다(캐시·캐스케이드 순서·잘못된 파일). 고치기를 반복하지 말고 적용 자체를 의심해라.
12. **페이지→노드 직렬화는 NaN 방어** — `getComputedStyle(el).fontSize` 등을 문자열 보간하면 "16px"이 NaN이 되고, 하니스는 NaN을 `null`로 받아 `"////px"` 같은 고장난 상세를 리포트한다. 수치는 항상 `parseFloat(x) || 0` 로 감싼다.
13. **`position: fixed` 요소의 보임 판정은 offsetParent 로 하지 마라** — fixed 는 offsetParent 가 `null`이다. display 계산값·높이로 판정한다.
14. **실기기(에뮬레이터) 검증은 텍스트 근거로** — `uiautomator dump` 의 `text="…"`에서 화면 제목·활성 탭을 읽어 단언한다. 스크린샷+비전은 "백 직후 전환 중 프레임"을 잡는 타이밍 오탐이 있다(실측 사고). 물리 폰이 붙어 있으면 `adb -s <emulator>` 로 대상을 한정한다.
15. **시각 기준 갱신은 의도 변경 리뷰를 동반한다** — "달라졌으니 갱신"으로 결함이 기준에 박힌다(실측: 왼쪽 인셋 결함 화면이 기준 갱신으로 '정상'이 되어 재발을 숨겼다). 갱신 전에 무엇이 의도적으로 바뀌었는지 한 줄씩 나열하고, 사용자 보고가 있던 화면은 수리 후 재촬영한다. 증거 보존과 실패 분류는 [렌더 측정 계약](#렌더-측정-계약)을 따른다.
16. **사용자 보고 스크린샷은 비전 요약이 아니라 픽셀 판정으로 해부한다** — 비전 모델의 픽셀 추정("~0px", "~16px")은 부정확하다. pngjs 등으로 화면을 가로 밴드로 나눠 각 밴드의 텍스트 시작 x를 직접 재면(보임 행만, 왼쪽 테두리 아티팩트 제외) "무엇이 어디서 어긋났는지"가 수치로 나온다. 실측: '제목이 가장자리에 붙는다'는 보고가 앱바 24px·제목 16px의 리듬 불일치로 확정됐다.
17. **새 검사는 수정 전 코드로 RED 재현 후 GREEN** — 검사 자체의 측정 하자(오탐)도 이렇게 잡는다. 실측: 왼쪽 인셋 검사 초안이 ①`display:none` 요소의 rect=0, ②전폭 컨테이너의 보더박스(텍스트가 아님)로 38개 오탐 — RED 단계에서 발견해 교정했다.
18. **시각 E2E는 기본 화면만 찍지 않는다** — URL별 390·1440 기본 화면에 더해 결과가 바뀐 입력, 오류/검토, 모달/차단 상태를 찍는다. 하니스는 파일 개수·가로세로·0바이트·해시 중복을 단언하고, 에이전트는 파일을 실제로 열어 시장 적합성·위계·크롭·반복을 판정한다. 스크린샷을 저장했다는 사실만으로 PASS하지 않는다.
19. **absolute/fixed 자식은 containing block을 잰다** — 모바일 미디어쿼리에서 부모의 `position`을 바꿨다면 각 절대 배치 자식의 `offsetParent` 또는 기준 rect가 의도한 컨테이너인지 단언한다. `overflow-x:hidden`으로 누수를 숨기지 않는다.
20. **정적 검사는 재귀 입력과 대상 수를 잠근다** — 서브페이지가 생기는 저장소에서 한 단계 glob은 금지한다. 실행 전에 발견한 HTML/CSS/JS 파일 수를 출력하고 0개 또는 이전 계약보다 적으면 실패시킨다.
21. **시나리오·단언·캡처 수를 hard-lock한다** — 대규모 E2E는 `EXPECTED_SCENARIOS`, `EXPECTED_ASSERTIONS`, `EXPECTED_CAPTURES`를 둔다. 리팩터링으로 검사가 삭제돼도 전체가 조용히 GREEN이 되지 않게 한다. 수를 채우기 위한 중복 단언은 금지하고 공통·정상·오류·부작용·접근성·시각 상태로 분류한다.
22. **사용자 승인과 검증 결과는 다른 상태다** — 사용자가 방향 게이트를 자율 승인했어도 기계·기능·시각 게이트는 면제되지 않는다. 승인됨/검증됨을 보고서에서 별도 표기한다.
23. **혼합 액션 행은 직접 자식 hitbox 중심을 비교한다** — 같은 액션 묶음·같은 행의 직접 자식인 채운 버튼과 텍스트 링크가 둘 다 44px 이상인지, `rect.top + rect.height / 2` 차이가 브라우저 CSS 픽셀 2px 이하인지 단언한다. 중첩 아이콘·배지나 모바일 세로 스택은 비교 대상이 아니다. `align-items:center` 선언 존재는 검사가 아니다.
24. **연속 섹션 토폴로지를 목록화한다** — 섹션별 `역할 / DOM 골격 / 회색조 실루엣`을 적는다. 역할이 다른 연속 3개가 같은 `제목+목록` 골격이면 시각 RED다. 반복 데이터처럼 같은 골격이 맞는 경우는 이유를 기록한다.
25. **조건부 패널의 닫힘은 트랙 회수까지 단언한다** — 데스크톱 분할 작업대는 `display:none`만 검사하지 않는다. 인스펙터가 닫히면 주 표면의 오른쪽 끝과 작업대 끝 차이가 CSS 픽셀 2px 이하인지, 다시 선택하면 분할 트랙이 복원되는지 잰다. 모바일 시트·전체 화면 패널은 트랙 대신 닫힘·배경 복귀·포커스 복원을 검증한다.
26. **진실 라벨은 개수가 아니라 의미 범위를 단언한다** — 같은 상태·출처·행동 가능성을 공유하는 자식은 제목과 경계가 분명한 가장 가까운 공통 부모가 한 번 소유하고 프로그램적으로 연결한다. 상태가 다른 자식만 예외 라벨을 둔다. 같은 공개 문구가 한 그룹의 모든 자식에 복제되면 범위 설계를 다시 본다.
27. **보이는 입력과 판정 의존성을 같은 SSOT로 잠근다** — 폼의 사용자 조건 이름 집합과 순수 판정 함수가 참조하는 사용자 조건 집합을 비교한다. 숨은 기본값을 사용자 선호로 세지 않고, 보이는 입력을 무시하지 않으며, 분석 전 완료 결과가 없고 복합 비용 일부로 적합을 확정하지 않는지 단언한다.
28. **공개와 관리 콘텐츠 소유권을 테스트한다** — 공개 DOM에는 내부 ID·원문 출처 원장·규제 채널 상수·감사 로그를 복제하지 않는다. 대신 사용자 행동을 막거나 바꾸는 제한과 해석 가능한 이유는 남긴다. 관리자는 그 요약을 재현하는 출처·검증 이벤트·판정 기록을 가진다.
29. **색·가시성 측정기는 새 CSS 문법과 보이는 텍스트를 구분한다** — computed color가 CSS Color 4 `color(srgb r g b / a)`이면 0~1 채널을 0~255으로 변환하고, 반투명 배경은 자식에서 부모 방향으로 올바르게 합성한다. `Range.selectNodeContents()`로 sr-only·`aria-hidden` 장식·빈 overlay까지 합산하지 말고 실제 보이는 text node의 rect만 잰다. 두 경우 모두 UI를 고치기 전에 RED fixture → 파서 수정 → 같은 fixture GREEN 순서로 검사기 자체를 검증한다.
30. **native dialog의 위치를 UA 스타일에 맡기지 마라** — 전역 reset은 dialog의 기본 margin을 지울 수 있다. open 상태에서 `(left + width/2, top + height/2)`와 viewport 중심의 오차, 화면 안 rect, `html` overflow, Escape 뒤 복귀를 390·1440·2560에서 측정한다. 내용이 길면 dialog 또는 내부 본문 하나만 scroll owner여야 한다.
31. **초광폭 히어로는 비율 계약을 잰다** — 2560 같은 실제 wide viewport에서 hero 폭/정렬, copy 최소 폭, media 최대 폭, 다음 정보 레일 폭을 함께 단언한다. media만 넓고 카피가 가늘어지는 상태를 `overflow=0`으로 통과시키지 않는다. 절대 수치는 페이지별 토큰이 소유하며, 풀블리드 편집 사진 예외는 이유를 기록한다.
32. **캐시 무효화도 사용자 화면에서 단언한다** — static CSS/JS를 덮어쓴 뒤 HTML만 새 URL이면 브라우저는 이전 stylesheet를 계속 쓴다. stylesheet와 `@import` 의 버전을 같이 바꾸고, 공개 URL에서 실제 href·새 토큰/핵심 computed value를 검사한다. HTTP 200과 새 HTML만으로 배포 성공을 선언하지 않는다.
33. **썸네일은 안정한 프레임만 저장한다** — 캡처 전 local HTTP 경로에서 `document.fonts.ready`·모든 이미지 `decode()`·bounded settle을 기다리고, `prefers-reduced-motion: reduce`를 적용한다. 그 뒤 video/audio·CSS/WAAPI·rAF canvas를 멈추고, local 4xx·pageerror·깨진 이미지가 하나라도 있으면 실패시킨다. 원리는 `animation:none`으로 초기 상태를 재현하는 것이 아니라 **완성된 한 프레임을 고정**하는 것이다. 이는 실제 모션 검수와 별도다. 카드·OG·핵심 상태가 같은 계약을 공유하는지 파일 치수·0바이트·시각 검수까지 단언한다.
34. **초광폭 text-media split은 가족별로 잰다** — hero가 통과했다고 과정·추천·가맹 분할이 통과한 것이 아니다. 서로 다른 DOM/여백 규칙을 가진 각 split root에서 1920·2560의 rail, copy/media rect, heading의 실제 내용 폭과 줄 수를 hard fail로 기록한다. wide override는 양쪽 logical padding을 명시하고, 상위 container의 `max-width`가 specificity 때문에 named stage를 다시 줄이지 않는지 단언한다. grid 바깥 rect가 정상이어도 text content가 한 글자 열로 수축하면 실패다.
35. **커스텀 제스처 컨트롤은 완주까지 시험한다** — 언제: 슬라이더·드래그 표면·스크롤형 컨트롤 스트립이 있을 때. 레이아웃 검증과 별개로 (a) 탭 응답과 대상 입력방식으로 드래그를 끝까지(시작만이 아니라) 확인한다 (b) 컨트롤을 가로지르는 스와이프와 컨트롤 축을 따르는 드래그를 구분해 둘 다 시험한다 — 레이아웃 통과가 제스처 통과를 보장하지 않고, 둘 다 조용히 실패할 수 있다 (c) 증거 출처(에뮬레이션·합성 터치·실기기, 엔진명 Chromium≠Safari)를 명시한다. 접근 불가한 하드웨어는 **보고된 갭**으로 정직하게 남긴다 — 차단 사유가 아니다 [SKILL-IMPECCABLE].
36. **증거 방향성을 지킨다** — 런타임 동작이 결과를 좌우하는 항목은 시각 외관만 보고 코드-레벨 finding을 내지 않고, 소스코드만 보고 시각 finding을 내지 않는다. 각 finding은 `path/to/file:line`과 그 항목이 실제로 필요로 하는 증거(렌더 확인 또는 소스 대조)를 함께 남긴다 [SKILL-BETTER-INTERFACE].
## 리포트 양식
```
하드 게이트: 충족 N / 미검증 M / 차단 K · 방향: 열린 Direction d건 · 시각 루브릭: <점수>
프로젝트 계약: 충족 n / 미검증 m / 위반 k
게이트: 기능·접근성 자동 검사 / 시각 평가를 별도 보고 · 총 Ns
| 범주 | 결과 | 항목 | 소요 | 근거 |
```
결과 값은 PASS/FAIL 둘이 아니라 셋이다. 실행할 수 없는 체크(도구·권한·환경 제약으로 확인 못 한 것)는 FAIL로 적지 않고 **미검증(Not verified)**으로 따로 적는다. 미검증은 finding 개수에 들어가지 않고, 통과로 추정하지도 않는다. 통과·실패·미검증 세 줄을 분리해 나열한다 [SKILL-BETTER-INTERFACE].
첫 줄의 문자열과 집계·납품 판정 규칙은 위 "완료 보고 첫 줄 — 정본"이 정본이다. 둘째 줄은 프로젝트 계약을 대조한 경로에서만 적는다.
결과 값은 PASS/FAIL 둘이 아니라 셋이다. 실행할 수 없는 체크(도구·권한·환경 제약으로 확인 못 한 것)는 FAIL로 적지 않고 **미검증(Not verified)**으로 따로 적는다. 미검증은 finding 개수에 들어가지 않고, 통과로 추정하지도 않는다. 통과·실패·미검증 세 줄을 분리해 나열한다 [SKILL-BETTER-INTERFACE]. 게이트 도구가 `OFF` 로 찍은 검사(설정으로 끈 것)가 덮던 행도 통과가 아니다. 직접 재거나 미검증으로 보고한다.
개별 발견 사항의 심각도·What/Why/Fix 서식은 [critique.md](critique.md)를 따른다 — 위 표는 자동 게이트 집계용이고 개별 리뷰 보고 형식은 별도다. diff·PR 단위 변경 리뷰(스코프 계산·제거된 쪽 읽기·Introduced/Regression/Pre-existing 분류)는 [change-review.md](change-review.md)를 따른다.
실패 상세는 원문 테일 포함. 시각 평가는 같은 viewport·상태·자산을 전후로 보존해 위계·브랜드·타입·구도·이미지·리듬을 기록한다. 사용자 성과의 인과나 취향 우승을 검증 없이 주장하지 않는다. 리포트 파일(gate-report.md / verify-report.md)은 커밋 대상이다.
실패 상세는 원문 테일 포함. 시각 평가는 같은 viewport·상태·자산을 전후로 보존해 [critique.md](critique.md) §2-1 루브릭 축별 점수와 근거를 기록한다. 사용자 성과의 인과나 취향 우승을 검증 없이 주장하지 않는다. 프로젝트에 게이트를 심은 경우 리포트 파일(gate-report.md / verify-report.md)은 커밋 대상이다. 프로젝트 밖에서 돌린 실행의 리포트는 프로젝트에 넣지 않고 응답으로 전달한다.