designpaca/packages/skill/references/change-review.md
Yun Chan 88c11682b1 feat(skill): route fixes, delivery report, composition diversity, platform refresh
- 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
2026-10-06 16:55:38 +09:00

343 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# change-review — diff·PR·커밋 범위 리뷰
**리뷰 경로**에서 연다. 화면 전체가 아니라 **git diff·PR·커밋 범위로 정의된 변경**을 스코프로 잡는 요청("이 PR 봐줘", "이 브랜치 확인해줘", "방금 커밋 리뷰해줘", "이거 고친 거 회귀 있는지 봐줘")이 트리거다. 화면 전체를 감사할 때는 이 문서가 아니라 [preflight.md](preflight.md)·[audit-gate.md](audit-gate.md)를 쓴다. 이 문서가 소유하는 것은 **스코프를 어떻게 잡는가**와 **finding에 어떤 상태를 매기는가** 두 가지, 그리고 화면 감사까지 포함한 **리뷰 경로 전체의 비변경 계약**(§1-1)이다. 무엇이 얼마나 나쁜가(심각도·에스컬레이션·저비용 수정 사다리)는 [critique.md](critique.md) 소관이며, 이 문서는 그것을 다시 정의하지 않는다. 정확성·테스트·보안·성능은 프로젝트의 일반 코드 리뷰 소관이다 — 한 번만 짚고 넘어간다[SKILL-INTERFACE-REVIEW].
이 문서의 **필수 절차**는 리뷰를 진행하는 과정의 규칙이다. 하드 게이트·프로젝트 계약·관찰 후보라는 판정 위계는 finding 자체에만 붙인다 — 절차 규칙을 하드 게이트라 부르면 심각도 매핑(하드 게이트 = Blocking)과 섞인다.
## 이 문서를 읽는 법
| 상황 | 읽을 곳 |
|---|---|
| 리뷰하다가 게이트 설치·빌드·의존성 추가가 필요해 보인다 | §1-1 |
| 렌더를 볼 수 없는데 디자인 리뷰를 요청받았다 | §11 "렌더가 없을 때" |
| 리뷰 대상 자체가 뭔지부터 정해야 한다 | §2 |
| 락파일·스냅샷 같은 게 스코프에 잡혀 노이즈가 커진다 | §3 |
| 토큰 파일 하나 고쳤는데 몇 화면에 퍼지는지 모른다 | §4 |
| "고치다가 실수로 뭔가 지운 것 같다"를 확인해야 한다 | §5 |
| "이 버그 우리가 만들었나 원래 있었나"를 가려야 한다 | §6 |
| PR이 주장한 걸 다 했는지 확인해야 한다 | §7 |
| detached HEAD·shallow clone·리베이스 도중이다 | §8 |
| 파일이 이동+편집됐다 | §9 |
| GitHub·Forgejo·Gitea의 PR을 가져와야 한다 | §10 |
| 리포트를 어떻게 쓰는지 | §11 |
| 심각도를 어디서 가져오는지 | §12 |
---
## 0. 언제 이 문서를 연다
트리거: 사용자가 브랜치·PR·커밋 범위·미커밋 변경을 구체적으로 지목한다. "이번에 뭐가 바뀌었나"를 묻는 요청이지 "이 화면이 지금 맞나"를 묻는 요청이 아니다. 두 질문은 다르다 — 전자는 diff의 `-`쪽과 `+`쪽을 함께 읽고 회귀를 찾는 것이고, 후자는 렌더된 최종 상태만 본다. 리뷰 경로는 0(스코프 확정) → 이 문서의 절차 → [critique.md](critique.md) 형식의 보고로 끝나며, 구현이 필요하면 사용자 요청에 따라 국소·연장 경로로 올라간다.
0단계 브리프 인터뷰(무엇을 만들지 확인)와 이 문서의 "스코프가 불분명하면 묻고 기다린다"(§2, 무엇을 검토할지 확인)는 목적이 다르다. 둘을 섞지 않는다. 검증 못 한 항목을 "미검증"으로 표시하고 진행하는 것(§11)도 0단계 인터뷰와는 별개 축이다 — 인터뷰는 시작 전에, 미검증 표시는 끝난 뒤에 쓴다[SKILL-INTERFACE-REVIEW].
---
## 1. 읽기 전용 계약
**필수 절차.** 리뷰 대상 브랜치·PR·커밋을 체크아웃·switch·stash·reset하지 않는다. 작업자가 열어 둔 파일을 리뷰 스킬이 덮어쓰거나 버리게 만드는 사고를 막는 안전 계약이며, 예외 없이 지킨다.
- **허용**: `git fetch`(원격 ref를 `.git` 안에만 쓴다), `git show <ref>:<path>`로 파일 읽기, `git blame`, `git diff`, `git log -L`, `git grep <ref>`.
- **금지**: `git checkout <ref>`, `git switch <ref>`, `git stash`, `git reset --hard`, 포지 CLI의 `pr checkout`류 명령.
- **파일은 워킹트리 사본이 아니라 `git show <ref>:<path>`로 읽는다.** 특히 포크에서 온 PR은 워킹트리와 다른 파일일 수 있다.
- **렌더 검증이 필요하면**: 옵트인이다. 프로젝트가 저렴한 프리뷰(스테이징 배포 등)를 이미 제공하거나 사용자가 명시적으로 요청하지 않는 한, 시각·런타임 주장은 **"미검증"**으로 남긴다(§11). 격리가 필요하면 저장소 안 디렉터리에 `git worktree add`로 임시 워크트리를 만들고 끝나면 `git worktree remove`로 정리한다 — `rm -rf`로 지우지 않는다. 리뷰 대상 저장소가 무엇이든 같은 위생을 지킨다[SKILL-INTERFACE-REVIEW]. 워크트리 생성도 저장소에 흔적을 남기므로 §1-1의 동의 대상이다.
### 1-1. 리뷰 경로 전체의 비변경 계약
**필수 절차.** 위 git 규칙은 diff 리뷰만의 것이 아니다. SKILL.md의 **리뷰(평가만) 경로 전체** — diff·PR 리뷰든, 화면 감사(5′)든 — 는 사용자의 프로젝트를 바꾸지 않는다. 평가만 요청한 사용자의 워킹트리가 리뷰 뒤에 더러워져 있으면, 그것은 리뷰가 아니라 요청하지 않은 변경이다(레드팀 01 F10: 게이트 설정 생성·devDependency 설치·빌드 산출물이 실제로 남는 경로가 있었다).
| 하지 않는 것 | 이유 |
|---|---|
| `tools/design-gate.mjs --init`으로 프로젝트에 `gate.config.json`을 만드는 것 | 프로젝트 파일 생성이다 |
| `npm i -D`·`pnpm add -D` 등 의존성 설치, `package.json`·락파일 수정, `"verify"` 같은 스크립트 추가 | 의존성·설정 변경이다. 스크립트에 스킬 설치 경로(사용자 홈)가 박혀 팀원·CI에서 깨지기도 한다 |
| 프로젝트 디렉터리 안에 빌드 산출물(`dist/` 등)·스크린샷·보고서 파일을 남기는 것 | 리뷰 결과는 응답으로 전달한다 |
| `design.md` 작성·추기 | 6단계는 리뷰 경로에 없다 |
| 전역 도구 설치(`npm i -g`, 브라우저 내려받기) | 사용자 환경 변경이다. 크기가 수백 MB일 수 있다 |
**측정이 필요할 때 할 수 있는 것**
1. **프로젝트 밖 임시 디렉터리에서 돌린다.** 게이트 설정·캡처·측정 스크립트는 하네스의 스크래치 디렉터리나 OS 임시 디렉터리에 만들고, 스킬 경로의 `tools/design-gate.mjs`를 그 설정으로 실행한다. 결과 파일도 그곳에 둔다. 떠 있는 주소가 있으면 설정 파일도 필요 없다 — 아래 명령은 프로젝트 디렉터리에 아무것도 쓰지 않는다(`--out-dir`을 빼면 리포트 파일 없이 표준 출력으로만 낸다). `--init`은 리뷰 경로에서 쓰지 않는다.
```
node <스킬>/tools/design-gate.mjs --url <주소> --out-dir <프로젝트 밖 임시 경로>
```
`url`만 준 실행은 파일을 읽는 검사(죽은 선택자·토큰 위생)가 미검증으로 남는다. 게이트가 `미검증`으로 낸 줄과 브라우저·`puppeteer-core`가 없어 못 돌린 계층은 §11의 미검증으로 옮긴다. 옵션과 종료 코드는 [audit-gate.md](audit-gate.md) "게이트 실행"에 있다.
2. **이미 떠 있는 것을 쓴다.** 사용자가 띄워 둔 개발 서버, 배포된 URL, 스테이징 프리뷰처럼 프로젝트를 바꾸지 않고 접근할 수 있는 렌더를 우선한다.
3. **바꿔야만 측정할 수 있으면 먼저 묻는다.** 빌드·서버 기동이 프로젝트 안에 파일을 쓰거나, 도구 설치가 필요하면, 무엇이 생기고 어떻게 지울지 한 줄로 말하고 동의를 받는다. 이 질문은 0단계 인터뷰 라운드가 아니라 리뷰 범위 확인(§2와 같은 축)이다. 물었으면 턴을 끝낸다.
4. **못 돌린 검사는 미검증이다.** 동의가 없거나 도구가 없으면 그 항목을 §11의 "미검증"으로 남긴다. 소스만 보고 시각 주장을 통과로 추정하지 않는다.
리뷰 중에 사용자가 "고쳐 줘"라고 하면 그때부터는 리뷰 경로가 아니다. SKILL.md 0단계 경로 판정으로 국소·연장 중 어디로 가는지 정하고 **경로를 바꿨다고 말한 뒤** 그 경로의 규칙(5단계 하드 게이트, 6단계 기록)을 따른다. "리뷰하면서 고칠 수 있는 건 고쳐 줘"도 같다 — 손댄 섹션 수로 경로를 판정하고, 리뷰 경로의 자동 적용이라는 이유로 5·6단계를 건너뛰지 않는다.
---
## 2. 스코프 계산
**필수 절차.** 리뷰 대상을 지어내지 않는다 — 무엇을 봤다고 주장하는지 먼저 확정하고, 그 확정 근거를 스코프 블록(§11)에 남긴다.
### 대상 미지정일 때의 우선순위
사용자가 타깃을 지목하지 않으면 아래 순서로 시도하고 **첫 매치에서 멈춘다**.
1. `HEAD`가 `git merge-base origin/<default-branch> HEAD` 대비 앞서 있다 → 그 범위 **+** 미커밋 변경. 커밋 수와 미커밋 파일 수를 **따로** 보고한다.
2. 워킹트리가 dirty(추적되는 파일이든 아니든)하다 → 미커밋 변경만.
3. 둘 다 아니다 → "리뷰할 변경이 없음"(아래 함정 참고).
**작업 트리를 먼저 확인해야 하는 이유**: 순서를 뒤집으면 한 줄짜리 포매팅 수정이 열두 커밋짜리 브랜치를 가리면서도 "전체를 봤다"고 보고하는 사고가 난다. 추적되지 않은 새 파일도 포함한다 — 새로 추가한 컴포넌트가 "전체 커버"라 주장하는 스코프에서 조용히 빠지는 것을 막는다[SKILL-INTERFACE-REVIEW].
### 대상이 명시됐을 때
| 타깃 | 의미 |
|---|---|
| `working` | 워킹트리 변경(추적+미추적) |
| `staged` | 스테이지된 변경만 |
| `branch` | 현재 브랜치 vs 기본 브랜치, merge-base 기준 |
| `pr <n>` / `mr <n>` | §10 |
| 단독 `<ref>` | 그 커밋 하나 |
| `<a>..<b>` | 두 점 — 끝점끼리 직접 비교 |
| `<a>...<b>` | 세 점 — `merge-base(<a>,<b>)`와 `<b>` 비교 |
**두 점과 세 점을 임의로 바꾸지 않는다.** 사용자가 쓴 점 개수를 그대로 존중한다. `release..feature`를 세 점으로 바꾸면 `release`와 merge-base 사이의 변경분을 통째로 놓칠 수 있고, 그게 바로 사용자가 물어본 것일 수 있다.
**브랜치 diff는 merge-base(세 점) 기준이다.** 두 점 diff는 기본 브랜치에 이미 올라간 업스트림 커밋까지 변경으로 잡아버린다.
### 함정 — `git diff HEAD`가 놓치는 것
`git diff HEAD`는 **추적된 변경만** 본다. 미커밋 작업이 포함될 수 있는 타깃(`working`, `branch` + 워킹트리 dirty)에서는 반드시 `git ls-files --others --exclude-standard`와 짝지어 새로 추가된 파일을 잡는다. 안 그러면 새 컴포넌트가 "전체 커버"라 주장하는 스코프에서 빠진다.
### 리뷰할 게 없을 때 — 임의로 대체하지 않는다
**필수 절차.** 클린 트리 + 기본 브랜치 대비 앞선 커밋이 없으면, 이것은 "존재하지 않는 변경을 리뷰해 달라는 요청"이다. **마지막 커밋(`HEAD~1..HEAD`)으로 임의 대체하지 않는다.** 마지막 커밋은 우연히 거기 있게 된 무엇(머지 커밋이거나 남의 작업일 수 있다)이고, 그것에 대한 보고는 사용자가 실제로 의미한 것과 구분되지 않는다[SKILL-INTERFACE-REVIEW].
발명하는 대신 사실을 먼저 모으고 제시한 뒤 **대기**한다.
1. 현재 브랜치, 클린 여부, 기본 브랜치 대비 커밋 수, 마지막 커밋의 짧은 SHA와 제목을 모은다.
2. 현재 브랜치에 열린 PR/MR이 있는지 확인하고, 있으면 **가장 먼저** 제시한다 — 커밋이 이미 머지돼 "변경 없음"으로 보여도 그 PR은 여전히 사용자가 의미한 것일 수 있다.
3. 정확히 3가지 경로만 제시한다: (a) 마지막 커밋 — SHA와 제목을 구체적으로 밝혀 뭘 받을지 보이게 함, (b) 사용자가 지정하는 타깃, (c) 저장소 전체 인터페이스 감사(이건 change review가 아니라 [critique.md](critique.md)의 화면 감사로 그대로 핸드오프 — 스코프 블록·상태·Pre-existing 섹션 없이).
제외 규칙(§3) 적용 후 스코프가 비었을 때도 같은 방식으로 묻는다. **"아무것도 볼 게 없는 상태"를 통과로 보고하지 않는다.**
---
## 3. 제외 경로와 예외
**프로젝트 계약.** 아래는 리뷰 스코프에서 제외해 노이즈를 줄이는 기본값이다 — 프로젝트가 다른 생성물 경로를 쓰면 맞춰 조정한다.
| 카테고리 | 패턴 |
|---|---|
| 락파일 | `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lock`, `bun.lockb`, `Cargo.lock`, `composer.lock`, `Gemfile.lock`, `poetry.lock`, `uv.lock` |
| 스냅샷·픽스처 | `__snapshots__/`, `*.snap`, `*.approved.*`, `test-results/`, `playwright-report/` |
| 생성 산출물 | `dist/`, `build/`, `out/`, `.next/`, `.turbo/`, `coverage/`, `*.min.js`, `*.min.css`, `*.map` |
| 생성 소스 | `*.gen.ts`, `*.generated.*`, 빌드가 뱉은 `*.d.ts` |
| 벤더 코드 | `vendor/`, `third_party/`, `node_modules/` |
| 바이너리·미디어 | 원본 자산 바이트 자체(폰트·이미지 예외는 아래 참고) |
**무엇을 제외했는지 리포트에 이름으로 밝힌다.** 제외 후 스코프가 비면 §2의 "리뷰할 게 없음" 처리와 같다.
**예외 2가지는 스코프에 남는다** — 바이트 자체가 아니라 그것을 참조하는 코드를 본다.
- 추가·교체된 **폰트 파일**: [typography.md](typography.md)가 다루는 로딩·서브셋 계약이 바뀌었을 수 있다.
- 컴포넌트에 추가된 **이미지**: `alt` 텍스트 분류와 파일 조달·크롭 모두 [images.md](images.md) 소관.
**언더익스클루전 함정 2가지**
1. `*.lock`은 `yarn.lock`·`Cargo.lock`은 잡지만 `package-lock.json`·`pnpm-lock.yaml`은 못 잡는다 — 표의 확장자를 각각 커버해야 한다.
2. `**`는 glob 매직이 필요하다. 없으면 `*`가 `/`를 못 건너뛰어 `**/dist/**`가 루트 레벨 `dist/`를 놓친다.
**검증**: 제외 pathspec 유무로 diff를 두 번 돌려, 파일 수가 정확히 이름 붙인 만큼 줄었는지 확인한다[SKILL-INTERFACE-REVIEW].
---
## 4. 파급 범위 — diff는 표면이 아니다
**프로젝트 계약.** 변경 파일은 증거일 뿐 리뷰 대상 전부가 아니다. 실제로 리뷰할 것은 그 파일이 렌더되는 **표면 전체(blast radius)**다.
- **기본은 1홉 확장**: 변경된 컴포넌트·함수를 직접 import·호출하는 것까지.
- **2홉 확장은 디자인 토큰·테마 값·공유 프리미티브에 한정**한다 — 한 줄이 제품 전체에 닿을 수 있는 파일이라서다. [tokens.md](tokens.md)가 유일 원본인 토큰 파일이 여기 해당한다.
- **고정 개수 상한을 두지 않는다.** 대신 위험 기준으로 고르고 선언한다: 사용자에게 노출되는 경로(라우트·레이아웃 진입점) 먼저, 그다음 importer 수가 많은 순, 동률이면 같은 패키지·기능 디렉터리 근접성. 프로젝트에 라우트 개념 자체가 없으면(컴포넌트 라이브러리 등) 1순위를 건너뛰고 바로 importer 수로 정렬한다. **몇 개를 확장했고 몇 개를 확장하지 않았는지 리포트에 명시한다** — 상한 없는 스윕은 주장할 수 있는 커버리지를 지지하지 못하고, 컷오프를 밝히지 않으면 완전한 것처럼 읽힌다.
### 컨슈머 검색 명령
```bash
# 리뷰 중인 ref를 대상으로 검색한다 — 워킹트리를 검색하면
# 이 변경 자체가 추가한 importer를 놓친다(특히 PR에서)
git grep -n "ComponentName" "$REVIEWED_REF" -- '*.tsx' '*.astro'
# 패턴이 -로 시작하면(예: 토큰명 --color-*) -e로 옵션 파싱을 막는다
git grep -n -e "--color-brand" "$REVIEWED_REF"
```
결과는 `<rev>:path/to/file` 형태로 오며 `git show`로 읽는다(워킹트리 사본 금지, §1). **토큰·테마 값이 바뀐 경우 파일명이 아니라 토큰 이름으로 검색한다** — 컨슈머는 토큰을 import하지 않고 이름으로 참조하기 때문이다[SKILL-INTERFACE-REVIEW].
---
## 5. 제거된 쪽 읽기
**필수 절차다.** 회귀는 변경 후 상태에서는 보이지 않는다. `+`쪽만 읽고 끝내지 않는다 — 모든 헝크의 `-`쪽을 아래 표와 대조한다. 이 절차 자체(제거된 쪽을 읽는다)는 건너뛰지 않는 필수 단계다. 다만 **표에 걸린 각 항목의 실제 심각도**는 그 항목을 소유하는 문서(accessibility.md·typography.md 등)의 판정 위계를 따른다 — 여기서 다시 정의하지 않는다.
| `-`쪽에서 제거된 것 | 소유 문서 | 확인할 것 |
|---|---|---|
| `aria-label`, `aria-labelledby`, `aria-describedby`, `aria-live`, `role=` | [accessibility.md](accessibility.md) | 컨트롤·영역이 접근 가능한 이름·설명·알림을 잃었는가 |
| `<label`, `for=`, `scope=` | [accessibility.md](accessibility.md) | 필드·표 셀이 프로그램적 연결을 잃었는가 |
| `alt=` | [images.md](images.md) | 이미지가 대체 텍스트를 잃었는가 — 분류는 images.md §5 |
| `<button>`, `<a>`, `<nav>`, `<main>`, `<ul>`이 `div`·`span`으로 교체 | [accessibility.md](accessibility.md) | 키보드·보조기술 동작이 스타일과 맞바뀌었는가 |
| `:focus-visible`, `:focus`, `outline`, `tabindex` | [accessibility.md](accessibility.md) | 포커스 표시자를 잃었거나 탭 순서에서 빠졌는가 |
| `prefers-reduced-motion`, `prefers-contrast` | [accessibility.md](accessibility.md) | 모션·대비가 시스템 선호를 더는 존중하지 않는가 |
| 논리 속성이 `left`·`right`로 교체 | [layout.md](layout.md) | 방향 인식 레이아웃이 빠졌는가 |
| `lang=`, `dir=` | [typography.md](typography.md) | 언어 메타·텍스트 방향이 빠졌는가 |
| `text-wrap`, `line-clamp`, `overflow-wrap`, `tabular-nums` | [typography.md](typography.md) | 텍스트 렌더링·줄바꿈·숫자 정렬이 조용히 바뀌었는가 |
| 색 토큰이 리터럴로, 또는 더 옅은 토큰으로 교체 | [color.md](color.md) | 렌더된 대비 쌍이 실패할 수 있다 — 실측한다 |
| 사용자 노출 문자열이 삭제·축약 | [product-copy.md](product-copy.md) | 라벨·오류·빈 상태가 담던 정보를 잃었는가 |
**등가 대체 — 아래 7가지는 회귀가 아니다.** 먼저 확인하지 않으면 정당한 리팩터를 회귀로 오보한다.
1. `aria-label`이 화면에 보이는 텍스트를 가리키는 `aria-labelledby`로 바뀜.
2. 명시적 `role`이 사라졌지만 요소 자체가 네이티브 동급이 됨(`role="button"`인 `div` → `<button>`).
3. `outline`이 여전히 포커스 표시 규칙을 충족하는 `box-shadow` 포커스 링으로 교체.
4. 네이티브로 focusable해진 요소에서 `tabindex="0"`이 빠짐.
5. 색 리터럴이 같은 렌더 페어를 측정하는 토큰으로 교체.
6. 물리적 속성이 논리적 대응으로 교체(이건 회귀가 아니라 수정 그 자체).
7. 문자열이 삭제가 아니라 번역 카탈로그·상수 파일로 이동(다국어 프로젝트에 한정 — designpaca가 다루는 단일 로케일 랜딩페이지는 대부분 카탈로그 자체가 없으므로 이 항목이 적용되지 않는다).
### 검색 명령
```bash
git diff -U0 "$BASE"...HEAD -- '*.tsx' '*.css' \
| grep -E '^-[^-]' \
| grep -E 'aria-|role=|alt=|focus|tabindex|prefers-'
```
**컨텍스트 없이 결론 내지 않는다.** `-U0`는 의도적으로 컨텍스트를 숨기며, 제거된 속성은 그것이 붙어 있던 요소 없이는 의미가 없다. 헝크 컨텍스트를 넓혀(`-U3` 이상) 실제로 무엇이 없어졌는지 확인한 뒤 표로 가져간다[SKILL-INTERFACE-REVIEW].
---
## 6. 상태 분류
**필수 절차.** 모든 finding에 상태 하나를 매긴다 — 생략하지 않는다.
| 상태 | 의미 |
|---|---|
| `Introduced` | 이 변경이 새로 만들었다 |
| `Regression` | 이 변경이 이전에 정상이던 것을 약화시켰다 |
| `Pre-existing` | 손댄 코드 안에 있지만 이 변경이 원인이 아니다 |
**파일이 아니라 diff가 실제로 건드린 지점으로 상태를 매긴다.** 헝크에서 몇 줄 떨어진, 이번 변경이 손대지 않은 줄은 `Pre-existing`이다. 필요하면 base ref와 대조한다:
```bash
git blame -L <line>,<line> "$BASE" -- path/to/file
```
**`Pre-existing`은 §12의 심각도 집계와 별도로 나열한다.** 레거시 파일 하나를 건드렸다고 전체 감사 범위가 되는 것을 막는다. 심각도 높은 순으로 정렬한다. 고정 개수 상한은 두지 않는다 — [critique.md](critique.md) §7이 이미 정한 원칙("숫자 상한은 근거 없는 수치이고, 스코프를 좁히는 판단이 상한보다 먼저다")을 여기서도 그대로 따른다. 목록이 감당하기 어렵게 길면(레거시 파일을 광범위하게 건드린 변경) 전부 나열하는 대신 "Pre-existing N건, 이 변경 책임 아님"으로 요약하고 근거를 밝힌다.
---
## 7. 의도 대비 완성도
**프로젝트 계약.** PR 제목·본문, 연결된 이슈, 커밋 메시지를 읽고 인터페이스가 그 주장을 실제로 이행하는지 본다. 표면 리뷰만으로는 안 보이는 "미완성 변경"을 드러내는 항목들이다.
- **변형 상태 매트릭스**: 새 variant·size·theme가 hover·focus·active·disabled·loading·selected 중 일부에만 적용됐는가. 새 컴포넌트에 empty·loading·error·disabled·narrow-width 상태가 빠졌는가.
- **형제 표면 대칭**: 한 표면에만 추가된 컨트롤이, 이미 그 동료 컨트롤을 가진 형제 표면들에는 없는가.
- **번역 카탈로그**(다국어 프로젝트에 한정. 언제: 프로젝트가 실제로 번역 카탈로그를 쓸 때만 — 단일 로케일 하드코드 카피가 표준인 프로젝트에는 적용하지 않는다): 새 사용자 노출 문자열에 카탈로그 항목이 없는가.
**scope creep(변경이 너무 많은 일을 하는가)은 이 문서가 보고하지 않는다.** 그건 인터페이스 문제가 아니라 프로세스 문제다[SKILL-INTERFACE-REVIEW].
---
## 8. 까다로운 저장소 상태
- **Detached HEAD**: 기본 브랜치 대비 merge-base를 그대로 쓰되, 스코프 블록의 "브랜치" 필드에는 브랜치명 대신 **SHA**를 적는다.
- **Shallow clone**(CI 기본값 — `merge-base`가 아무것도 못 돌려준다): `git fetch --deepen=50` 후 재시도, 안 되면 `--deepen=200`, 그래도 안 되면 "스코프 해석 불가"로 보고한다. `--deepen`은 `.git` 아래에만 쓰므로 §1의 읽기 전용 계약을 어기지 않는다.
- **리베이스·머지 진행 중**(조용히 실패하는 유일한 케이스 — `git diff`가 성공해서 뭔가를 돌려주지만 그게 실제 변경이 아니다): `git rev-parse --git-path rebase-merge`(및 `rebase-apply`, `MERGE_HEAD`, `CHERRY_PICK_HEAD`)로 감지한다. **`.git/` 경로를 직접 테스트하지 않는다** — 링크된 워크트리 안에서는 디렉터리가 아닐 수 있다. 감지되면 "트리가 작업 도중"이라고 말하고 멈춘다.
- **그 외 모든 실패**(원격 없음, unrelated histories, 커밋 없는 저장소)는 `merge-base`에서 실패로 나타난다 — base를 못 정했다고 말하고 멈춘다. **이름 붙일 수 없는 범위는 리뷰하지 않는다**[SKILL-INTERFACE-REVIEW].
---
## 9. 이름 변경 감지
리네임 감지는 git에서 기본 켜져 있다(`--name-status`에서 `R100 old/path new/path`). 같은 변경에서 이동과 편집이 동시에 일어났으면 감지 윈도를 넓힌다:
```bash
git diff --find-renames=40% --find-copies-harder "$BASE"...HEAD
```
**리네임은 삭제+추가가 아니라 이동으로 리뷰한다.** 이동에서 살아남은 코드는 손대지 않은 코드다 — 진짜 편집분만 스코프에 넣는다. 이렇게 하지 않으면 파일 이동이 잦은 리팩터에서 안 바뀐 코드가 새 결함처럼 보고된다.
---
## 10. PR·MR 가져오기 (포지 중립)
**언제**: 사용자가 PR·MR 번호나 링크를 지목했을 때.
GitHub·Forgejo·Gitea·GitLab은 CLI와 PR ref 노출 방식이 서로 다르고, 이 스킬은 특정 포지를 전제하지 않는다. 아래는 어떤 포지에서든 통하는 git 기반 절차이며, 포지 CLI(`gh`, `tea`, `glab` 등)가 있으면 메타데이터(제목·본문·상태) 조회를 더 짧게 해 준다.
1. **head를 로컬 ref로 받는다.** 많은 포지가 PR/MR head를 가리키는 서버측 ref를 노출한다(예: GitHub의 `refs/pull/<n>/head`). 정확한 경로는 프로젝트가 쓰는 포지마다 다를 수 있으므로 확실하지 않으면 지어내지 말고, 원격 ref 목록을 직접 조회해 확인한다(`git ls-remote origin | grep -i pull`류). 확인이 안 되면 기여자 브랜치를 직접 지정해 fetch하는 경로로 대체한다:
```bash
git fetch origin <contributor-branch>:refs/remotes/pr/<n>
```
서버측 PR ref가 확인되면 그것을 쓴다:
```bash
git fetch origin "refs/pull/<n>/head:refs/remotes/pr/<n>"
```
두 방식 모두 포크에서 온 기여에도 동작한다(`origin/<branch>`만으로는 포크 브랜치에 닿지 않는다).
2. **파일은 `git show refs/remotes/pr/<n>:path/to/file`로 읽는다.** 워킹트리 사본을 열지 않는다(§1) — 포크 PR에서는 다른 파일일 수 있다.
3. **포지 CLI의 diff/patch 보기는 지름길이지만 한계가 있다** — 변경 안 된 컨텍스트를 못 읽고, 컨슈머로 확장할 방법이 없다. 항상 ref도 같이 fetch한다.
4. **의도(stated intent)**: PR·MR의 제목·본문이 §7의 "의도 대비 완성도"에서 대조할 주장이다. 본문이 비어 있으면 커밋 제목들을 대신 쓴다.
5. **인용 줄 번호**: 리뷰는 fetch된 ref 기준이고, 그 줄 번호는 워킹트리와 다를 수 있다. §11의 스코프 블록에 head ref와 그 SHA를 선언해 줄 번호가 무엇 기준인지 풀리게 한다.
포지 CLI가 없거나 인증이 안 돼 있어도 오류가 아니라 "PR 메타데이터 없음"으로 취급하고, §2의 3경로(마지막 커밋 / 사용자 지정 타깃 / 저장소 전체 감사)를 제시한다[SKILL-INTERFACE-REVIEW].
---
## 11. 리포트 형식
### 스코프 블록 — 리뷰를 시작하기 전에 무엇을 봤다고 주장하는지 먼저 밝힌다
| 필드 | 값 |
|---|---|
| Target | `branch`, `working`, `staged`, `pr 482`, 또는 사용자가 입력한 범위 그대로 |
| Base ref | `origin/main` at `a1b2c3d` |
| Head ref | `refs/remotes/pr/482` at `e4f5g6h` |
| Commits | 7 커밋 + 미커밋 파일 2개(따로 표기) |
| Files in scope | 제외 적용 후 12개 |
| Excluded | `pnpm-lock.yaml`, `src/__snapshots__/` — 락파일과 스냅샷 |
| Surfaces expanded | `CheckoutPage`, `SettingsPanel`; `Button` 컨슈머 3개는 더 확장하지 않음 |
### findings 표 — [critique.md](critique.md) §3 형식에 상태 열을 더한다
finding 형식·표 컬럼(심각도·위치·무엇·왜·고침)은 새로 만들지 않는다 — critique.md §3을 그대로 쓰고, 앞에 **상태** 열 하나만 더한다.
| 상태 | 심각도 | 위치 | 무엇 | 왜 | 고침 |
|---|---|---|---|---|---|
| Regression | Blocking | `src/Dialog.tsx:42` | `aria-label="닫기"`가 이 변경에서 제거됨 | 하드 게이트 — 닫기 컨트롤이 이 변경 전에는 접근 가능한 이름을 갖고 있었고 지금은 없다 [WCAG-NRV] | `aria-label="닫기"` 복원 |
`Introduced`·`Regression`이 하나도 없으면 표를 생략하고 critique.md §13 템플릿을 그대로 쓴다: **"이 흐름에서 Blocking·Direction·Important 발견 없음. Polish 관찰 후보는 \<있으면 나열, 없으면 없음\>."**
### Pre-existing 섹션
§6에서 분리한 목록을 심각도 높은 순으로 따로 둔다. "이 변경의 책임이 아니다"라고 평이하게 적는다. 없으면 섹션 자체를 생략한다. 이 섹션은 critique.md §4의 심각도 집계에 들어가지 않는다(§12).
### 렌더가 없을 때 — 멈추지 않고 나눠서 보고한다
**우선 규칙.** diff·PR 리뷰는 렌더가 없어도 멈추지 않는다. 코드로 확정 가능한 것과 렌더가 있어야 하는 것을 나눠, 뒤쪽은 미검증으로 보고한다. 스크린샷을 요청하고 턴을 끝내는 것은 화면 전체의 시각 평가만 요청받았고 볼 것이 하나도 없을 때다. 변경분 리뷰에서는 코드로 확정되는 finding을 먼저 내고, 보고 끝에서 스크린샷이나 프리뷰 주소를 한 번 청한다.
| 구분 | 예 | 보고 |
|---|---|---|
| 코드로 확정 가능 | 토큰 대신 들어온 색·radius·간격 리터럴, 없는 토큰 참조 / `aria-label` 삭제, `button` → `div`, `alt` 누락, 제목 단계 건너뜀 / 대체 스타일 없는 `outline: none`, `prefers-reduced-motion` 분기 삭제 / 삭제된 상태 분기(빈·오류·로딩), 지워진 문구 | finding으로 낸다. 미검증이 아니다 |
| 렌더가 있어야 함 | 실제 대비(합성 뒤 색), 줄바꿈·잘림·겹침, 뷰포트별 배치와 가로 스크롤, 터치 타깃의 실측 크기, 포커스 표시가 보이는지, 모션의 체감, 시각 위계와 루브릭 점수 | 미검증으로 낸다. 무엇이 있으면 검증되는지 적는다 |
코드로 확정한 finding에는 `path:line`을 붙이고, 렌더가 필요한 항목을 코드만 보고 통과·실패로 단정하지 않는다(증거 방향성, critique §9). 이 규칙은 "코드로 확정 가능" 범위를 미검증으로 뭉개지 말라는 뜻이기도 하다.
### 미검증 · 미점검
렌더 검증을 하지 않았거나(§1, 옵트인) 확인할 수단이 없는 주장은 [critique.md](critique.md) §8의 두 상태를 그대로 구분해 쓴다 — **미검증**(시도했지만 도구·권한·환경 제약으로 확인 못 함, 통과로 추정하지 않는다)과 **미점검**(§4의 파급 범위 확장에서 의도적으로 뺀 컨슈머 등, "확인 안 함"이라고 명시). 둘 다 finding 개수·심각도 집계에 넣지 않는다.
**빈 스코프를 "문제 없음"으로 보고하지 않는다.** §2에서 이미 처리된 "리뷰할 게 없음"과, 스코프는 있지만 findings가 없는 "실행 가능한 결함 없음"을 혼동하지 않는다.
---
## 12. 판정 — critique.md에 맡긴다
**심각도 스케일·에스컬레이션 트리거·finding 상한·저비용 수정 우선순위는 이 문서가 소유하지 않는다.** 심각도·에스컬레이션·저비용 수정은 [critique.md](critique.md) §4~§5, finding에 상한을 두지 않는 원칙은 §7을 따른다. 이 문서가 하는 일은 findings에 상태(§6)를 매겨 그 심각도 체계에 넘기는 것뿐이다.
- `Pre-existing`은 critique.md §4의 심각도 집계에서 제외한다 — 레거시 결함만 있는 변경은 Blocking·Direction·Important 집계가 비게 된다.
- `Introduced`·`Regression`만 심각도 집계에 들어간다.
- critique.md가 아직 없거나 심각도 매핑을 못 찾으면, 스코프와 파일 인벤토리만 보고하고 무엇이 누락됐는지 밝힌 뒤 멈춘다 — 심각도 스케일을 이 문서에서 임의로 지어내지 않는다[SKILL-INTERFACE-REVIEW].