- 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
24 KiB
24 KiB
audit-gate-rules — 게이트 규칙과 사고 이력
audit-gate.md의 부속 문서다. 매 감사에서 통째로 읽지 않는다. 아래 경우에만 연다.
이 문서를 읽는 법
| 이 문서를 여는 때 | 읽을 곳 |
|---|---|
| 프로젝트에 새 검사·E2E·하니스를 쓰거나 기존 게이트를 확장한다 | §2 게이트 규칙 전부 |
| 사용자가 결함을 보고했다 — 원인을 수치로 해부하고 재발 방지 검사를 더한다 | §2의 G15~G17, 그다음 §1에서 같은 유형의 사고를 찾는다 |
| 어떤 검사가 왜 있는지, 빼도 되는지 판단한다 | §1 사고 목록 |
| 대비 측정 스크립트를 직접 쓴다 | §3 |
매 감사에서 지키는 규칙의 요약은 audit-gate.md "매 감사에서 지키는 규칙"에 있다. 이 문서의 G 번호는 예전 문서가 "하니스 규칙 N"이라고 부르던 번호와 같다(하니스 규칙 15 = G15).
1. 사고 목록 — 각 검사가 생긴 이유
이 게이트의 각 검사는 사용자 보고로야 발견됐던 결함에서 왔다. 어느 검사가 어느 사고를 잡는지가 설계 근거다. 검사를 지우거나 완화하려면 해당 행의 사고가 다른 검사로 잡히는지 먼저 확인한다.
| 사고 (실제) | 검사 |
|---|---|
| CSS 닫는 중괄호 누락 — 이후 규칙 절반이 죽은 채 렌더 | stylelint(구문). 게이트에서는 checks.static 을 켜고 프로젝트에 stylelint 가 있을 때만 돈다. 없으면 미검증이다 |
| 브랜드가 모바일에서 1글자 폭으로 수축, 세로로 쌓임 | 수축 탐지(짧은 라벨 n줄 이상) + 브랜드 줄바꿈(텍스트 블록 각각 1줄) |
| 기기 글꼴 확대(안드로이드 큰 글꼴 1.3×)에서 제목 부서짐 | 스케일×폭 h1 행렬 |
| 본문 보조색이 배경 대비 4.4:1 (AA 미달) | 전 요소 대비 스캔(블렌드 계산) |
CSS color-mix()의 computed color(srgb …)를 0~255 rgb로 오독해 정상 대비를 1:1로 거짓 실패 |
색 정규화를 브라우저에 맡긴다(1×1 캔버스에 칠하고 픽셀을 읽는다) + 알파 합성 RED fixture |
대비 스캐너가 rgb()만 읽어 oklch()·color-mix() 글자색을 조용히 빼고 통과시킴. html 배경·그라디언트·조상 opacity도 무시 |
같은 정규화 + documentElement 배경 + 조상 opacity 합성. 못 잰 요소는 "대비 미검증 n요소"(G29) |
못 돌린 검사가 PASS … SKIP으로 찍히고 리포트가 "전 항목 통과" |
결과 상태 넷(PASS·FAIL·미검증·OFF). 미검증이 있으면 "검증 미완료" |
| 설정의 뷰 이름이 틀려도 같은 화면을 다시 재고 PASS | 뷰 전환 단언 — 버튼을 못 찾으면 실패(G8) |
<a href> 전부를 인라인으로 봐서 붙은 16px 아이콘 링크가 WCAG 2.5.8 층에서 빠짐 |
인라인 예외를 문장 속 display: inline으로 좁힘. 시각적으로 숨긴 요소는 제외 |
url(x.woff2)가 죽은 선택자로, #fade{}가 색 리터럴로 잡힘 |
주석·문자열·url()을 지운 뒤 선택자와 선언값을 나눠 읽는다 |
.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) | 단위 테스트(브라우저 밖 경계값) |
| 같은 데이터 속성이 여러 뷰에 존재해 선택자 오작동 | G1: 선택자는 컨테이너 스코프 |
| 혼합 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점" 입력이 조용히 통과(값을 덧붙인 타이핑) | G8(검사는 단언)·G9(number 입력 조작) |
| 모바일 라운드 — 중앙 모달이 썸존 밖, 백 키가 앱을 통째로 종료, safe area 무시, 입력 확대 | L6 시나리오(백·시트·터치타깃 스윕) + 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를 따른다 |
| 2560px 분할 히어로에서 사진이 1400px 이상으로 팽창하고 카피 열은 200px 안팎으로 붕괴 | hero·copy·media·후속 정보 레일의 rect 계약. stage/media/copy/workbench 토큰을 분리하고 media 상한·copy 최소폭·정렬을 hard fail로 측정 |
| 탭 전환 스크롤 잔류 — 짧은 뷰에서 클램프되어 헤더 반쯤 잘림, 뷰마다 크롬 위치 제각각 | 뷰 전환 뒤 scrollTo(0,0) 단언 — 게이트 미구현, 프로젝트 E2E에서 단언한다 + mobile-app-ux.md |
| 크럼을 헤더로 내보내 빈약한 헤더 — 계측 전부 통과했지만 사용자에게 '깨져 보인다' | 헤더 sticky·브랜드+액션 구조 단언 + 시각 무게 평가(눈) — mobile-app-ux.md |
| 왼쪽 인셋 리듬 불일치(제목 16 vs 앱바·카드 24) — 오버플로 검사는 오른쪽만 보므로 통과 | 왼쪽 인셋 검사 — 게이트 leftInset은 390 폭에서 텍스트 시작점 ≥12px만 잰다. 320 폭과 같은 축 ±10px 판정은 preflight.md의 M-E 행에서 직접 잰다 |
모바일에서 절대 배치 지도 자식의 부모를 position: static으로 바꿈 — offsetParent가 BODY가 되어 격자·핀이 히어로 전체를 덮음. 기하 게이트 8/8 통과 |
상태별 실제 캡처를 열어 보는 시각 E2E + absolute/fixed 자식의 containing block 단언(G19) |
새 관리자·가맹 서브페이지가 생겼지만 public/work/*/index.html만 검사 — 새 HTML 오류 8건이 검증 밖 |
정적 입력을 public/work/**/*.html·**/*.css로 재귀화 + 검사 대상 파일 수 단언(G20) |
| 마케팅 h1의 의도적 2줄 때문에 범용 줄 수 규칙을 강제할 위험 | 페이지별 읽기 폭·문구·글꼴 확대의 계약을 기록하고 375·390 × 글꼴 확대에서 실제 줄 수와 가독성을 판정. 줄 수를 맞추려고 글자 크기를 축소하지 않음 |
| 표제 마지막 줄에 한 어절만 남는 고아 줄("만든", "집부터.")이 1440·390에서 게이트를 통과해 배포됨 | 게이트 미구현 — 프로젝트 E2E나 수동 판정으로 h1·h2 마지막 줄의 글자 수를 잰다. 줄 수 계약(h1MaxLines)과는 별개다. 조판 기준은 typography.md |
이탤릭 면이 없는 한글 서체에 font-style: italic을 걸어 합성 사체가 렌더됨 |
게이트 미구현 — 강조 요소의 computed font-style과 실제 로드된 face를 대조하고, 렌더를 열어 본다. font-synthesis 값 확인 |
| 히어로 문단이 옆 패널과 34px 겹침 — 오버플로 검사는 통과 | 게이트 미구현 — 히어로 형제 요소의 rect 교차를 잰다. 의도한 겹침이면 design.md에 이유를 적는다 |
| h1 top이 헤더 bottom과 같아 여백 0 | 게이트 미구현 — h1.top − header.bottom이 간격 토큰 이상인지 잰다 |
이 표의 마지막 네 행은 2026-10-01 쇼케이스 21종 감사에서 나왔다. 검사를 게이트에 넣으면 "게이트 미구현"을 지우고 검사 이름으로 바꾼다.
2. 게이트 규칙 (테스트를 만드는 테스트)
- G1. 선택자는 스코프필수 — 같은 data-* 가 다른 뷰에 있으면 숨은 요소를 잡는다(
#as-table [data-assign]처럼). - G2. page.evaluate 클로저 금지 — Node 스코프 함수를 페이지 안에서 부르지 않는다. 측정 코드는 문자열로 주입.
- G3. 줄 수 측정의 오탐 두 종류 — 폰트 메트릭 top 차이(±5px 허용오차), 인라인 아이콘과 텍스트의 top 차이(텍스트 노드만 분리 측정). line box와 실제 글리프 잘림의 구분은 렌더 측정 계약을 따른다.
- G4. CSS 수정 직후 검증은 캐시 차단(
setCacheEnabled(false)) + 실제 로드된?v=확인. - G5. 비전(모델) 검수는 '여부'가 아니라 '지점 평가'에만 — 여부는 계측으로.
- G6. 모달 닫힘 직후 포커스 강탈 — 뷰 전환+타깃 포커스는 한 evaluate 로, settle 후 유실 재포커스.
- G7. 게이트를 통과해도 리포트에 수치를 남긴다 — "ALL PASS"가 아니라 "무엇을 몇으로 확인했나".
- G8. 검사는 단언한다 —
pass("차터", st)처럼 상태를 상세로만 찍는 건 검사가 아니다. 실측 사고: 점수 입력에 "88"을type()하면 기존 값에 덧붙어 "9288"이 되었고, 차터는 상태 텍스트만 인쇄해 통과했다. 기대값과 비교해 pass/fail 을 내라. - G9. number 입력은 선택-덮어쓰기가 안 된다 — 트리플 클릭 선택이 무시된다. 값을 비우고 타이핑하거나
el.value = x; dispatchEvent(new Event("input"))로 설정한다. - G10. 등장 모션과 캡처 상태를 맞춘다 — 모션 지속시간은 사용자 경험의 목적·입력 반응·감소 모션 계약으로 정한다. 시각 회귀는 그 값을 줄이라고 요구하지 않고, 실제 완료를 관측한 뒤 캡처한다. 임의 sleep으로 진행 프레임을 통과시키지 않으며 실제 모션 검수는 렌더 측정 계약을 따른다.
- G11. 같은 diff 수치가 반복되면 패치가 적용되지 않은 것 — 수정하고도 시각 diff %가 소수점까지 동일하면 수정이 렌더에 닿지 않았다(캐시·캐스케이드 순서·잘못된 파일). 고치기를 반복하지 말고 적용 자체를 의심해라.
- G12. 페이지→노드 직렬화는 NaN 방어 —
getComputedStyle(el).fontSize등을 문자열 보간하면 "16px"이 NaN이 되고, 하니스는 NaN을null로 받아"////px"같은 고장난 상세를 리포트한다. 수치는 항상parseFloat(x) || 0로 감싼다. - G13.
position: fixed요소의 보임 판정은 offsetParent 로 하지 마라 — fixed 는 offsetParent 가null이다. display 계산값·높이로 판정한다. - G14. 실기기(에뮬레이터) 검증은 텍스트 근거로 —
uiautomator dump의text="…"에서 화면 제목·활성 탭을 읽어 단언한다. 스크린샷+비전은 "백 직후 전환 중 프레임"을 잡는 타이밍 오탐이 있다(실측 사고). 물리 폰이 붙어 있으면adb -s <emulator>로 대상을 한정한다. - G15. 시각 기준 갱신은 의도 변경 리뷰를 동반한다 — "달라졌으니 갱신"으로 결함이 기준에 박힌다(실측: 왼쪽 인셋 결함 화면이 기준 갱신으로 '정상'이 되어 재발을 숨겼다). 갱신 전에 무엇이 의도적으로 바뀌었는지 한 줄씩 나열하고, 사용자 보고가 있던 화면은 수리 후 재촬영한다. 증거 보존과 실패 분류는 렌더 측정 계약을 따른다.
- G16. 사용자 보고 스크린샷은 비전 요약이 아니라 픽셀 판정으로 해부한다 — 비전 모델의 픽셀 추정("~0px", "~16px")은 부정확하다. pngjs 등으로 화면을 가로 밴드로 나눠 각 밴드의 텍스트 시작 x를 직접 재면(보임 행만, 왼쪽 테두리 아티팩트 제외) "무엇이 어디서 어긋났는지"가 수치로 나온다. 실측: '제목이 가장자리에 붙는다'는 보고가 앱바 24px·제목 16px의 리듬 불일치로 확정됐다.
- G17. 새 검사는 수정 전 코드로 RED 재현 후 GREEN — 검사 자체의 측정 하자(오탐)도 이렇게 잡는다. 실측: 왼쪽 인셋 검사 초안이 ①
display:none요소의 rect=0, ②전폭 컨테이너의 보더박스(텍스트가 아님)로 38개 오탐 — RED 단계에서 발견해 교정했다. - G18. 시각 E2E는 기본 화면만 찍지 않는다 — URL별 390·1440 기본 화면에 더해 결과가 바뀐 입력, 오류/검토, 모달/차단 상태를 찍는다. 하니스는 파일 개수·가로세로·0바이트·해시 중복을 단언하고, 에이전트는 파일을 실제로 열어 시장 적합성·위계·크롭·반복을 판정한다. 스크린샷을 저장했다는 사실만으로 PASS하지 않는다.
- G19. absolute/fixed 자식은 containing block을 잰다 — 모바일 미디어쿼리에서 부모의
position을 바꿨다면 각 절대 배치 자식의offsetParent또는 기준 rect가 의도한 컨테이너인지 단언한다.overflow-x:hidden으로 누수를 숨기지 않는다. - G20. 정적 검사는 재귀 입력과 대상 수를 잠근다 — 서브페이지가 생기는 저장소에서 한 단계 glob은 금지한다. 실행 전에 발견한 HTML/CSS/JS 파일 수를 출력하고 0개 또는 이전 계약보다 적으면 실패시킨다.
- G21. 시나리오·단언·캡처 수를 hard-lock한다 — 대규모 E2E는
EXPECTED_SCENARIOS,EXPECTED_ASSERTIONS,EXPECTED_CAPTURES를 둔다. 리팩터링으로 검사가 삭제돼도 전체가 조용히 GREEN이 되지 않게 한다. 수를 채우기 위한 중복 단언은 금지하고 공통·정상·오류·부작용·접근성·시각 상태로 분류한다. - G22. 사용자 승인과 검증 결과는 다른 상태다 — 사용자가 방향 게이트를 자율 승인했어도 기계·기능·시각 게이트는 면제되지 않는다. 승인됨/검증됨을 보고서에서 별도 표기한다.
- G23. 혼합 액션 행은 직접 자식 hitbox 중심을 비교한다 — 같은 액션 묶음·같은 행의 직접 자식인 채운 버튼과 텍스트 링크가 둘 다 프로젝트의 터치 타깃 기준(audit-gate.md "터치 타깃 — 두 층 판정": WCAG 2.5.8 24px와 간격 예외가 하한, 44px는
tapTargetPolicy가 계약으로 정했을 때)을 충족하는지,rect.top + rect.height / 2차이가 브라우저 CSS 픽셀 2px 이하인지 단언한다. 중첩 아이콘·배지나 모바일 세로 스택은 비교 대상이 아니다.align-items:center선언 존재는 검사가 아니다. - G24. 연속 섹션 토폴로지를 목록화한다 — 섹션별
역할 / DOM 골격 / 회색조 실루엣을 적는다. 역할이 다른 연속 3개가 같은제목+목록골격이면 시각 RED다. 반복 데이터처럼 같은 골격이 맞는 경우는 이유를 기록한다. - G25. 조건부 패널의 닫힘은 트랙 회수까지 단언한다 — 데스크톱 분할 작업대는
display:none만 검사하지 않는다. 인스펙터가 닫히면 주 표면의 오른쪽 끝과 작업대 끝 차이가 CSS 픽셀 2px 이하인지, 다시 선택하면 분할 트랙이 복원되는지 잰다. 모바일 시트·전체 화면 패널은 트랙 대신 닫힘·배경 복귀·포커스 복원을 검증한다. - G26. 진실 라벨은 개수가 아니라 의미 범위를 단언한다 — 같은 상태·출처·행동 가능성을 공유하는 자식은 제목과 경계가 분명한 가장 가까운 공통 부모가 한 번 소유하고 프로그램적으로 연결한다. 상태가 다른 자식만 예외 라벨을 둔다. 같은 공개 문구가 한 그룹의 모든 자식에 복제되면 범위 설계를 다시 본다.
- G27. 보이는 입력과 판정 의존성을 같은 SSOT로 잠근다 — 폼의 사용자 조건 이름 집합과 순수 판정 함수가 참조하는 사용자 조건 집합을 비교한다. 숨은 기본값을 사용자 선호로 세지 않고, 보이는 입력을 무시하지 않으며, 분석 전 완료 결과가 없고 복합 비용 일부로 적합을 확정하지 않는지 단언한다.
- G28. 공개와 관리 콘텐츠 소유권을 테스트한다 — 공개 DOM에는 내부 ID·원문 출처 원장·규제 채널 상수·감사 로그를 복제하지 않는다. 대신 사용자 행동을 막거나 바꾸는 제한과 해석 가능한 이유는 남긴다. 관리자는 그 요약을 재현하는 출처·검증 이벤트·판정 기록을 가진다.
- G29. 색·가시성 측정기는 새 CSS 문법과 보이는 텍스트를 구분한다 — computed color는
oklch()·lab()·color(srgb r g b / a)로도 온다. 문법을 하나씩 파싱하지 말고 브라우저에 정규화를 맡기고(1×1 캔버스에 칠한 뒤 픽셀을 읽는다), 못 읽은 요소는 건너뛰지 않고 미검증으로 센다. 반투명 배경은 자식에서 부모 방향으로documentElement까지 합성하고, 조상 opacity는 그 조상의 배경과 글자를 한 묶음으로 계산한다. 뒤에 이미지·그라디언트가 있으면 계산값으로 단정하지 않는다. 비활성 컨트롤의 글자는 WCAG 1.4.3 예외라 빼고 수를 남긴다.Range.selectNodeContents()로 sr-only·aria-hidden장식·빈 overlay까지 합산하지 말고 실제 보이는 text node의 rect만 잰다. 두 경우 모두 UI를 고치기 전에 RED fixture → 파서 수정 → 같은 fixture GREEN 순서로 검사기 자체를 검증한다. - G30. native dialog의 위치를 UA 스타일에 맡기지 마라 — 전역 reset은 dialog의 기본 margin을 지울 수 있다. open 상태에서
(left + width/2, top + height/2)와 viewport 중심의 오차, 화면 안 rect,htmloverflow, Escape 뒤 복귀를 390·1440·2560에서 측정한다. 내용이 길면 dialog 또는 내부 본문 하나만 scroll owner여야 한다. - G31. 초광폭 히어로는 비율 계약을 잰다 — 2560 같은 실제 wide viewport에서 hero 폭/정렬, copy 최소 폭, media 최대 폭, 다음 정보 레일 폭을 함께 단언한다. media만 넓고 카피가 가늘어지는 상태를
overflow=0으로 통과시키지 않는다. 절대 수치는 페이지별 토큰이 소유하며, 풀블리드 편집 사진 예외는 이유를 기록한다. - G32. 캐시 무효화도 사용자 화면에서 단언한다 — static CSS/JS를 덮어쓴 뒤 HTML만 새 URL이면 브라우저는 이전 stylesheet를 계속 쓴다. stylesheet와
@import의 버전을 같이 바꾸고, 공개 URL에서 실제 href·새 토큰/핵심 computed value를 검사한다. HTTP 200과 새 HTML만으로 배포 성공을 선언하지 않는다. - G33. 썸네일은 안정한 프레임만 저장한다 — 캡처 전 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바이트·시각 검수까지 단언한다. - G34. 초광폭 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가 한 글자 열로 수축하면 실패다. - G35. 커스텀 제스처 컨트롤은 완주까지 시험한다 — 언제: 슬라이더·드래그 표면·스크롤형 컨트롤 스트립이 있을 때. 레이아웃 검증과 별개로 (a) 탭 응답과 대상 입력방식으로 드래그를 끝까지(시작만이 아니라) 확인한다 (b) 컨트롤을 가로지르는 스와이프와 컨트롤 축을 따르는 드래그를 구분해 둘 다 시험한다 — 레이아웃 통과가 제스처 통과를 보장하지 않고, 둘 다 조용히 실패할 수 있다 (c) 증거 출처(에뮬레이션·합성 터치·실기기, 엔진명 Chromium≠Safari)를 명시한다. 접근 불가한 하드웨어는 보고된 갭으로 정직하게 남긴다 — 차단 사유가 아니다 [SKILL-IMPECCABLE].
- G36. 증거 방향성을 지킨다 — 런타임 동작이 결과를 좌우하는 항목은 시각 외관만 보고 코드-레벨 finding을 내지 않고, 소스코드만 보고 시각 finding을 내지 않는다. 각 finding은
path/to/file:line과 그 항목이 실제로 필요로 하는 증거(렌더 확인 또는 소스 대조)를 함께 남긴다 [SKILL-BETTER-INTERFACE].
3. 대비를 스크립트로 잴 때 — 반투명 배경을 불투명으로 계산하지 마라
getComputedStyle(el).backgroundColor로 조상을 거슬러 올라가며 "투명이 아닌 첫 값"을 배경으로 쓰면, rgb(255 255 255 / 0.085) 같은 유리 틴트를 순백으로 취급한다. 흰 글자가 1.15:1로 나온다. 알파가 1 미만인 층은 계속 쌓고, 아래에서 위로 합성한 뒤에 잰다. CSS Color 4 color(srgb …) 파싱은 G29를 따른다.
// 알파 < 1 이면 멈추지 말고 계속 쌓는다
const stack = []; let e = el;
while (e && e !== document.documentElement) {
const p = parse(getComputedStyle(e).backgroundColor);
if (p && p[3] > 0) { stack.push(p); if (p[3] >= 1) break; }
e = e.parentElement;
}
let [r, g, b] = rootBg;
for (let i = stack.length - 1; i >= 0; i--) {
const [sr, sg, sb, sa] = stack[i];
r = sr*sa + r*(1-sa); g = sg*sa + g*(1-sa); b = sb*sa + b*(1-sa);
}
이 조각은 원리만 보인다. rootBg는 documentElement의 배경이고(없으면 흰색), 나머지는 G29를 따른다.
유리·오버레이·틴트를 쓰는 페이지에서는 이 차이가 통과와 실패를 뒤집는다. 검사 도구가 틀리면 통과도 실패도 의미가 없다. 실패가 나오면 값을 고치기 전에 측정을 먼저 의심한다.