designpaca/packages/skill/references/audit-gate.md
Yun Chan 6930906583
All checks were successful
ci / build (push) Successful in 42s
release / release (push) Successful in 1m5s
release: v0.10.0
2026-08-29 21:19:26 +09:00

17 KiB
Raw Blame History

audit-gate — 자율 검증 폐쇄 루프

5단계(감사)를 완료 선언의 게이트로 만드는 문서다. 핵심 원칙은 하나다:

완료의 정의는 게이트 통과다. 사용자를 QA 로 쓰지 않는다.

"고쳤다"는 말의 근거는 검증 리포트의 수치다. 게이트가 실패 항목을 내면 4단계로 돌아가 고치고 다시 전체를 돌린다 — 고친 것만 재검하지 않는다. 이 루프(구현 → 게이트 → 실패 → 수정 → 재게이트)가 designpaca의 기본 동작이며, 게이트 없이 6단계로 넘어가는 것은 경로 무관 금지다.

왜 필요한가 — 실제 사고 목록

이 게이트의 각 검사는 사용자 보고로야 발견됐던 결함에서 왔다. 어느 검사가 어느 사고를 잡는지가 설계 근거다.

사고 (실제) 검사
CSS 닫는 중괄호 누락 — 이후 규칙 절반이 죽은 채 렌더 stylelint(구문) + 중괄호 균형
브랜드가 모바일에서 1글자 폭으로 수축, 세로로 쌓임 수축 탐지(짧은 라벨 n줄 이상) + 브랜드 1줄
기기 글꼴 확대(안드로이드 큰 글꼴 1.3×)에서 제목 부서짐 스케일×폭 h1 행렬
본문 보조색이 배경 대비 4.4:1 (AA 미달) 전 요소 대비 스캔(블렌드 계산)
조사만 하고 안 고친 영역(관심 과목 등) 전 뷰 순회 — 검사는 '내가 고친 것'이 아니라 전체 표면
대시보드 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 관습은 조사 없이 바꾸지 않는다
탭 전환 스크롤 잔류 — 짧은 뷰에서 클램프되어 헤더 반쯤 잘림, 뷰마다 크롬 위치 제각각 뷰 전환 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줄 때문에 범용 1줄 규칙을 꺼 버릴 위험 기본 1줄 유지 + 페이지별 h1MaxLines 계약. 375·390 × 글꼴 확대 매트릭스도 같은 상한 적용

7계층

tools/design-gate.mjs 가 한 파일로 돌리는 것과 프로젝트가 보강하는 것이 있다.

계층 도구 비고
L0 정적 stylelint + html-validate 문법·구문 위반을 브라우저 켜기 전에
L1 단위 순수 계산 로직 추출(calc.js) + 경계값 테스트 프로젝트별 작성 — 0명/만석/초과/빈배열. 입력 검증·신청 가능 같은 '판정'(가능/불가+사유)도 여기 둔다 — UI 는 판정을 문장으로 번역만
L2 불변식 design-gate.mjs — 오버플로·h1·수축·대비·SEO/meta·죽은 선택자·토큰 위생·theme-color 정합 전 뷰 × 전 폭
L3 시각 회귀 기준 화면 pixelmatch diff (≥0.1% 실패) + 핵심 상태 캡처 갱신은 검증된 배포 후 --update-baseline. 캡처 존재와 사람의 시각 판정은 별개다
L4 WebKit Playwright webkit 사파리 엔진 렌더 차이
L5 탐색 키보드 완전 통과 + 사용자 여정 차터 프로젝트별 작성 — 마우스 금지
L6 시나리오 E2E 실사용자 여정 재현 — 페르소나의 하루·장기 과업 포함 프로젝트별 작성. 기대값 단언 필수(하니스 규칙 8), 백 키·시트·양방향 흐름은 mobile-app-ux.md 패턴 기준. 실측: RED 1,742 단언 중 318 실패가 신규 기능에 정확히 집중 — 시나리오를 먼저 쓰면 미구현이 수치로 드러난다

설치 (프로젝트에 게이트 심기)

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)면 배포 불가

L1·L5 는 프로젝트 안에 tools/unit/*.test.mjs, tools/exploratory.mjs 로 보강하고 verify 체인에 묶는다. 근거와 완성 예는 designpaca 저장소 apps/site/tools/ 가 정본 샘플이다.

SEO / meta 감사 (design-gate 내장)

조사 기준(2026): title 1065자·유일, description 60165자, viewport, html lang, charset, canonical(프로덕션), og:title/description/image/url, twitter:card, favicon, h1 유일, img alt 전부, JSON-LD 파싱. 데모·로컬 페이지는 canonical/robots 없음을 허용한다 — 검사는 맥락을 존중하되 나열 근거는 리포트에 남긴다.

OS · 브라우저 특성 — 검사에 반영할 것

  • Windows: 스크롤바가 오버레이가 아니다 — overflow-x:auto 짝에는 overflow-y:hidden + scrollbar-width 검토. 스크롤바 자체가 레이아웃을 민다.
  • iOS/Safari(WebKit): 100vh 주소창 문제(100dvh), 텍스트 인플레이션(-webkit-text-size-adjust:100%), WebKit 만의 flex/행높이 차이 → L4 로 재검.
  • 안드로이드: 시스템 글꼴 확대(1.15~1.3× 흔함) — rem 전면 확대로 제목 칸 수축 → 스케일×폭 행렬으로 재현·검증.
  • 폰트 스왑: font-display: swap 의 CLS — 폴백 메트릭 보정(typography.md §4) 없이 검사 통과를 선언하지 않는다.

하니스 규칙 (테스트를 만드는 테스트)

  1. 선택자는 스코프필수 — 같은 data-* 가 다른 뷰에 있으면 숨은 요소를 잡는다(#as-table [data-assign] 처럼).
  2. page.evaluate 클로저 금지 — Node 스코프 함수를 페이지 안에서 부르지 않는다. 측정 코드는 문자열로 주입.
  3. 줄 수 측정의 오탐 두 종류 — 폰트 메트릭 top 차이(±5px 허용오차), 인라인 아이콘과 텍스트의 top 차이(텍스트 노드만 분리 측정).
  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. 등장 모션 지속시간 ≤ 감사 도구 대기시간 — 시각 회귀가 뷰 전환 100ms 뒤에 스크린샷을 찍는다면 등장 애니메이션은 그보다 짧게(≤70ms). 아니면 매번 다른 프레임이 찍혀 회귀가 흔들린다.
  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·원문 출처 원장·규제 채널 상수·감사 로그를 복제하지 않는다. 대신 사용자 행동을 막거나 바꾸는 제한과 해석 가능한 이유는 남긴다. 관리자는 그 요약을 재현하는 출처·검증 이벤트·판정 기록을 가진다.

리포트 양식

게이트: 전 계층 통과 — 배포 가능 · 총 Ns
| 계층 | 결과 | 항목 | 소요 |

실패 상세는 원문 테일 포함. 리포트 파일(gate-report.md / verify-report.md)은 커밋 대상이다.