문서 목차
이 페이지에서목차

visual-verify

구현한 Flutter·Jaspr 화면이 Figma 시안과 일치하는지 판정할 때 사용합니다. 상태·크기·테마·글꼴을 맞춰 픽셀을 비교하고 통과·실패·판정 불가와 차이 원인을 보고합니다. 수정 반복은 pixel-loop-guide에서 다룹니다.

전 화면 개선, 공유 co_faker 데이터, 실제 앱 실행, CoUI 확장을 함께 수행할 때는 전 화면 일치 작업의 상태 목록·원본 보존·중간 커밋 절차도 적용한다.

한마디로#

상태·크기·테마·글꼴을 맞춰 픽셀을 비교하고 통과·실패·판정 불가와 차이 원인을 보고합니다. 수정 반복은 pixel-loop-guide에서 다룹니다.

무엇을·언제#

  • 무엇을 해 주나요
    • 시안의 상태별 화면(기본 · 로딩 · 빈 화면 · 오류)을 밝은 테마와 어두운 테마로 각각 찍습니다.
    • 같은 조건으로 앱 화면을 그려 두 사진을 점 단위로 비교하고, 다른 곳을 칠한 비교 이미지와 보고서를 남깁니다.
    • "비교할 수 없었다"는 결과를 통과로 바꾸지 않습니다 — 왜 비교하지 못했는지가 보고서에 남습니다.
  • 언제 쓰면 좋나요
    • "이 화면 구현이 끝났다"고 말하기 전 — 화면 개발의 완료 기준입니다(/cc-blueprint:build, /cc-dev:run Step 7.3).
    • PR 을 올리기 전 디자인 확인, 여러 화면을 한꺼번에 점검할 때.
  • 언제 쓰면 안 되나요
    • 다른 곳을 직접 고쳐 주길 원할 때는 cc-pixel-loop:pixel-loop(이건 검사만 합니다).

핵심 용어#

용어쉬운 설명
상태 프레임 시안에서 한 화면의 상태별 그림. Mobile-Default · Mobile-Empty 처럼 「플랫폼-상태」 이름을 씁니다
렌더앱 코드가 실제로 그린 화면 사진
배율화면의 한 점을 실제 화소 몇 개로 그리는지. 2 면 가로세로 두 배로 촘촘합니다
가림비교에서 빼는 띠. 상태 바처럼 앱이 그리지 않는 곳만, 이유와 함께 뺍니다
영역서로 다른 점들이 모인 덩어리. 보고서의 한 줄입니다
판정 불가비교하지 못했다는 결과. 통과가 아닙니다
알려진 차이CoUI 가 이미 추적하는, 시안과 앱이 다르게 그리는 자리. 앱을 고치지 않습니다
차이 항목co_golden 디자인 분석이 남기는 한 줄. 어느 글자 · 컴포넌트가 어떤 값으로 다른지와 고칠 소스 줄이 들어 있습니다

판정은 스크립트가, 설명은 모델이#

비교의 통과 · 실패는 scripts/figma_compare.py(표준 라이브러리만 쓴다)가 정한다. 모델이 두 이미지를 보고 "±1px 차이"를 주장하는 방식은 쓰지 않는다 — 같은 입력이면 같은 결과가 나와야 게이트다. 모델이 하는 일은 남은 영역을 설명하고, 고치기 전에 그 영역을 고칠 자리로 나누는 것(아래 5절)뿐이다.

FC="${CLAUDE_SKILL_DIR}/scripts/figma_compare.py"

순서#

1. 입력      compare-frames 레시피(cc-blueprint)로 섹션의 상태 프레임을 읽는다 → frames-<섹션>.json
             사양(선택) — design-spec 레시피로 비교할 프레임마다 → design/<label>.design.json (co_golden 구조 대조)
2. 계획      python3 "$FC" plan     → plan.json (프레임 × 밝기, 배율 · 가림 · Figma 원천)
3. 확인      python3 "$FC" fonts    → 시안의 서체가 앱 · 의존 패키지에 있고 테스트가 로드하는가
             python3 "$FC" coui     → 앱이 해석한 CoUI 릴리스 · 라이브러리와 달라진 컴포넌트
4. Figma 쪽  download_assets(png · defaultScale = 배율) → figma/<label>.png
             다른 밝기는 dark-frames 레시피의 복제본을 찍고 지운다
5. 렌더 쪽   use_case 상태를 plan 의 장치로 캡처(Flutter) · 브라우저 캡처(Jaspr) → render/<label>.png
             co_golden 캡처는 python3 "$FC" ingest 가 label 로 옮긴다
             디자인 참조(figma/<label>.png + 사양)를 붙이면 co_golden 이 analysis/ 보고서도 남긴다(3절)
6. 비교      python3 "$FC" diff --run <run> --label <label>   (비교마다)
7. 판정      남은 영역마다 고칠 자리를 judgement.json 에 — 고치기 전에(5절). co_golden 차이 항목이 있으면 그것이 근거다
8. 덧대기    python3 "$FC" literals → 변경분의 리터럴 덧대기
9. 합계      python3 "$FC" summary → summary.json · summary.md (PR 본문에 그대로)

종료 코드는 모든 명령이 같다: 0 통과 · 1 실패 · 2 사용 오류 · 3 판정 불가. 판정 불가는 통과가 아니다 — 부른 쪽은 그것을 실패와 따로, 정지(BLOCKED)나 사람이 확인한 스킵으로 다룬다(cc-dev:run Step 7.3 · /cc-blueprint:build Step 5). 실패는 고칠 곳이 있다는 뜻이고, 판정 불가는 대조할 수단이나 시안이 없다는 뜻이라 처방이 다르다.

1. 맞추는 것 — 정규화#

축Figma 쪽렌더 쪽어긋나면
크기 상태 프레임의 크기(논리 px) 장치의 논리 크기 = 프레임 크기 undetermined:size-mismatch
배율 download_assets 의 defaultScale devicePixelRatio = 같은 배율. 기본 Mobile 2 · Web 1 같다
상태 바 · 홈 인디케이터 그 노드의 높이(레시피 insets) safeArea 를 같은 값으로. 그 띠는 이유를 단 가림 —
글자 배율 — textScaler 1.0 (textScales: [1]) —
서체 프레임이 쓰는 패밀리(가림 띠 밖) 앱 · 의존 패키지 pubspec.yaml 에 선언되고, 테스트가 로드 fonts 실패 → 판정 불가
밝기 프레임에 고정한 모드. 다른 밝기는 복제본 같은 밝기의 앱 테마 리터럴 색 → figma-unthemable
상태 프레임 이름의 상태 use_case 의 같은 상태 시나리오(위젯북 Knob 과 같은 mock) 상태를 못 읽으면 비교하지 않고 경고
앱 인자 frame-modes 의 브랜드 · 서페이스 스타일 · 밀도 같은 CoUIApp 인자 /cc-blueprint:build Step 0
움직임 — MediaQuery(disableAnimations: true) — 스켈레톤 맥동이 멈춘 상태 —
  • Figma 이미지는 download_assets 로 받는다. get_screenshot 의 maxDimension 은 크기를 줄이기만 하고 키우지 않아 늘 1배 이하다(실측) — 배율을 맞출 수 없다. download_assets(fileKey, nodeId, defaultFormat: "png", defaultScale: <배율>) 의 export URL 을 curl 로 figma/<label>.png 에 받는다.
  • 프레임에는 불투명한 채움이 있어야 한다. 내보내기는 투명한 곳을 캔버스 색으로 칠한다(실측) — 레시피가 경고한다.
  • 배율은 정수다. 렌더가 Figma 의 정수배면(기기 3배 ↔ Figma 1배) 상자 평균으로 줄인다. 그 밖의 비율은 보간하지 않고 판정 불가다.

2. Figma 쪽#

어느 섹션을 대조하나 — 화면의 디자인 소스는 아래 셋 중 하나라도 있으면 있다. 셋 다 같은 섹션을 가리키면 한 번만 대조한다.

출처읽는 곳대조 단위
화면 레지스트리 저장소의 화면 레지스트리( docs/figma-screen-registry.yaml , 없으면 이름이 figma*screen*registry* 인 파일) 중 그 화면( dart_class )의 행 figma_node 섹션
designLink 그 화면 use_case 의 @UseCase(designLink:) 그 프레임이 든 섹션(compare-frames 가 넓힌다)
figma_analysis .claude/docs/<scope>/figma_analysis.md 의 Figma URL 그 노드 — 프레임이면 그 섹션

출처가 하나도 없으면 대조 대상이 없다(해당 없음). 출처를 읽지 못한 것(파일 오류 · 깨진 URL)은 해당 없음이 아니라 판정 불가다.

  1. cc-blueprint 의 references/recipes/compare-frames.figma.js 를 위 표가 고른 섹션마다 돌려(읽기 전용, figma:figma-use 먼저) 반환값을 그대로 frames-<섹션>.json 에 저장한다. 「플랫폼-상태」 이름이 아닌 옛 파일은 레시피의 NAMES 로 프레임마다 상태를 준다(보고에 남는다).
  2. python3 "$FC" plan --frames frames-*.json --run <run> — 비교마다 label(<프레임 id>__<밝기>) · 배율 · 가림 · Figma 원천(frame · clone:light · clone:dark · unthemable)과 캡처 장치(device) · 시나리오 이름(captureScenario)이 정해진다. 밝기는 늘 라이트 · 다크 둘이다 — 하나를 빼려면 --exclude-theme '<밝기>:<이유>'(6절).
    • 「플랫폼-상태」 이름이 아닌 프레임은 비교 목록에 들지 않고 unplanned 에 남아 요약을 판정 불가로 만든다(undetermined:unnamed-frame) — 등록된 상태가 조용히 빠지지 않게. 섹션 안의 화면이 아닌 프레임(메모 류)은 --ignore-frame '<프레임 id>=<이유>' 로 뺀다. 그 이유는 summary.md 에 남는다.
    • 계속 바뀌는 노드(스피너 · 시계 류)는 --mask-node '<노드 id>=<이유>' 로 그 범위를 가린다 — 노드 행(레시피)에 있는 노드만, 이유와 함께. devices 는 렌더 쪽 장치 목록이다 — 크기 · 배율 · 상태 바 · 홈 인디케이터가 같은 프레임은 장치 하나를 같이 쓴다.
  3. 원천이 frame 인 비교는 그 프레임을, clone:<밝기> 인 비교는 dark-frames.figma.js create(MODE = 그 비교의 밝기 — Dark 또는 Light)가 돌려준 복제본을 download_assets 로 받는다. 프레임이 Dark 로 고정돼 있으면 라이트가 복제본이다. 받은 뒤에는 실패해도 remove 를 돌린다 — 다음 create 도 남은 복제본을 먼저 지운다.
    • 이 레시피만 쓰기다. 원본은 건드리지 않고, 이름이 ⟂ compare 로 시작하는 잠근 복제본을 캔버스 오른쪽에 잠깐 둔다. 디자이너의 파일이므로 편집 권한이 없으면 다른 밝기는 판정 불가로 남는다.
  4. 원천이 unthemable 이면 받지 않는다 — 시안의 색이 변수에 묶이지 않아 Figma 가 그 밝기를 그릴 수 없다. 그것은 디자인 쪽 결함(cc-blueprint:design-self-check 3번 「토큰 단일 출처」 L0)이고, 그 비교는 판정 불가로 남아 디자이너에게 간다. 복제본을 만들지 못한 비교(권한 · 모드 없음)도 그 비교만 판정 불가다 — Figma 를 읽지 못한 것으로 세지 않는다.

3. 렌더 쪽#

Flutter — use_case 상태를 프레임 크기 장치로 (기본)#

기기도 실행 중인 앱도 필요 없다 — flutter test 가 그린다. 그래서 Flutter Web 앱도 같은 경로다. 렌더는 위젯북 use_case 의 상태 Knob 과 같은 시나리오 enum 의 같은 bloc() 을 쓴다(데이터는 build 의 mock/). enum 의 값마다 시안 프레임의 상태 이름이 들어 있어, 상태 프레임과 캡처가 그 이름으로 짝지어진다(cc-flutter:figma-widgetbook-alignment §2-1). 그래서 캡처 테스트는 그 enum 을 import 할 수 있는 패키지 — use_case 의 fixture 를 가진 패키지 — 의 test/ 에 둔다.

새 앱 표준(cc-flutter:golden-test-guide 의 co_golden)이면 capture 모드로 찍는다 — 기준 이미지가 아니라 대조용 사진이다. 장치는 plan.json 의 devices 를 그대로 옮기고, 시나리오 이름은 비교의 captureScenario(상태의 소문자 — co_golden 의 이름 규칙)다:

// test/src/golden/<feature>_figma_frames_test.dart — Figma 대조용 캡처. 기준 이미지를 만들지 않는다
final figmaFrames = GoldenMatrix<ThemeData>(
  suite: '<feature>_figma',
  coverage: GoldenCoverage(
    devices: const [
      // plan.json 의 devices 그대로 — 이름 · 논리 크기 · 배율 · 상태 바 / 홈 인디케이터
      GoldenDevice(
        name: 'mobile-402x874',
        logicalSize: Size(402, 874),
        devicePixelRatio: 2,
        platform: TargetPlatform.iOS,
        safeArea: EdgeInsets.only(top: 62, bottom: 34),
      ),
    ],
    themes: [
      GoldenTheme(name: 'light', data: AppTheme.light),
      GoldenTheme(name: 'dark', data: AppTheme.dark, brightness: Brightness.dark),
    ],
    locales: const [Locale('ko')],
    textScales: const [1],
    sampling: GoldenSampling.full,
  ),
  // 앱 루트와 같은 CoUIApp 인자(브랜드 · 서페이스 스타일 · 밀도)로 감싸고 동작 줄이기를 켠다.
  // co_golden 은 ticker 도 멈춘다(freezeAnimations 기본 true)
  app: (variant, child) => FigmaFrameHost(theme: variant.theme.data, child: child),
);

void main() {
  for (final s in XxxScenario.values) {
    // 시나리오 이름 = 비교의 captureScenario(default · loading · empty · error) — 위젯북 Knob 과 같은 enum · 같은 bloc()
    figmaFrames.scenario(s.captureScenario, build: (_) => XxxPage(bloc: s.bloc()));
  }
}
CO_GOLDEN_MODE=capture flutter test --tags co_golden test/src/golden/<feature>_figma_frames_test.dart
python3 "$FC" ingest --run "$RUN" --captures build/co_golden/images/<feature>_figma
# <captureScenario>/<device>__<theme>__<locale>.png → <run>/render/<label>.png — 없는 캡처는 판정 불가로 남는다
  • 서체는 flutter_test_config.dart 의 loadGoldenFonts() 가 로드한다. 앱이 번들하지 않은 서체는 상자로 그려진다 — 그래서 fonts 가 먼저다.
  • co_golden 이 없는 저장소는 같은 일을 testWidgets 로 한다 — tester.view.physicalSize = 프레임 크기 × 배율 · devicePixelRatio = 배율, 프레임 크기의 SizedBox 를 감싼 RepaintBoundary 를 toImage(pixelRatio: 배율) 로 PNG 에 쓴다. Alchemist 의 CI 골든은 글자를 상자로 가리므로 대조에 쓰지 않는다.
  • 맥동 · 스피너가 있는 상태(Loading)는 disableAnimations: true 로 멈춘 프레임을 찍는다 — CoUI Skeleton 은 동작 줄이기에서 맥동을 멈추고 쉼 끝의 면으로 그린다. 스피너처럼 계속 도는 표시는 plan --mask-node '<노드 id>=<이유>' 로 가린다(가림 상한 15%).

co_golden 디자인 분석 — 판정의 근거

같은 캡처 테스트에 디자인 참조를 붙이면 co_golden 이 캡처마다 analysis/<suite>/<scenario>/<variant>.json(co_golden.design_report v1)을 남긴다 — 어느 글자의 서체 · 굵기 · 크기 · 행간이, 어느 컴포넌트의 변형 속성이, 어느 요소의 위치 · 크기 · 토큰이 다른지를 Figma 노드(레이어 경로 · 텍스트 스타일)와 위젯의 소스 줄(file:line:column)까지 붙여서. 이 스킬이 받은 PNG(figma/<label>.png)와, 비교마다 design-spec.figma.js 로 읽은 사양(design/<label>.design.json)을 그대로 넘긴다:

figmaFrames.scenario(
  s.captureScenario,
  build: (_) => XxxPage(bloc: s.bloc()),
  designReference: (variant) {
    // plan.json 의 비교(captureScenario · theme)로 label 을 찾는다 — 없으면 대조하지 않는다
    final label = labelFor(s.captureScenario, variant.theme.name);
    return label == null
        ? null
        : GoldenDesignReference(
            imagePath: '<run>/figma/$label.png',
            specPath: '<run>/design/$label.design.json',
          );
  },
);
  • 판정은 바뀌지 않는다. 통과 · 실패 · 판정 불가는 여전히 diff · summary 가 정한다. co_golden 의 픽셀 규칙 · 기본값은 이 스크립트를 옮긴 것이라(CIEDE2000 · 안티앨리어싱 · 위치 반경 · 글자 상자 완화 · 4 논리 px 칸 · 예산 0.2% / 32 px²) 같은 기준으로 센다. co_golden 의 판정과 차이 항목은 5절에서 영역을 판정할 때의 근거다.
  • 상태 바 · 홈 인디케이터 띠는 co_golden 도 기본으로 가린다(maskSafeArea). plan --mask-node 로 가린 노드는 같은 이유로 GoldenDesignMask.figmaNode('<노드 id>', reason: …) 를 준다 — 두 도구의 가림이 같아야 영역이 맞는다.
  • 사용법 정본은 cc-flutter:golden-test-guide 6절과 co_golden README 「Design analysis」다.

Jaspr — 브라우저 캡처#

cc-jaspr-web 의 Playwright 로 페이지를 띄워 browser_resize(프레임 너비, 프레임 높이) 뒤 browser_take_screenshot 을 render/<label>.png 로 저장한다. 다크는 browser_emulate_media(colorScheme: dark) 로 바꾼다. window.devicePixelRatio 를 읽어 plan 의 배율과 다르면 --scale 로 plan 을 다시 만든다.

서체는 pubspec 이 아니라 CSS 로 오므로, 페이지가 실제로 불러온 서체로 판정한다 — browser_evaluate 로 [...document.fonts].filter(f => f.status === "loaded").map(f => f.family) 를 읽어 fonts --browser-fonts <쉼표 목록> 에 넘긴다(--app 대신).

기기 — 실행 중인 앱 (게이트 밖)#

Marionette 스크린샷은 실제 기기의 동작까지 보는 데 쓴다(/cc-pixel-loop:loop --source=device). 배율은 기기의 것이고 상태 바는 가림으로 뺀다. 애니메이션 · 시계 · 네트워크가 사진을 바꾸므로 완료 판정(게이트)의 근거로는 쓰지 않는다 — 게이트는 위의 테스트 렌더다.

4. 비교 — 규칙과 기본값#

픽셀 p 는 다음이 모두 참일 때 차이다:

  1. 두 색의 CIEDE2000 이 color_delta_e 를 넘는다.
  2. 반경(pixel × 배율) 안 상대 이미지 어디에도 그 색이 없다 — 양쪽으로 본다(시안에만 있는 것 · 렌더에만 있는 것).
  3. 안티앨리어싱 가장자리가 아니다(두 색 사이에 낀 화소 — pixelmatch 와 같은 판정).

글자 상자(레시피가 준 TEXT 노드의 글리프 범위) 안에서는 가로 반경만 text_pixel 로, 색 허용치를 text_delta_e 로 넓힌다 — Figma 와 Flutter 의 글자 엔진은 같은 줄에서 글자 폭이 조금씩 쌓여 어긋나고 획의 진하기가 다르다. 세로 반경은 그대로라 줄 간격 · 여백이 어긋난 글자는 센다. 프레임 가장자리의 허용 반경 띠는 밖을 볼 수 없어 세지 않는다.

차이 픽셀은 4 논리 px 칸으로 묶여 영역이 되고, 영역마다 범위 · 가장 깊은 노드(레시피의 노드 행) · Figma 색과 렌더 색 · 평균 ΔE 가 보고된다. 영역 id 는 node:<노드 id> 라서 반복해도 같은 이름이다.

기본값값뜻
pixel1위치 허용(논리 px)
color_delta_e3색 허용(CIEDE2000)
text_pixel2글자 상자 안 가로 위치 허용(논리 px)
text_delta_e10글자 상자 안 색 허용
예산 ratio0.2%비교한 픽셀 중 차이 비율 상한
예산 region32가장 큰 영역의 차이 넓이 상한(논리 px²)
가림 상한15%넘으면 undetermined:over-masked — 가리지 말고 비교한다

통과는 ratio 와 region 을 둘 다 지킬 때다. 바꾸려면 --tolerance · --budget 로 주고, 바꾼 값은 보고서에 남는다.

기본값은 실측으로 정했다 — CoUI 라이브러리 멤버의 Figma 내보내기와, 같은 위젯을 Pretendard 로 배율 2 에서 그린 Flutter 렌더:

쌍결과차이 비율가장 큰 영역(논리 px²)
같은 시안 — Button primary · secondary md통과0.017%0.5
같은 시안 — Card outlined(머리 · 본문 · 푸터)통과0.032%4.25
라벨 2px 이동실패3.18%45.5
푸터 글자색 다름실패0.32%108.25
본문 2px 아래실패0.54%133.25
다른 변형(primary → secondary)실패86.5%2596
라벨 굵기 한 단계(600 → 500)통과 — 놓친다0.042%0.5

같은 시안의 잡음(≤ 4.25)과 가장 작은 결함(45.5) 사이에 region 32 가 있다. 같은 실측에서 Card 는 라이브러리가 본문 · 푸터 간격을 코드보다 16px 넓게 그려 실패했다 — 라이브러리 결함이라 CoUI 이슈 — Card 본문 · 푸터 간격으로 올렸고, 그 차이는 앱이 고치지 않는다(5절 known).

5. 판정 — 고치기 전에#

실패한 비교의 영역마다, 패치를 쓰기 전에 고칠 자리를 judgement.json 에 적는다. 질문은 이 순서다:

순서자리언제하는 일면제
1 known cc-flutter:figma-to-coui 의 「알려진 차이」(F7)에 해당하거나, 라이브러리 인스턴스가 코드와 다르게 그린 것 앱을 고치지 않는다. F7 행의 이슈, 없으면 CoUI 에 올린 이슈를 ref 에 이슈 URL 이 있고, --check-issues 면 그 이슈가 열려 있을 때
2 version 영역이 라이브러리 인스턴스 안이고, 그 컴포넌트가 앱의 CoUI 릴리스와 라이브러리 사이에서 달라졌다( coui.json 의 drift · unknownInstances ) 앱을 고치지 않는다 — 앱의 CoUI 를 올리거나( cc-coui-upgrade ) 라이브러리를 맞춘다( cc-blueprint:coui-library-update ). ref = coui:<컴포넌트> 그 컴포넌트가 coui.json 에 있고 영역의 노드가 그 인스턴스 안일 때
3 style-slot 라이브러리 인스턴스인데 캔버스의 덮어쓰기(색 · 패딩 · 반경 다시 묶기, 효과 스타일)가 코드에 없다 그 인스턴스의 withStyle(...) · 토큰 getter, 제품 전체 값이면 CoreComponentTheme(F3 · F6) 없음 — 고친다
4 app 제품이 그린 레이아웃 · 고른 컴포넌트 · 상태 대응 · 내용 앱 코드를 CoUI 부품 · 토큰으로 고친다 없음 — 고친다
{"schema": 1, "regions": [
  {"label": "12-345__light", "id": "node:12:359", "place": "version", "ref": "coui:card",
   "evidence": "Card 의 bodyFooterGapStyle 이 앱 릴리스와 라이브러리 ref 에서 다르다", "fix": null},
  {"label": "12-345__light", "id": "node:12:353", "place": "app", "ref": null,
   "evidence": "제목 위 여백 16 ↔ 렌더 24", "fix": "Gap 을 CoreSpace.space16 으로"}
]}
  • 리터럴 덧대기는 고침으로 세지 않는다. literals 가 변경분에서 색 · 여백 · 반경 · 글자 크기 · 손 장식(BoxDecoration 류) · 위치 밀기 리터럴을 찾으면 summary 가 실패한다(fail:literal-patch). 꼭 필요한 리터럴은 그 줄에 // literal-ok: <이유> 를 달고, 그 이유가 보고서에 남는다.
  • version · known 만 면제된다. 면제는 summary 가 근거를 다시 확인한 뒤 예산을 남은 영역으로 다시 잰다 — 근거가 맞지 않으면 그 영역은 남는다. 보고서 상한(50) 밖으로 잘린 영역은 면제할 수 없다.
  • 판정은 모델의 몫이지만, 면제의 근거는 기계가 확인한다. 면제된 영역은 전부 PR 본문(summary.md)에 이슈 · 컴포넌트와 함께 남아 리뷰어가 본다.

co_golden 차이 항목 — 영역의 근거#

캡처에 디자인 참조(사양 포함)를 붙였으면, 영역을 판정하기 전에 같은 자리의 차이 항목을 읽는다. 영역 id 의 노드(node:<노드 id>)와 차이 항목의 design.id 는 같은 Figma 노드다 — 두 레시피가 같은 파일의 노드 id 를 쓴다. 차이 항목은 무엇이(서체 · 굵기 · 크기 · 역할, 컴포넌트와 변형, 위치 · 간격, 토큰) 얼마나 다르고 어느 소스 줄에서 고치는지를 말한다 — 그 메시지와 소스 줄을 evidence 에, 힌트를 fix 에 옮긴다.

차이 항목판정 자리
known 이 붙은 항목(GoldenKnownDifference 의 issue) known — ref 는 그 이슈
owner: designSystem(CoUI 컴포넌트가 안에서 그린 값 — 앱은 그 컴포넌트를 만들기만 했다) 앱을 고치지 않는다 — 그 컴포넌트가 coui.json 의 drift 면 version , 추적 이슈가 있으면 known , 둘 다 아니면 CoUI 에 이슈를 올리고 known
component 변형 속성 차이(Variant secondary → primary) 그 컴포넌트가 coui.json 의 drift 면 version , 아니면 app (F3 의 생성자 인자)
component 손으로 만든 위젯(시안은 CoUI 컴포넌트)app — CoUI 부품으로 바꾼다
component catalog(앱의 CoUI 카탈로그에 없는 컴포넌트) cc-flutter:figma-to-coui F8 의 판정을 따른다 — 추적 이슈가 있으면 known , 없으면 app (CoUI 부품 조립 + 이슈 · 만료 조건)
typography 역할 · 굵기 · 크기 · 행간 · 자간 app — 텍스트 역할 체인(F1). 휴대폰 · 태블릿 제목 크기면 known(F7)
layout · shape · color 가 라이브러리 인스턴스 안 캔버스의 덮어쓰기면 style-slot, 아니면 F7 · version 부터 본다
layout · shape · color 가 제품이 그린 곳 app — 토큰 · Gap · 컴포넌트 기하로
font · frame판정 자리가 아니다 — 서체 로드 · 장치 크기를 먼저 맞추고 다시 찍는다
visual(차이 항목이 설명하지 못한 영역)지금처럼 영역을 보고 판정한다
  • 차이 항목은 근거이지 면제가 아니다 — 면제(known · version)의 근거는 지금처럼 summary 가 다시 확인한다.
  • error 차이 항목이 남아 있는데 이 스크립트가 pass 이면, 그 항목을 결과 보고에 함께 싣는다 — 굵기 한 단계처럼 픽셀이 놓치는 차이다(8절). 고치는 일은 /cc-pixel-loop:loop 가 한다.

6. 결과#

summary 의 상태뜻
pass계획한 비교가 전부 있고, 남은 영역이 예산 안이며(면제 포함), 서체 · 덧대기 검사도 통과
fail 예산을 넘는 영역이 남았거나(fail:diff), 리터럴 덧대기가 있다(fail:literal-patch)
undetermined비교하지 못한 것이 있다

summary.json 의 status 는 이 셋 중 하나이고, 이유는 reasons[] 에 따로 있다 — fail:diff · fail:literal-patch · undetermined:comparison(비교 하나 이상이 판정 불가) · undetermined:font-missing · undetermined:fonts-not-checked · undetermined:literals-not-checked · undetermined:unnamed-frame · undetermined:no-comparisons, 그리고 막지 않는 warn:defaults-changed · warn:judgement-for-unknown-region. 비교마다의 사유는 rows[].status 다 — undetermined:not-compared(보고서 없음) · missing-image · size-mismatch · over-masked · unexplained-mask · unreadable-png · figma-unthemable:<리터럴 수> · no-status.

  • 서체 · 덧대기 검사를 돌리지 않았으면 통과가 아니다(*-not-checked) — 검사하지 않은 것을 통과로 읽지 않는다.
  • 밝기 하나를 빼는 것은 이유와 함께만 된다 — plan --exclude-theme 'dark:<이유>'(예: 제품 결정으로 라이트만 내는 앱 — 그 결정 기록). 밝기를 목록으로 고르는 옵션은 없고, 제외는 summary.md 에 남는다.
  • 허용치 · 예산 · 가림 상한을 기본값과 다르게 판정한 비교는 summary.md 에 그 값이 남는다(warn:defaults-changed) — 리뷰어가 본다.

7. 산출물#

.claude/docs/visual-verify/<run_id>/
├── frames-<섹션>.json        # compare-frames 반환값 그대로
├── design/<label>.design.json   # design-spec 반환값 그대로(선택) — co_golden 디자인 참조의 사양
├── plan.json · nodes/        # 비교 목록 · 프레임별 노드 행
├── fonts.json · coui.json · literals.json · judgement.json
├── figma/<label>.png · render/<label>.png
├── reports/<label>.report.json · <label>.diff.png   # Figma | 렌더 | 차이(빨강 차이 · 노랑 안티앨리어싱 · 파랑 가림)
└── summary.json · summary.md                         # summary.md 는 PR 본문에 그대로
python3 "$FC" plan --frames frames-*.json --run "$RUN" [--exclude-theme 'dark:<이유>'] \
  [--ignore-frame '<프레임 id>=<이유>'] [--mask-node '<노드 id>=<이유>'] [--scale 2]
python3 "$FC" fonts --frames frames-*.json --app <앱 루트> --test-package <렌더 테스트 패키지> --run "$RUN"
#   Jaspr: python3 "$FC" fonts --frames frames-*.json --browser-fonts "<불러온 서체, 쉼표>" --run "$RUN"
gh api -H 'Accept: application/vnd.github.raw' \
  'repos/coco-de/coui/contents/tools/figma-token-sync/src/data/components.json?ref=main' > "$RUN/library-components.json"
python3 "$FC" coui --app <앱 루트> --library-components "$RUN/library-components.json" --instances frames-*.json --run "$RUN"
python3 "$FC" ingest --run "$RUN" --captures build/co_golden/images/<suite>   # co_golden 캡처 → render/
python3 "$FC" diff --run "$RUN" --label <label>          # plan 의 비교마다
python3 "$FC" literals --app <앱 루트> --base origin/main --run "$RUN"
python3 "$FC" summary --run "$RUN" --check-issues
  • coui 는 앱의 .dart_tool/package_config.json · pubspec.lock 이 해석한 CoUI 를 읽는다 — 스킬 저장소의 기준(coui_skills_basis)이 아니다. 버전 차이를 판정하는 기준이 앱의 릴리스여야, 그 차이를 앱 코드에서 "고치지" 않는다.
  • 라이브러리 ref 는 라이브러리를 마지막으로 그린 CoUI 의 components.json 이다. 디자이너가 게시한 라이브러리는 CoUI main 에서 그리므로 기본은 main 이다.
  • 같은 실행 폴더로 다시 돌릴 때(--run-id) Figma 쪽(frames-*.json · plan.json · figma/)은 재사용하고 렌더 · 보고서 · 요약은 늘 다시 만든다. 시안을 고쳤으면(색을 변수에 묶음 · 프레임 이름) 새 실행이다 — 옛 plan 과 내보내기는 고치기 전의 시안이다.

8. 도달 범위 — 여기까지만 참이다#

  • 굵기 한 단계(600 ↔ 500)는 픽셀 비교가 놓친다(실측 0.042%) — 이 스크립트만으로 대조하면(PNG 만) 지금도 그렇다. 사양(design-spec.figma.js)을 붙인 co_golden 캡처면 구조 비교가 굵기를 값으로 견주어 typography 차이 항목(fontWeight 600 → 500)과 그 글자의 소스 줄로 잡는다(co_golden 테스트에서 700 → 600 을 소스 줄과 함께 확인했다). 이 스크립트의 판정은 그대로 픽셀이다 — 그 차이 항목은 5절의 근거다. 글자의 굵기 · 크기는 텍스트 스타일 이름의 역할 체인이 정한다(cc-flutter:figma-to-coui F1).
  • 글자 상자 안에서는 가로 2 논리 px · 색 ΔE 10 까지 같은 것으로 본다. 다른 색의 글자는 잡지만(실측 108 px²), 같은 계열의 아주 가까운 색으로 바뀐 글자는 놓칠 수 있다. 사양이 있으면 co_golden 이 글자 색을 값으로(ΔE 3) 견준다.
  • 프레임 가장자리의 허용 반경 띠(1 논리 px)는 위치 차이를 세지 않는다.
  • 모양만 본다 — 동작(누름 · 스크롤 · 전환)은 보지 않는다(cc-marionette · cc-mcp-toolkit).
  • 시안의 색이 변수에 묶이지 않았으면 다른 밝기는 판정 불가다. 실측한 제품 파일 두 벌은 화면 페인트의 대부분이 리터럴이라 다크를 그릴 수 없었다 — 그 비교는 디자이너의 일로 돌아간다.

다른 검증과의 경계#

검증담당이 스킬과의 관계
시안 ↔ 렌더 픽셀 대조(게이트)이 스킬등록된 상태 프레임 × 밝기
고치며 수렴 cc-pixel-loop:pixel-loop (/cc-pixel-loop:loop) 같은 비교를 반복마다 돌린다
정적 회귀(골든) cc-flutter:golden-test-guide 코드가 스스로 승인한 기준 — 시안과 같다는 증거가 아니다
글자 · 컴포넌트 · 위치의 값 대조(차이 항목) cc-flutter:golden-test-guide 6절 — co_golden 디자인 분석 같은 캡처에서 영역의 근거를 낸다 — 판정은 이 스킬
Figma ↔ 위젯북 집합 대조/cc-blueprint:sync어느 화면이 어디에만 있는가
위젯 트리 · 앱 내부 상태 cc-marionette · cc-mcp-toolkit 실행 중인 앱
Jaspr DOM · 웹 회귀cc-jaspr-web브라우저 캡처를 이 스킬이 빌려 쓴다
  • cc-blueprint 의 references/recipes/compare-frames.figma.js · dark-frames.figma.js — Figma 쪽 입력
  • cc-blueprint 의 references/recipes/design-spec.figma.js — co_golden 구조 대조의 사양
  • cc-flutter:figma-to-coui — F1(텍스트 역할) · F3(인스턴스 덮어쓰기) · F7(알려진 차이)
  • cc-flutter:golden-test-guide — co_golden 캡처 · 서체 로드, 6절 디자인 참조 · 분석 · 갤러리 --design-report
  • cc-pixel-loop:pixel-loop — 고치며 수렴
  • cc-dev:run Step 7.3 · /cc-blueprint:build Step 5 — 이 스킬을 완료 기준으로 부르는 곳

검색어를 입력하면 문서를 찾습니다.

↑ ↓ 이동 · Enter 열기Esc 닫기