designpaca/packages/skill/references/change-review.md
Yun Chan 6805fb2be7 feat(skill): absorb external design skills, restore interview gate, add review route
- Restore the step-0 interview as a mechanical gate the skill explicitly
  depends on; add harness.md (per-harness question tools, limits,
  fallbacks) and brief-interview.md (slots, question cards, rounds).
- Add 10 reference docs absorbed from external design skills
  (accessibility, interaction-feel, elevation, color, icons, product-copy,
  component-systems, critique, change-review, print-email) and extend
  existing references.
- Add a review-only route and two hard-gate clauses (truncated content
  reachability, three-flashes limit).
- design-gate: split tap targets into WCAG 2.5.8 and 44px contract layers,
  run axe-core when available, and fix false positives found on a real
  site (decorative alt="", stacked wordmark line count, url-only pages).
- lint-skill: fail if the interview gate section or its links disappear.
- Ship agents/openai.yaml and THIRD_PARTY_NOTICES.md.
2026-09-24 13:26:03 +09:00

303 lines
25 KiB
Markdown

# change-review — diff·PR·커밋 범위 리뷰
**리뷰 경로**에서 연다. 화면 전체가 아니라 **git diff·PR·커밋 범위로 정의된 변경**을 스코프로 잡는 요청("이 PR 봐줘", "이 브랜치 확인해줘", "방금 커밋 리뷰해줘", "이거 고친 거 회귀 있는지 봐줘")이 트리거다. 화면 전체를 감사할 때는 이 문서가 아니라 [preflight.md](preflight.md)·[audit-gate.md](audit-gate.md)를 쓴다. 이 문서가 소유하는 것은 **스코프를 어떻게 잡는가**와 **finding에 어떤 상태를 매기는가** 두 가지뿐이다. 무엇이 얼마나 나쁜가(심각도·에스컬레이션·저비용 수정 사다리)는 [critique.md](critique.md) 소관이며, 이 문서는 그것을 다시 정의하지 않는다. 정확성·테스트·보안·성능은 프로젝트의 일반 코드 리뷰 소관이다 — 한 번만 짚고 넘어간다[SKILL-INTERFACE-REVIEW].
이 문서의 **필수 절차**는 리뷰를 진행하는 과정의 규칙이다. 하드 게이트·프로젝트 계약·관찰 후보라는 판정 위계는 finding 자체에만 붙인다 — 절차 규칙을 하드 게이트라 부르면 심각도 매핑(하드 게이트 = Blocking)과 섞인다.
## 이 문서를 읽는 법
| 상황 | 읽을 곳 |
|---|---|
| 리뷰 대상 자체가 뭔지부터 정해야 한다 | §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].
---
## 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·Important 발견 없음. Polish 관찰 후보는 \<있으면 나열, 없으면 없음\>."**
### Pre-existing 섹션
§6에서 분리한 목록을 심각도 높은 순으로 따로 둔다. "이 변경의 책임이 아니다"라고 평이하게 적는다. 없으면 섹션 자체를 생략한다. 이 섹션은 critique.md §4의 심각도 집계에 들어가지 않는다(§12).
### 미검증 · 미점검
렌더 검증을 하지 않았거나(§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·Important 집계가 비게 된다.
- `Introduced`·`Regression`만 심각도 집계에 들어간다.
- critique.md가 아직 없거나 심각도 매핑을 못 찾으면, 스코프와 파일 인벤토리만 보고하고 무엇이 누락됐는지 밝힌 뒤 멈춘다 — 심각도 스케일을 이 문서에서 임의로 지어내지 않는다[SKILL-INTERFACE-REVIEW].