feat(skill): absorb external design skills, restore interview gate, add review route

- Restore the step-0 interview as a mechanical gate the skill explicitly
  depends on; add harness.md (per-harness question tools, limits,
  fallbacks) and brief-interview.md (slots, question cards, rounds).
- Add 10 reference docs absorbed from external design skills
  (accessibility, interaction-feel, elevation, color, icons, product-copy,
  component-systems, critique, change-review, print-email) and extend
  existing references.
- Add a review-only route and two hard-gate clauses (truncated content
  reachability, three-flashes limit).
- design-gate: split tap targets into WCAG 2.5.8 and 44px contract layers,
  run axe-core when available, and fix false positives found on a real
  site (decorative alt="", stacked wordmark line count, url-only pages).
- lint-skill: fail if the interview gate section or its links disappear.
- Ship agents/openai.yaml and THIRD_PARTY_NOTICES.md.
This commit is contained in:
Yun Chan 2026-09-24 13:26:03 +09:00
parent 79e79c120b
commit 6805fb2be7
37 changed files with 5688 additions and 128 deletions

View file

@ -13,7 +13,7 @@
| 사고 (실제) | 검사 |
|---|---|
| CSS 닫는 중괄호 누락 — 이후 규칙 절반이 죽은 채 렌더 | stylelint(구문) + 중괄호 균형 |
| 브랜드가 모바일에서 1글자 폭으로 수축, 세로로 쌓임 | 수축 탐지(짧은 라벨 n줄 이상) + 브랜드 1줄 |
| 브랜드가 모바일에서 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 |
@ -54,6 +54,21 @@
| L5 탐색 | 키보드 완전 통과 + 사용자 여정 차터 | 프로젝트별 작성 — 마우스 금지 |
| L6 시나리오 E2E | 실사용자 여정 재현 — 페르소나의 하루·장기 과업 포함 | 프로젝트별 작성. **기대값 단언 필수**(하니스 규칙 8), 백 키·시트·양방향 흐름은 `mobile-app-ux.md` 패턴 기준. 실측: RED 1,742 단언 중 318 실패가 신규 기능에 정확히 집중 — 시나리오를 먼저 쓰면 미구현이 수치로 드러난다 |
## 접근성 감사 루프
**하드 게이트.** 접근성 감사는 자동 점수 하나로 끝나지 않는다. 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].
**선택 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)**으로 보고한다 — 통과로 추정하지 않는다. 상태값의 뜻은 [리포트 양식](#리포트-양식)을 따른다.
## 터치 타깃 — 두 층 판정
L2의 터치 타깃 검사는 컨트롤 하나의 실측 `min(width, height)`을 두 문턱과 비교한다. `thresholds.inlineTargetMin`(기본 24px)은 본문 흐름 속 인라인 텍스트 링크에 적용되는 문턱이고, `thresholds.tapTargetMin`(기본 44px)은 버튼·입력·role 컨트롤 등 인라인이 아닌 컨트롤에 적용되는 문턱이다. 두 문턱은 서로 다른 판정 위계를 진다.
- **하드 게이트 — 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"` 로 바꾸거나 문턱을 조정한다.
인라인 판정 자체(요소가 `display:inline`인지, `<a href>`인지, `tapTargetsInline` 선택자에 매칭되는지)는 이 두 층 구분과 별개로 그대로 쓴다.
## 렌더 측정 계약
자동 측정은 실제 장면을 재현할 때만 의미가 있다. 새 범용 JS 엔진을 만들라는 뜻이 아니다. 프로젝트의 기존 E2E·시각 회귀 도구에 아래 계약을 적고, 페이지와 기능의 위험에 맞는 단언만 구현한다.
@ -64,6 +79,8 @@
화면 맞춤 hero는 헤더가 overlay인지, 문서 흐름에서 높이를 차지하는지 먼저 구분한다. 그 역할을 포함해 첫 화면의 남은 높이를 재고, 내용이 고정 높이보다 커지면 자연스럽게 확장되어야 한다. 모든 페이지 섹션에 `100vh`를 강요하지 않는다. 컨테이너의 `overflow=0`만으로 피사체·카피·행동의 안전 영역이 보존됐다고 판정하지 말고, 실제 크롭과 인접 콘텐츠의 겹침을 화면에서 확인한다.
**언제**: 프로젝트가 backdrop-filter를 쓰는 표면(유리·오버레이·스크림 헤더)을 가질 때. 그런 표면은 뒤로 스크롤되는 콘텐츠에 따라 대비가 달라지므로 한 장면만 재고 통과를 선언하지 않는다. 뒤에 올 수 있는 콘텐츠 중 가장 밝은 장면과 가장 어두운 장면 둘 다를 캡처해 최악·최선 대비를 각각 기록한다 [SKILL-BETTER-COLORS].
### 2. 모션과 상태
reveal·전환은 실제 UI 스크롤·클릭·키보드 이벤트로 장면을 먼저 활성화한다. 그 장면에 필요한 폰트·이미지가 준비된 뒤 대상과 전환에 관여하는 조상 요소의 유한 전환 완료를 관측하고, 프로젝트가 의도한 최종 가시성·색·대비·위치에 도달했는지 확인한다. 최종 가시성은 `opacity: 1`을 일괄 요구하는 값이 아니다. 의도한 반투명 텍스트도 조상 opacity 합성을 포함한 실제 대비와 상태 계약으로 판정한다. 정한 상한 시간 안에 끝나지 않으면 실패 또는 미검증으로 기록하고 원인을 남긴다.
@ -150,6 +167,8 @@ L1·L5 는 프로젝트 안에 `tools/unit/*.test.mjs`, `tools/exploratory.mjs`
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].
## 리포트 양식
@ -158,4 +177,8 @@ L1·L5 는 프로젝트 안에 `tools/unit/*.test.mjs`, `tools/exploratory.mjs`
| 범주 | 결과 | 항목 | 소요 | 근거 |
```
결과 값은 PASS/FAIL 둘이 아니라 셋이다. 실행할 수 없는 체크(도구·권한·환경 제약으로 확인 못 한 것)는 FAIL로 적지 않고 **미검증(Not verified)**으로 따로 적는다. 미검증은 finding 개수에 들어가지 않고, 통과로 추정하지도 않는다. 통과·실패·미검증 세 줄을 분리해 나열한다 [SKILL-BETTER-INTERFACE].
개별 발견 사항의 심각도·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)은 커밋 대상이다.