LogoSkills

/cc-coui-sync:absorb-apply — 캔버스에서 정한 것을 코드로 옮긴다

디자이너가 캔버스에서 확정한 결정을 코드로 옮기는 전 과정을 끝까지 몹니다 — 실측 survey, Core 계약 반영, 양 플랫폼 구현, export 재생성, 빌더·rename 표, 로컬 게이트, baseline 재촬영, main 도달, 플러그인 재빌드까지. 드리프트 이슈나 디자이너 결정 목록을 받아 실제로 코드에 반영해야 할 때, 흡수가 중간에 멈춰...

/cc-coui-sync:absorb-apply — 캔버스에서 정한 것을 코드로 옮긴다#

항목내용
실행 명령/cc-coui-sync:absorb-apply
분류cocode design
난이도●●● 높음
MCP 서버figma

한마디로#

디자이너가 Figma 에서 "이 버튼은 이만큼 커야 한다" 를 확정하면, 그 결정이 실제 제품 코드에 들어가야 캔버스와 화면이 같아집니다. 이 커맨드는 그 옮기는 일을 처음부터 끝까지 안내합니다 — 값을 재는 것부터 시작해, 코드를 고치고, 그림 검사 기준을 다시 찍고, 마지막으로 플러그인을 다시 빌드해 캔버스가 새 코드를 그리게 하는 데까지.

누가·언제 쓰나요#

  • /cc-coui-sync:absorb 가 "캔버스와 코드가 갈렸다" 는 신호를 만들었을 때
  • 디자이너가 결정 목록을 준 뒤 그것을 코드에 반영해야 할 때
  • 흡수를 하다 말았고, 어디까지 갔는지 · 무엇이 남았는지 다시 잡아야 할 때

왜 절차가 파일로 있나#

이 일은 한 세션에서 끝나지 않고, 끝나지 않은 채로도 화면은 옳아 보입니다. 흡수분이 머지되지 않은 브랜치에 있어도 그 브랜치에서 빌드한 플러그인은 캔버스를 옳게 그리므로, 좌초는 몇 주 뒤 "재생성했더니 달라졌다" 로만 드러납니다. 절차가 채팅 세션 안에만 살면 그 좌초를 막는 마지막 단계들(main 도달·재빌드)이 세션과 함께 사라집니다 — 실제로 그렇게 두 달치가 좌초했습니다.

실행 위치#

coco-de/coui 레포 루트. 모든 경로·가드·빌드가 그 레포의 것입니다.

이 파일은 절차(orchestration) 만 권위를 가집니다 — 각 단계가 지켜야 할 규범(계약 구조 · 커밋 규율 · 검증 형식)의 정본은 coui 레포 .claude/rules/ 의 해당 파일이며 충돌 시 그쪽이 우선합니다.

입력#

  • /cc-coui-sync:absorb 가 만든 드리프트 이슈, 또는 디자이너의 결정 목록(대화·문서).
  • 결정이 "무엇을 얼마로" 형태가 아니면(예: "간격이 이상함") 먼저 캔버스에서 값을 실측해 결정 가능한 형태로 만들어 확인받는다.

진행 원칙 (전 단계 공통)#

  • 전해 들은 주장은 실측 전 사실이 아니다. 감사·요약·이전 세션의 진술을 근거로 릴리스 노트나 커밋 메시지를 쓰지 않는다 — 커밋을 열고, 캔버스를 읽고, 값을 대조한 뒤 쓴다. 이 레포에서 "팔레트가 유채색에서 무채색으로 바뀌었다" 는 전언이 실측(매핑이 그 램프를 참조한 적 0회)으로 기각된 일이 있다 — 그대로 적었으면 거짓 breaking 고지가 영구히 남았다.
  • 검증은 Command / Expected / Evidence / Pass 4단으로 대화에 출력한다 (process/verify.md).

1. 실측 survey — 캔버스가 무엇을 말하는가#

결정 대상 컴포넌트마다 use_figma(읽기 전용, figma-use 스킬 로드 후)로 값을 직접 읽어 표를 만든다: 멤버 이름 전수 · 치수 · 타이포(role 항등 여부) · 색 바인딩 · 축 구성. 코드 쪽 현재값과 나란히 놓는다.

  • 캔버스 값이 기존 토큰 사다리 위에 있는지 확인한다. 사다리 밖 값이면 그 자체가 별도 결정(토큰 추가 vs 반올림)이므로 진행 전에 확인받는다.
  • 멤버 이름 전수는 뒤 5단계 rename 표의 모집단이다 — 여기서 세지 않으면 거기서 셀 수 없다.

2. Core 계약 반영#

core/style-contract.md 표준대로. 이 절차에서 특히 밟는 지뢰만 적는다:

  • 사라지는 공개 심볼은 예고한다@Deprecated('… (since <이번에 나갈 버전>)'). since 는 "이 줄이 처음 출하되는 버전" 이다. 마커가 태그 이전 커밋에 있는지 git tag --contains 로 확인하면 갈리지 않는다.
  • 후임이 스칼라가 아니면 복원 불가를 그 자리에 적는다 — 단일 상수가 per-축 표로 대체될 때, 옛 상수를 fallback 으로 되살리면 표가 대체한 값이 부활한다. 왜 되돌아올 수 없는지를 값과 함께 소스에 남긴다 (안 적으면 다음 사람이 누락으로 읽고 되돌린다).
  • 소비처를 잃은 default* 는 배선하거나 지우거나 baseline 등록 — check_unread_style_defaults.py 의 지시대로. deprecated 상수는 제거 창이 닫힐 때까지 baseline 에 남는 것이 정본 경로다.

3. 양 플랫폼 구현#

unify-* 스킬 패턴을 따른다 (한쪽만 고치면 반쪽 fix — principles/single-source-of-truth.md). 렌더가 바뀌는 변경이면 어느 골든·스냅샷·e2e 가 움직일지 이 시점에 예측 목록을 적어둔다 — 7단계에서 그 목록과 실측 diff 를 대조하는 것이 검증이다.

4. export 재생성#

dart run packages/coui_core/bin/export_figma_components.dart   # → components.json
bash scripts/guards/structure/check_figma_export_fresh.sh      # fresh 확인

토큰 값이 바뀌었으면 토큰 export 도 같이. Figma Variables 는 코드에서 생성된다 — 캔버스에서 변수值을 직접 고친 것은 다음 import 가 되돌리므로, 변수 쪽 결정은 반드시 이 경로(코드 → export → import)로 온다.

5. 빌더 + rename 표#

  • 정체성은 mint-by-name — 키는 이름으로 살아남는다. 세트의 축이 바뀌면 옛 이름 → 새 이름을 VARIANT_RENAMES전수 적는다. 1단계에서 센 라이브 멤버 전원이 유지·rename·폐기 중 정확히 하나로 설명돼야 한다 (UNCOVERED 0).
  • rename 표의 순서는 load-bearing 이다 — 새 축 세대는 자기 섹션의 끝에 붙인다. 순서를 어기면 통과하는 형태로 조용히 틀린다.
  • 셀프테스트 스위트 + npx tsc --noEmit 를 돌린다. 빌더가 chromeless 를 진술하는 필드를 얻었으면 suppressFieldChrome 적용까지가 한 단위다 — 적용하고 나면 그 필드의 unread-면제가 불필요해져 allowlist 가드가 스스로 지우라고 말한다. 그 말을 따른다.

6. 로컬 게이트 (SDK 없이 도는 것 전부)#

for g in scripts/guards/*/*.sh; do case  " $g "   in */lib/*) continue;; esac; bash  " $g "   || echo  " ❌ $g " ; done
  • 스윕이 도는 동안 git 작업·다른 가드를 병행하지 않는다. 같은 작업 트리를 공유하므로 병행은 어느 쪽도 믿을 수 없는 결과를 만든다 — 그렇게 만들어진 거짓 실패를 쫓느라 진짜 플레이키 가드 진단이 한 번 오염된 적 있다.
  • 로컬 red 를 CI red 로 단정하지 않는다 — 로컬 SDK 가 floor 미만이면 chain/번들류 가드가 환경 산물로 red 가 난다. 판정법: 같은 가드를 origin/main 에서 돌려 같은 모양으로 red 면 환경, 브랜치에서만 red 면 내 것.
  • chain 생성물 재생성 시 의미 변경만 커밋한다. 생성기는 결정적이지만 포맷터는 SDK 판을 탄다 — 로컬 판이 다르면 수십 파일이 줄바꿈만 다른 churn 으로 바뀌고, 그것을 커밋하면 CI 에서 도로 stale 이 된다. git diff --numstat 로 의미 변경(대개 소수)과 churn 을 가르고 churn 은 stash 로 치운다.

7. 재촬영 — 로컬 금지, workflow dispatch#

렌더가 의도적으로 바뀌었으면 baseline 이 따라와야 한다. 로컬에서 뜨지 않는다 — 게이트가 도는 러너의 SDK·래스터라이저와 갈리면 그 baseline 이 모든 미래 PR 을 오탐 red 로 만든다 (parity-baseline.yml 헤더가 정본 근거).

gh workflow run preset-baseline.yml --ref  < 브랜치 >   -f reason= " < 무엇이 왜 > " 
 gh workflow run golden-baseline.yml --ref  < 브랜치 >   -f reason= " < 무엇이 왜 > "
  • 워크플로 파일이 그 브랜치에 있어야 dispatch 가 되고, 렌더를 바꾼 코드가 있는 브랜치를 --ref 로 가리켜야 no-op 이 아니다.
  • 나온 브랜치의 diff 를 기계로 대조한다: 움직인 표면/골든 목록 == 3단계의 예측 목록인가. 밖의 것이 섞였으면 커밋하지 않는다 — 그건 이 브랜치가 모르고 바꾼 렌더고, 재촬영으로 덮으면 안 되는 것이다.
  • 일치하면 승격 커밋을 기능 브랜치로 cherry-pick 한다 — baseline 과 그것을 바꾼 코드가 같은 PR 에서 리뷰돼야 한다.
  • 골든은 세트가 둘이고 검사 주체가 다르다는 것을 안다: 일반 flutter test 스텝은 macos 세트를, golden:test 스텝은 ci 세트를 본다. 한쪽만 움직인 diff 는 버그가 아니라 이 구조다.
  • 렌더 e2e(프리셋 바닥값류)가 새 결정과 충돌하면 — 예: 보더를 의도적으로 0 으로 만든 표면 — 값을 되돌리는 게 아니라 표면 단위로, 렌더된 측정값과 함께 게이트에서 제외한다. 같은 sweep 이 바꾼 다른 표면은 측정이 다르게 나올 수 있으므로 sweep 단위로 판정하지 않는다.

8. 커밋 · PR 규율#

  • breaking 은 커밋당 BREAKING CHANGE: 푸터 하나, 같은 줄에서 시작. 릴리스 도구는 첫 푸터의 같은-줄 텍스트만 불릿으로 삼는다 — 다음 줄 들여쓰기 목록은 빈 불릿이 되고, 둘째 이후 푸터는 경고 없이 버려진다. CHANGELOG 는 append-only 라 태그가 찍히면 되돌릴 수 없다 (check_breaking_footer_renders.sh 가 두 형태를 막는다).
  • breaking 커밋이 둘 이상인 PR 은 rebase 로 머지한다 — squash 는 푸터를 하나로 접어 고지를 잃는다. PR 본문 맨 위에 그 사실을 적는다.
  • base 는 main. 다른 기능 브랜치 위에 쌓으면 CI 가 돌지 않고(트리거 브랜치 조건), base 브랜치가 머지되며 삭제될 때 자식 PR 은 재타겟되지 않고 닫힌다 — 닫힌 PR 은 base 변경도 재오픈도 불가라 새 PR 로 다시 열어야 한다.
  • 흡수는 커밋마다 즉시 push 한다. 원격에 사본 없는 커밋은 이 디스크와 수명을 같이한다.

9. 종결 체크리스트 — 여기까지가 흡수다#

머지 자체는 끝이 아니다. 아래가 비면 캔버스는 여전히 옛 코드(또는 세션-로컬 상태)를 그린다:

  • PR 이 main 에 머지됨 (breaking 여럿이면 rebase 머지였는지 확인)
  • main 체크아웃에서 npm run build — 플러그인은 이 컴퓨터의 빌드 산출물을 읽으므로, 재빌드 전까지 캔버스는 빌드 시점의 ref 를 그린다
  • Generate 1회 → 대상 컴포넌트가 결정대로 그려지는지 디자이너 확인
  • /cc-coui-sync:status 로 표가 전부 ✅ 인지 — 특히 "main 미도달 흡수 0" 과 "번들 출처 = main"
  • 릴리스 노트 관점: 이번 흡수의 breaking 이 다음 릴리스 PR 의 CHANGELOG 에 실제로 렌더되는지, 그 PR 이 생성됐을 때 눈으로 확인