designpaca/packages/skill/references/audit-gate.md
Yun Chan 88c11682b1 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
2026-10-06 16:55:38 +09:00

273 lines
36 KiB
Markdown
Raw Permalink 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.

# audit-gate — 자율 검증 폐쇄 루프
5단계(감사)를 **완료 선언의 게이트**로 만드는 문서다. 핵심 원칙은 하나다:
> **완료는 기능·프로젝트 계약·렌더 시각 평가가 함께 확인된 상태다. 사용자를 QA 로 쓰지 않는다.**
"고쳤다"는 말의 근거는 검증 리포트의 수치와 실제 렌더 평가다. 기능·접근성 자동 검사와 시각 평가는 별도 결과로 보고한다. 시각 평가의 축과 판정 기준은 [critique.md](critique.md) §2-1 시각 비평 루브릭을 쓴다.
각 검사가 어떤 실제 사고에서 왔는지(사고 목록)와 테스트를 새로 쓸 때의 게이트 규칙 G1~G36 전문은 [audit-gate-rules.md](audit-gate-rules.md)에 있다. 게이트를 확장하거나 사용자 보고 결함을 해부할 때만 연다.
## 이 문서를 읽는 법
| 상황 | 읽을 곳 |
|---|---|
| 실패를 몇 번까지 고치는지, 언제 멈추고 사용자에게 묻는지 | 루프와 재작업 상한 |
| 도구가 없어 검사를 못 했을 때 보고하는 법, 완료 보고 첫 줄 | 미검증은 통과가 아니다 |
| 매 감사에서 지킬 게이트 규칙 요약 | 매 감사에서 지키는 규칙 |
| 검사 계층의 구성 | 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계층
`tools/design-gate.mjs` 가 한 파일로 돌리는 것과 프로젝트가 보강하는 것이 있다.
| 계층 | 도구 | 비고 |
|---|---|---|
| L0 정적 | `design-gate.mjs` — 죽은 선택자·토큰 위생(기본 켜짐), stylelint + html-validate(`checks.static`, 기본 꺼짐) | 문법·구문 위반과 죽은 코드를 브라우저 켜기 전에. 브라우저가 없어도 이 계층의 결과는 남는다 |
| L1 단위 | 순수 계산 로직 추출(calc.js) + 경계값 테스트 | 프로젝트별 작성 — 0명/만석/초과/빈배열. **입력 검증·신청 가능 같은 '판정'(가능/불가+사유)도 여기 둔다** — UI 는 판정을 문장으로 번역만 |
| 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 | 실사용자 여정 재현 — 페르소나의 하루·장기 과업 포함 | 프로젝트별 작성. **기대값 단언 필수**(G8), 백 키·시트·양방향 흐름은 [mobile-app-ux.md](mobile-app-ux.md) 패턴 기준. 시나리오를 먼저 쓰면 미구현이 수치로 드러난다 |
## 접근성 감사 루프
**하드 게이트.** 접근성 감사는 자동 점수 하나로 끝나지 않는다. 브라우저 자동화 도구에 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"`(기본값)일 때 `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 컨트롤 등 인라인이 아닌 컨트롤에 적용되는 문턱이다. 두 문턱은 서로 다른 판정 위계를 진다. 기준의 정본은 [accessibility.md](accessibility.md) §8이다.
- **하드 게이트 — 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: none`·`visibility: hidden` 요소, 그리고 시각적으로 숨긴 요소(2px 이하 상자를 `clip`·`clip-path`·`overflow`로 가린 스킵 링크·스크린리더 전용 입력). `label` 이 감싸거나 `for` 로 연결한 체크박스·라디오는 입력 상자가 아니라 `label` 영역으로 잰다.
## 렌더 측정 계약
자동 측정은 실제 장면을 재현할 때만 의미가 있다. 새 범용 JS 엔진을 만들라는 뜻이 아니다. 프로젝트의 기존 E2E·시각 회귀 도구에 아래 계약을 적고, 페이지와 기능의 위험에 맞는 단언만 구현한다.
### 1. 장면과 레이아웃
각 artifact에는 빌드·revision 식별자, URL, viewport의 너비·높이·종횡비·zoom, 스크롤 위치, UI 상태, 로드한 폰트·이미지·영상 자산을 함께 남긴다. 폭만 적어서는 같은 폭에서 높이가 달라진 크롭, 짧은 화면의 고정 크롬, 확대에 따른 줄바꿈을 재현할 수 없다. 짧은 가로·긴 세로·확대가 결과에 영향을 주면 대표 장면에 포함한다. 배경 위치, 분기 종횡비, 헤더 높이, viewport 단위 선택은 프로젝트 계약으로 정한다.
화면 맞춤 hero는 헤더가 overlay인지, 문서 흐름에서 높이를 차지하는지 먼저 구분한다. 그 역할을 포함해 첫 화면의 남은 높이를 재고, 내용이 고정 높이보다 커지면 자연스럽게 확장되어야 한다. 모든 페이지 섹션에 `100vh`를 강요하지 않는다. 컨테이너의 `overflow=0`만으로 피사체·카피·행동의 안전 영역이 보존됐다고 판정하지 말고, 실제 크롭과 인접 콘텐츠의 겹침을 화면에서 확인한다.
**언제**: 프로젝트가 backdrop-filter를 쓰는 표면(유리·오버레이·스크림 헤더)을 가질 때. 그런 표면은 뒤로 스크롤되는 콘텐츠에 따라 대비가 달라지므로 한 장면만 재고 통과를 선언하지 않는다. 뒤에 올 수 있는 콘텐츠 중 가장 밝은 장면과 가장 어두운 장면 둘 다를 캡처해 최악·최선 대비를 각각 기록한다 [SKILL-BETTER-COLORS].
### 2. 모션과 상태
reveal·전환은 실제 UI 스크롤·클릭·키보드 이벤트로 장면을 먼저 활성화한다. 그 장면에 필요한 폰트·이미지가 준비된 뒤 대상과 전환에 관여하는 조상 요소의 유한 전환 완료를 관측하고, 프로젝트가 의도한 최종 가시성·색·대비·위치에 도달했는지 확인한다. 최종 가시성은 `opacity: 1`을 일괄 요구하는 값이 아니다. 의도한 반투명 텍스트도 조상 opacity 합성을 포함한 실제 대비와 상태 계약으로 판정한다. 정한 상한 시간 안에 끝나지 않으면 실패 또는 미검증으로 기록하고 원인을 남긴다.
계속 움직이는 배경·카운터·캔버스는 별도 상태 계약으로 관측한다. 모든 페이지 애니메이션이 끝날 때까지 기다리는 검사는 만들지 않는다. 정지 상태의 대비, 일반 설정에서 trigger와 완료가 실제로 작동하는지, `prefers-reduced-motion` 경로를 각각 보고한다. 안정한 정상 상태의 대비가 영구 애니메이션의 저대비나 발화 실패까지 통과시키지 않는다.
고정 sleep, 검사 중 `opacity: 1` 강제, `animation: none` 주입으로 제품 모션 검사를 GREEN으로 만들지 않는다. 썸네일·OG용 정지 프레임은 별도 산출물이다. 그 경우에는 완성된 프레임을 고정하고 축소 모션을 적용할 수 있지만, 실제 동작 검수의 대체가 아니다.
### 3. 텍스트·클리핑·폰트
`Range.getClientRects()`는 선택 범위의 요소 box와 텍스트 font metric에 따른 사각형이며 글리프 잉크 경계가 아니다. heading·line box 밖 rect만으로 실제 잘림을 확정하지 않는다. viewport 밖 rect는 가시성 후보로 계속 검사하되, viewport와 실제 `overflow` clipping 조상을 함께 추적한다. `clip-path`·mask·transform처럼 rect만으로 설명하기 어려운 경계는 실제 렌더를 열어 수동 판정한다. 텍스트 후보가 보이면 인접 콘텐츠와 겹치거나 읽기 영역을 침범하는지도 함께 확인한다. [CSSOM View의 `Range.getClientRects()` 편집 초안](https://drafts.csswg.org/cssom-view/#dom-range-getclientrects)을 2026-09-20에 확인했다.
`document.fonts.ready`와 FontFaceSet 상태는 로드 준비의 근거일 뿐, 모든 화면 문자가 의도한 폰트로 그려졌다는 증명은 아니다. 필요한 문자·언어·숫자·기호와 웨이트를 실제 카피로 렌더하고, 브라우저가 제공하는 rendered-font 확인 또는 필요한 글리프 검증과 화면 관찰을 결합한다. 폰트 선택·subset·fallback의 세부 절차는 [typography.md](typography.md)를 따른다.
### 4. 실패와 증거
실패는 제품 결함, 측정기 오류, 계약 변경으로 분류한다. 실패 artifact와 원문 결과는 보존한다. 의도 변경 근거와 전후 렌더 증거 없이 기대값을 고치거나 assertion을 완화해 통과시키지 않는다. 측정기가 틀렸다면 UI를 먼저 고치지 말고 RED 재현을 남겨 측정기를 고친 뒤 같은 사례를 GREEN으로 확인한다.
artifact 한 건에는 빌드·revision, viewport·zoom·스크롤·상태, 기대값과 관측값, 자동 결과, 실제 화면의 시각 판정을 같이 남긴다. 수치와 스크린샷이 서로 다른 장면이면 증거가 아니다.
| 작은 재현 예제 | 재현 조건 | 정상 기대 | 오판 / 금지되는 처리 |
|---|---|---|---|
| reveal 진행 중 / 완료 뒤 저대비 | 실제 스크롤로 reveal을 시작한 직후와 유한 전환 종료 뒤를 각각 캡처한다 | 진행 중 프레임은 완료 판정에서 제외하고, 종료 뒤에는 의도한 색·합성 대비와 가시성을 확인한다 | sleep 한 번 뒤 진행 프레임을 PASS로 쓰거나, 종료했지만 실제 색 대비가 부족한 텍스트를 통과시킨다 |
| line box 넘침 / 실제 hidden 조상 clip | 같은 긴 문장의 실제 잉크가 heading·line box 밖으로 나가게 두고, 한쪽은 `overflow: visible`, 다른 쪽은 잉크가 경계를 넘는 `overflow: hidden` 조상 안에 둔다 | 전자는 실제 잉크와 인접 영역을 보고 보존 여부를 판정하며, 후자는 실제 clip을 화면에서 확인한다 | Range rect 하나로 두 사례를 모두 잘림 또는 모두 정상으로 확정한다 |
| 같은 폭, 다른 높이의 피사체 크롭 | 같은 폭의 이미지 장면을 짧은 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 초안. 이미 있으면 덮어쓰지 않는다
<패키지 매니저> add -D puppeteer-core # 브라우저는 CHROME_PATH 또는 자동 탐색
package.json: "verify": "node tools/design-gate.mjs" # verify 가 exit 1 이면 배포하지 않는다
```
- **패키지 매니저는 프로젝트의 락파일로 정한다**(`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 내장)
게이트가 단언하는 것: 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 · 브라우저 특성 — 검사에 반영할 것
- **Windows**: 스크롤바가 오버레이가 아니다 — `overflow-x:auto` 짝에는 `overflow-y:hidden` + `scrollbar-width` 검토. 스크롤바 자체가 레이아웃을 민다.
- **iOS/Safari(WebKit)**: `100vh` 주소창 문제(`100dvh`), 텍스트 인플레이션(`-webkit-text-size-adjust:100%`), WebKit 만의 flex/행높이 차이 → L4 로 재검.
- **안드로이드**: 시스템 글꼴 확대는 제목·컨트롤을 수축시킬 수 있다. gate의 `fontScales`와 computed root 스케일은 회귀 신호일 뿐, px 고정 서체의 실제 OS 확대를 보장하지 않는다. 실제 접근성 확대와 WCAG 200% zoom은 별도 브라우저·기기 검증으로 보고한다.
- **폰트 스왑**: `font-display: swap` 의 CLS — 폴백 메트릭 보정(typography.md §4) 없이 검사 통과를 선언하지 않는다.
## 리포트 양식
```
하드 게이트: 충족 N / 미검증 M / 차단 K · 방향: 열린 Direction d건 · 시각 루브릭: <점수>
프로젝트 계약: 충족 n / 미검증 m / 위반 k
게이트: 기능·접근성 자동 검사 / 시각 평가를 별도 보고 · 총 Ns
| 범주 | 결과 | 항목 | 소요 | 근거 |
```
첫 줄의 문자열과 집계·납품 판정 규칙은 위 "완료 보고 첫 줄 — 정본"이 정본이다. 둘째 줄은 프로젝트 계약을 대조한 경로에서만 적는다.
결과 값은 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·상태·자산을 전후로 보존해 [critique.md](critique.md) §2-1 루브릭 축별 점수와 근거를 기록한다. 사용자 성과의 인과나 취향 우승을 검증 없이 주장하지 않는다. 프로젝트에 게이트를 심은 경우 리포트 파일(gate-report.md / verify-report.md)은 커밋 대상이다. 프로젝트 밖에서 돌린 실행의 리포트는 프로젝트에 넣지 않고 응답으로 전달한다.