# Implementation Report: 00-protocol 마일스톤 6 — onboarding-questions 초안

계획서: `.claude/plans/insuniverse-docs-00-protocol-m6.plan.md` (**아카이브하지 않는다** — 마일스톤이 승인 대기로 in-progress 이고, 경로를 옮기면 receipt 2종의 `plan_path` 가 어긋난다)
구현 리뷰: `.claude/notes/insuniverse-docs-00-protocol-m6.md`

> **현재 HEAD 상태 — 질문 55건 · 게이트 검사 7층 · golden 69행(onboarding 17행) · 음성 대조 51종.**
>
> 이 리포트는 1차 제출본 위에 §2차·§3차·§4차·§santa 라운드 1~12 를 덧쌓은 **누적 문서**다.
> **아래 §Summary 부터 §Tests Written 까지의 수치는 전부 1차(28건) 시점 기록이며 HEAD 상태가
> 아니다.** 각 절의 델타가 그 뒤에 붙는다. 표에서 "1차 실측" 열은 그 시점 값이고 "최종(HEAD)"
> 열이 현재다.
>
> 이 구분을 표 안까지 내린 이유: santa 라운드 1 에서 **리뷰어가 이 리포트의 1차 수치("41건")를
> 현재 값으로 인용해 한 criterion 을 오판**했고, 라운드 4 Reviewer B 가 같은 자리를 다시
> 지적했다. 누적 서술은 그 자체로 stale 인용원이다.

## Summary

*(1차 시점 기록 — HEAD 는 55건 · 7층 · onboarding golden 17행이다. 위 상자 참조.)*

`docs/_meta/onboarding-questions.yaml` 에 온보딩 질문 28건을 실었다. 이 파일은 하류 5개 문서 PRD 전부의 종료 조건이며, 각 PRD 의 §Success Metrics 표가 이름으로 지목한다(4개는 첫 데이터 행, `60-history-deprecated` 는 마지막 행이다 — "첫 행"이라고 적었던 것을 santa 라운드 5 에서 정정했다). `verify.sh` 에 `run_onboarding_gate()` 를 신설해 **측정 가능성**을 6층으로 검사하고, golden 8행으로 질문 집합을 동결했다.

핵심은 질문 문구가 아니라 세 가지 닫힘이다:

1. **출처 결속** — 질문마다 `src: "insuniverse-docs-<owner>.prd.md §<절>"` 을 두고 게이트가 `owner` 와 같은 PRD 인지 대조한다. 파서가 키 중복을 거부하므로 "한 질문에 두 번 쓰고 다른 질문에서 빼는" 총수 대조 우회가 막힌다.
2. **vacuous 종료 차단** — owner 5종·`50-domain` service 3종 **양방향** 대조. 태그 0건인 PRD 는 "해당 태그 질문 전부 O"가 공집합에서 자동 성립하는데, 그 경로를 닫았다. 제외는 `coverage_exceptions` 에 **사유와 함께**만 가능하고 golden 이 그 목록을 동결한다.
3. **승인 강제** — 지표 #5(`미승인 항목 0`)의 판정 시점 정본을 **PRD Delivery Milestones 6행 status** 로 잡았다. `complete` 인 순간 게이트가 `approved_at` 비어 있지 않음 + 전건 `approved: true` 를 요구한다. `--check` 와 `--update` 가 같은 함수를 타므로 미승인 상태를 기준선으로 축성할 수도 없다.

## Assessment vs Reality

| Metric | Predicted (Plan) | 1차 실측 | 최종 (HEAD) |
|---|---|---|---|
| Complexity | Medium | Medium | Medium |
| 질문 수 | 28 (20~30) | 28 | **55** (상한 폐지, 하한 20) |
| golden 행수 | 60 | 60 | **69** (onboarding 17행) |
| 게이트 검사 | 6층 | 6층 | **7층** (0 경계 ~ 6 승인) |
| 음성 대조 | 8종 | 8종 전부 exit 1 | **51종** 전부 exit 1 |
| Files Changed | 5 | 5 (+ notes 1 · report 1) | 8 (+ receipt 2) |

## Tasks Completed

*(1차 시점 — 아래 표의 28건 · 8행 · 52→60행은 그때의 값이다.)*

| # | Task | Status | Notes |
|---|---|---|---|
| 1 | `onboarding-questions.yaml` 작성 | Complete | 28건 · 배분표대로 8/6/5/6/3 |
| 2 | `oq_*` 파서 + 경로 상수 | Complete | **편차 1** — `substr` 오프셋 → `sub()` 접두어 제거 |
| 3 | `run_onboarding_gate()` | Complete | **편차 2·3** — 명시적 `return 0` · 파이프 제거 |
| 4 | `validate_candidate` 배선 | Complete | |
| 5 | golden 8행 편입 | Complete | 52 → 60행 |
| 6 | 결정성·selftest 회귀 | Complete | selftest 11종 PASS · 결정성 diff 빈 출력 |
| 7 | PRD · 백로그 갱신 | Complete | 6행 `in-progress` + Plan 셀 + 개정 이력 · 백로그 DEFER 1 |

## Validation Results

*(1차 시점 — HEAD 의 게이트는 7층이고 음성 대조는 51종이다.)*

| Level | Status | Notes |
|---|---|---|
| Static Analysis | Pass | `bash -n` 구문 검사 통과. 프로젝트에 타입체커·린터 없음(패키지 매니저 0) |
| Unit Tests | Pass | `verify.sh --selftest` 11종 — 기존 회귀 0 |
| Build | N/A | 빌드 산출물 없음 |
| Integration | Pass | `--update --bootstrap` → `--check --bootstrap` PASS. 게이트 6층 + golden 대조 통과 |
| Edge Cases | Pass | **음성 대조 8종 전부 exit 1** (아래) |

### 음성 대조 결과

| # | 변조 | 겨눈 층 | 결과 |
|---|---|---|---|
| 1 | `60-history-deprecated` 태그 3건을 owner·src 모두 `20-` 으로 치환 | 검사 3 (양방향) | PASS |
| 2 | `service: admin` 2건 → `site` | 검사 4 | PASS |
| 3 | `src:` 1행 삭제 | 검사 1 (`NF != 7`) | PASS |
| 4 | Q01 에 `src:` 중복 + Q02 에서 삭제 (총수 동일) | 파서 `DUPKEY` | PASS — `DUPKEY src (id=Q01)` |
| 5 | `src:` 를 다른 PRD 이름으로 | 검사 1 (src↔owner) | PASS |
| 6 | `screen_inventory: true` 추가, `ia_consumed: false` 유지 | 검사 5 | PASS |
| 7 | PRD 6행 `in-progress → complete`, 미승인 28건 유지 | 검사 6 | PASS — `PRD 마일스톤 6 이 complete 인데 approved_at 이 null 이다` |
| 8 | 질문 문구 1건만 수정 (태그·개수·출처 불변) | golden `set_sha256` | PASS |

8종 모두 원복 후 `check PASS` 로 복귀했다. 전부 `--check` 로만 돌렸다 — `--update` 를 먼저 돌리면 변조를 정당한 갱신으로 축성한다.

### Design Grounding

N/A — 설계 트리거 미발화. `impeccable-detect --mode implement` 가 `design_signal=false` (`no-signal`). 산출물이 yaml 1 · shell 함수 4 · golden 8행이라 렌더링 표면이 0이다.

## Files Changed

| File | Action | 규모 |
|---|---|---|
| `docs/_meta/onboarding-questions.yaml` | CREATED | 질문 28 + 판정 3키 |
| `docs/_meta/verify/verify.sh` | UPDATED | +파서 3 · +게이트 1 · +golden 8행 · +상수 2 |
| `docs/_meta/verify/golden.txt` | UPDATED | 52 → 60행 |
| `.claude/prds/insuniverse-docs-00-protocol.prd.md` | UPDATED | 6행 status·Plan · footer · 개정 이력 |
| `.claude/plans/codex-findings-backlog.md` | UPDATED | DEFER 1행 (plan 게이트) |

> **이 표는 1차(28문항) 시점이다.** 최종 규모는 질문 **55** · 파서 9(`oq_norm` · `oq_rationale_ids` · `prd_verdict_ids` · `oq_downstream_refs` · `oq_rationale_block` · `prd_verdict_line` 추가) · onboarding golden **17행**(전체 52 → **69**) · 게이트 검사 **7층**(경계 검사 추가)이다. 확장 경위는 §2차 반영 · §3차 반영, 게이트 보강은 §santa 라운드 1~12 에 있다.

소유 경로 밖(하류 PRD 8종 · 우산 PRD · doc-kit · manifest · template-config) `git status` 빈 출력 확인.

## Deviations from Plan

계획서 본문은 편집하지 않았다(`plan_hash`). 3건 전부 Implement-Codex R1 흡수 결과이며 `.claude/notes/insuniverse-docs-00-protocol-m6.md` §계획서 대비 의도된 편차 에 같은 표가 있다.

| # | 계획서 | 실제 | 사유 |
|---|---|---|---|
| 1 | `substr($0, N)` 8개소 | `sub(/^접두어/, "", v)` | **계획서가 실제로 틀렸다** — `  - id: `(8자)를 10부터로, `  - target: `(12자)를 14부터로 세어 `Q01` 이 `01` 로 잘렸을 것이다. 접두어를 정규식과 손계산 숫자로 두 번 적은 것이 원인이라 오프셋 산술 자체를 없앴다 |
| 2 | (명시 없음) | 게이트 끝에 `return 0` | `cmd && { …; return 1; }` 가드가 마지막 문이면 좌변 실패가 함수 반환값을 오염시킨다 |
| 3 | `cut … \| grep -qx true` | 파일로 실체화 후 `grep -qx true 파일` | pipefail + SIGPIPE(141)가 boolean 을 뒤집을 수 있는 의존 제거 |

## Issues Encountered

- **음성 대조 도중 yaml 이 truncate 됐다.** `sed '0,|…|s||…|'` 이 잘못된 주소 문법이라 실패했는데, `> "$oq"` 리다이렉트가 이미 파일을 비운 뒤였다. golden 이 동결돼 있었으므로 재작성 후 `--check` exit 0 으로 **바이트 동일 복원**을 확인했다 — 이 사고 자체가 `set_sha256` 행의 값을 증명했다. 이후 변조는 임시파일에 만든 뒤 복사하는 형태로 바꿨다.
- 첫 배치가 2분 타임아웃에 걸렸다(검증 1회 ~15초 × 8). 배치를 쪼개고 타임아웃을 늘려 해결.

## Tests Written

프레임워크 없음 — 이 저장소의 규약이다(PRD §MVP: *"테스트 프레임워크 금지, 셸 스크립트 1개 + `diff` 로 충분"*).

| 검사 | 건수 | 덮는 것 |
|---|---|---|
| `run_onboarding_gate` 6층 | 6 | 형태·src결속 / 규모 / owner 전수 / service 전수 / ia 판정 / 승인 상태 |
| 음성 대조 (계획서 §Validation) | 8 | 위 6층 + 파서 `DUPKEY` + golden `set_sha256` |
| golden 동결 | 8행 | 개수·id 전수·태그 분포·서비스 분포·예외 목록·미승인 수·ia 판정·전체 해시 |

`verify.sh --selftest` 편입은 백로그로 미뤘다 — `VERIFY_MANIFEST` 하나로 fixture 를 물리는 구조라 두 번째 env 오버라이드가 필요하다.

## 2차 반영 (2026-08-07, 사용자 검토 후)

사용자가 초안 28건을 **좋다**고 판정하면서 *"운영 수준의 정책 질문 — 흐름에 집중한 질문, 하나의 행동으로 여러 군데에서 동시 다발적으로 일어나는 상황, 조건에 따라 분기되는 상황"* 을 요구했다. 예시로 든 것이 **슬롯 포화 시 서버 동작과 사용자 화면**이다.

이 요구가 PRD 의 `20~30개` 상한을 넘긴다 — 우산 PRD 의 열린 질문 *"20~30개로 충분한가"* 에 **부족했다**로 답하는 것이다. 상한을 올리고 13건을 더했다.

| 부류 | 질문 | 건수 |
|---|---|---|
| (a) 흐름 + 포화·실패 분기 | Q29(슬롯 포화 화면·대기열) · Q30(슬롯 상한 단위와 코드) · Q31(벤더 무응답 stall) · Q32(인증 반복 차단 지점) · Q33(정산 배치 회차 경계) | 5 |
| (b) 하나의 행동 → 동시 다발 | Q34(분석 완료 시 동시 갱신 + 부분 실패) · Q35(결제 성공 후속 5종 + 부분 실패) · Q36(인증 완료 시 site·server 동시 진행) · Q37(취소·환불 4개 상태 순서) | 4 |
| (c) 조건 분기 | Q38(권한 4층 중 어디서 먼저 401/403) · Q39(열람토큰 on/off 분기) · Q40(벤더 결과 갈림·자료없음↔장애 구별) · Q41(웹↔웹뷰 분기 + 최소버전) | 4 |

### 델타

| 항목 | 1차 | 2차 |
|---|---|---|
| 질문 수 | 28 | **41** |
| 게이트 규모 상한 | 20~30 | **20~45** |
| owner 분포 | 8·6·5·6·3 | **9·9·8·12·3** |
| `50-domain` service | server 2 · site 2 · admin 2 | **server 4 · site 5 · admin 3** |
| golden `set_sha256` | `e0c5ae95…` | `71b4318f…` |
| 음성 대조 | 8종 | **9종** (규모 하한 추가) |

golden 행수는 60 으로 동일하다 — 값 8행의 *내용*만 바뀌었다.

`50-domain` 이 6 → 12 로 늘어난 것은 규약대로다. 절차 서술의 정본이 그 PRD 단독 소유이므로(우산 제약 #1) 흐름 질문이 거기로 모인다.

### 음성 대조 9번 (신규)

게이트 검사 2(규모)는 1차 8종이 한 번도 밟지 않은 층이었고, 상한을 바꿨으므로 확인했다. 질문을 19건으로 줄여 `--check` → exit 1, 사유 `질문 수 19 가 PRD 범위(20~45) 밖이다`. 원복 후 PASS.

### `ia_consumed` 판정 — 사용자 위임 → `false` 유지

> **이 절의 수치는 2차(41건) 시점이다.** 판정(`false`)은 유지되지만 근거는 §santa 라운드 1 에서 최종 55건 전수로 다시 도출했다 — 화면 언급 3건 → **9건**. 살아 있는 근거는 그쪽이다.

사용자가 판단을 맡겼다. 41건 중 화면 **목록**을 묻는 질문이 0건이다. 화면을 언급하는 3건(Q29 *"어떤 화면을 보게 되는가"* · Q39 열람 분기 · Q41 *"어느 지점에서 막히는가"*)은 전부 **흐름의 한 단계에서 만나는 화면**을 묻는 것이라 `flow-doc@1` 의 각 단계가 이름을 적으면 답이 된다. 인벤토리를 요구하는 것은 목록형 질문뿐이고 그것이 0건이다.

비대칭이 판정을 갈랐다:

- `true` → `50-domain`(이미 18문서)이 IA 세트를 추가로 만들고, §해제 조항의 *"화면 참조는 대응 IA 문서가 실재할 때만 건다"* 가 관용에서 의무로 바뀌어 `30-admin` 5문서가 IA 를 선행조건으로 갖는다.
- `false` → 뒤집는 비용은 목록형 질문 1건 + `--update`. 게이트가 `screen_inventory: true` 를 보면 `ia_consumed: true` 를 강제한다.

화면 인벤토리(`admin.temp.md` §5)는 PRD §스크래치 승격 규약이 이미 `50-domain` 소관으로 배정했다 — 각론 본문의 재료이지 별도 문서 타입을 요구하는 사실이 아니다.

사유는 `onboarding-questions.yaml` 의 `ia_verdict` + 그 위 주석, PRD §닫힌 질문 양쪽에 있다.

### 2차에서 바뀐 파일

| File | 변경 |
|---|---|
| `docs/_meta/onboarding-questions.yaml` | Q29~Q41 13건 + `ia_verdict` 사유 보강 |
| `docs/_meta/verify/verify.sh` | 규모 상한 30 → 45 + 근거 주석 |
| `docs/_meta/verify/golden.txt` | `--update` 재생성 (행수 60 유지, 값 8행 변경) |
| `.claude/prds/insuniverse-docs-00-protocol.prd.md` | Scope 표 `20~45` · §질문 규모 신설 · 닫힌 질문 2건 · 열린 질문 1건 교체 · 개정 이력 |

`.claude/prds/insuniverse-policy-docs.prd.md`(우산)의 같은 열린 질문은 **편집하지 않았다** — 이 worktree 소관이 아니고, 우산 PRD 자신이 확정을 `00-protocol` 에 위임했으므로 `00` 의 닫힌 질문이 답이다.

## 3차 반영 (2026-08-07, 관리자 축)

사용자 요구: *"관리자쪽에 대해서만. 권한에 따른 화면분기가 많고 기능이 다양해서 정상 케이스보다 예외 케이스가 더 많이 나올 수 있다. fail 케이스에 대해 운영적으로 어떻게 해결하는가. 관리자가 행하는 행동에 대한 질문. 10개 이상."*

14건 추가(Q42~Q55). 세 부류:

| 부류 | 질문 | 건수 |
|---|---|---|
| (d) 권한 → 화면 분기의 실제 모양 | Q42(사라지나 비활성인가·URL 직접 입력) · Q43(**버튼이 사라져도 API 는 열려 있는가**) · Q44(권한 변경이 열린 세션에 언제 반영) · Q45(배분 미확보 시 판정 경로와 문서의 한계 경계) | 4 |
| (e) 실패 건의 운영 처리 | Q46(분석 실패 재처리·재시도 상한·벤더 비용·재통지) · Q47(진료 실패 목록·대량 장애 일괄 재처리) · Q48(알림톡/팩스 재발송·**중복 수신 방지**) · Q49(결제 후속 실패 건을 화면에서 고치나 개발자 개입인가) · Q50(대량 작업 부분 실패·실패분만 재실행) | 5 |
| (f) 되돌릴 수 없는 행동 · 이력 · 동시성 | Q51(확정 정산 수정·변경 이력) · Q52(회원 데이터 열람 이력·열람자·보존기간) · Q53(계정 정지 시 진행 건·해제 시 복원 여부) · Q54(두 관리자 동시 처리) · Q55(운영 중 데이터 직접 수정 범위·정산/통계 반영) | 5 |

Q43 이 이 묶음의 핵심이다 — 우산 제약이 경고한 *"메뉴 권한 = API 인가"* 오독을 **질문 층에서** 잡는다. 화면만 가려지고 API 가 응답하는 구성이면 그것이 답이어야 하고, 문서가 얼버무리면 지표 #2 가 X 로 잡는다.

### 델타

| 항목 | 2차 | 3차 |
|---|---|---|
| 질문 수 | 41 | **55** |
| 게이트 규모 검사 | 20~45 | **하한 20 · 상한 폐지** |
| owner 분포 | 9·9·8·12·3 | **10·9·10·23·3** |
| `50-domain` service | server 4 · site 5 · admin 3 | server 4 · site 5 · **admin 14** |
| golden `set_sha256` | `71b4318f…` | `b2728bbb…` |

golden 행수는 여전히 60 이다.

### 상한 폐지 — 왜 다시 올리지 않았나

한 세션에 두 번(30 → 45 → 넘김) bump 된 숫자는 규약이 아니다. 반사적으로 올리는 검사는 검사가 아니고, 질문 집합의 크기는 이 제품의 운영 표면이 정하는 값이지 책상에서 정할 값이 아니다.

잃는 것이 없다는 것이 판단의 근거다 — 의도치 않은 증가는 golden 의 `onboarding.question_count` 와 `onboarding.ids` 가 이미 잡아 명시적 `--update` 를 강제한다. 상한은 그 위에 얹힌 중복이었다.

**하한 20 은 남겼다.** 그 밑으로 떨어진 파일은 확장이 아니라 파손이며(하류 5개 PRD 의 종료 조건이 통째로 헐거워진다) 그것은 결정된 불변식이다. 음성 대조 9번을 다시 돌려 `질문 수 19 가 하한 20 미만이다` 로 확인했다.

### admin 축의 구조적 한계를 숨기지 않는다

`admin` 이 14 로 가장 무거운 축이 됐는데, **admin 은 자동화된 테스트 근거가 0개다**(server 37 · site 16 대비). 이 14건의 답은 구조적으로 근거가 약하고 상당수가 `unknown` 으로 나올 수 있다. 그것을 실패로 보지 않는다 — `50-domain` §반드시 지킬 것 이 *"근거 부재를 문서 최상단에 명시"* 를 이미 요구했고, **`unknown` 건수 자체가 admin 문서 신뢰도의 실측치**가 된다. 답을 지어내는 것이 이 프로젝트의 최우선 리스크(*"이 실패는 문서가 없는 것보다 나쁘다"*)다.

### 3차에서 바뀐 파일

| File | 변경 |
|---|---|
| `docs/_meta/onboarding-questions.yaml` | Q42~Q55 14건 |
| `docs/_meta/verify/verify.sh` | 규모 검사: 상한 폐지 + 근거 주석 |
| `docs/_meta/verify/golden.txt` | `--update` 재생성 (행수 60 유지) |
| `.claude/prds/insuniverse-docs-00-protocol.prd.md` | Scope 표 · §질문 규모 재작성 · 닫힌 질문 갱신 · 개정 이력 |

## 4차 — 승인 완료, 마일스톤 6 close (2026-08-07)

사용자가 55건을 승인했다. 전건 `approved: true` + `approved_at: 2026-08-07T07:38:42.056Z` → `--update` → PRD 마일스톤 6행 `complete`.

| golden 행 | 값 |
|---|---|
| `onboarding.question_count` | 55 |
| `onboarding.unapproved_count` | **0** ← 지표 #5 성립 |
| `onboarding.ia_consumed` | false |
| `onboarding.coverage_exceptions` | (비어 있음 — 5개 PRD · 3개 서비스 전수 커버) |
| `onboarding.set_sha256` | `04739e18…` |

**승인 강제가 이제 실제로 작동한다.** PRD 6행이 `complete` 인 상태에서 `run_onboarding_gate` 검사 6이 매 `--check` 마다 승인 완료를 재확인한다. 음성 대조 10번으로 확인했다 — 질문 1건을 `approved: false` 로 되돌리자 `approved_at 이 채워졌는데 미승인 질문이 있다 (반쪽 승인)` 로 exit 1, 원복 후 PASS.

이것이 계획서 §승인은 PRD 상태와 묶는다 가 겨눈 지점이다. Codex R1-F1 이 지적한 *"승인 누락 상태에서 milestone 이 complete 로 갱신되어도 검증은 계속 통과한다"* 는 이제 성립하지 않는다 — 양방향 모두 막힌다(승인 없이 complete 불가 · complete 상태에서 승인 철회 불가).

### 하류 PRD 통지 — 필수 수정 0건

사용자가 *"기존 PRD 중 수정돼야 하는 항목이 있는가"* 를 물었다. **없다.** 5개 문서 PRD 전부가 Success Metrics 에서 `onboarding-questions.yaml` 을 **이름으로** 참조하므로(*"중 `<tag>` 태그 질문 전부 O"*), 태그 질문이 늘면 종료 조건이 자동으로 따라온다. 개수를 PRD 본문에 박아 둔 곳이 없다.

다만 병렬 worktree 가 착수 전에 알아야 할 사실 5건을 `00-protocol` PRD §하류 PRD 통지 에 표로 기록했다:

1. **`ia_consumed = false` 확정** — `50-domain` 이 IA 문서를 만들지 않는다. flow 의 화면 참조는 영구히 선택. 두 PRD 모두 *"판정은 `00-protocol` 마일스톤 6 이 한다"* 로 위임해 뒀으므로 조치 불필요
2. **태그 부담 급증** — `50-domain` 6 → 23(admin 14). 그 PRD 의 "18문서 완주 실패" 리스크가 무거워졌다
3. **Q49 의 "개발자 개입 연락 경로"** — 현재 어느 산출물에도 자리가 없다. *"없다"* 도 답이지만 자리를 둘지 판단 필요
4. **Q43 이 "버튼이 사라져도 API 는 열려 있는가"를 화면 단위로 묻는다** — `20`(원리)과 `50`(그 화면의 실제) 경계는 이미 규정돼 있어 조치 불필요
5. **우산 PRD 열린 질문 종결** — 우산 파일은 이 worktree 가 편집하지 않으므로 소유자가 반영할 때 참조

`ownership.md`(마일스톤 7)가 이 표의 정식 거처다. M7 에서 옮긴다.

### 최종 음성 대조 10종

| # | 겨눈 층 | 결과 |
|---|---|---|
| 1~8 | 1차(28건) 기준 8종 — 양방향 owner · service · `NF` · `DUPKEY` · src↔owner · ia 정합 · complete 승인 · golden 해시 | PASS |
| 9 | 규모 하한 (19건 → exit 1) | PASS (상한 폐지 후 재실행) |
| 10 | **complete 상태에서 승인 철회** | PASS |

## santa 라운드 1 — 독립 2인 리뷰 (Opus + gpt-5.4)

두 리뷰어 모두 **NAUGHTY**. 합쳐 7건을 고쳤다. 세 부류다.

### (1) `producer | grep -q` — pipefail 이 뒤집는 boolean

가장 무거운 건이다. `set -o pipefail` 아래에서 `producer | grep -q` 는 **매치가 나면** grep 이 먼저 끝나 producer 가 SIGPIPE(141)로 죽고, pipefail 이 그 141 을 파이프라인 상태로 올린다. `cmd && { 거부 }` 가드에 쓰면 **거부해야 할 때 가드가 조용히 안 터진다.**

이 파일은 검사 5 에서 그 위험을 주석까지 달아 `cut > 파일` 로 고쳐 놓고, **검사 6 에서는 같은 형태를 그대로 썼다.** 하필 마일스톤 6 의 간판 불변식(*"complete 인데 `approved_at` 이 null"*)이 그 자리다. 게다가 이번 마일스톤이 질문 수 **상한을 폐지**해 파일이 파이프 버퍼(64KB)를 넘길 경로를 스스로 열었다 — 지금 25KB 라 실현되지 않을 뿐이다.

수정은 이 저장소가 이미 가진 규약으로 되돌리는 것이다. `normalize_manifest` 가 `NORM` 사본을 1회 만들고 `parse_repos` 가 `awk … "$NORM"` 으로 **파일을 넘긴다.** m6 코드만 그 자리를 `tr | awk` 파이프로 바꿔 썼다. `oq_norm()` + `$ONORM` 신설로 되돌렸고, 이 함수 안의 스칼라 조회는 전부 파일을 직접 읽는다.

| 위치 | 형태 | 방향 | 발견 |
|---|---|---|---|
| 검사 6 (`approved_at` null 판정) | `tr \| grep -qx && 거부` | **fail-open** | 자체 |
| 검사 6 (반쪽 승인 분기) | `if ! tr \| grep -qx` | 오분기 | 자체 |
| `oq_slice` | `tr \| awk {exit}` | 파싱 실패 위장 | 자체 |
| 검사 1 위반 목록 | `awk \| head -3` | 급사(메시지 소실) | B |
| `m6` 행 읽기 | `tr \| awk {exit}` | 급사 | 자체 |
| `--selftest` phantom-repo | `bash "$0" \| grep -q` | **fail-open** | B |

selftest 건은 특히 나쁘다 — fixture 가 결함을 재현한 **바로 그 경우**에 `ok` 를 찍는다.

### (2) 승인이 질문 집합에 결속돼 있지 않았다 (B)

`approved_at` 은 시각 하나뿐이라 *무엇을* 승인했는지 말하지 못한다. 파일 주석은 *"질문을 고치면 null 로 되돌려야 한다"* 를 규범으로 적어 두고 **자동 경로에 강제를 두지 않았다.** golden 의 `set_sha256` 이 붉어져 `--update` 를 강제하지만 그것은 "값이 바뀌었다"까지만 말한다 — complete 이후 문구·owner·`src` 를 고치고 전건 `approved: true` 를 유지한 채 `--update` 한 번이면 승인이 그대로 승계된다.

`approved_sha256` 을 신설했다. `approved_at` 이 null 이 아닌 동안 게이트가 현재 집합 해시와 대조한다. 입력은 golden 의 `onboarding.set_sha256` 과 **같은 계산**(정렬한 레코드 전문)이라 파일 전체가 아니고, 따라서 이 행 자체가 입력에 섞이는 순환이 없다.

### (3) 동결이 덮지 않던 자리 · 범위 오염

- **`coverage_exceptions` 의 `why` 가 golden 밖이었다 (B)** — `target` 만 실어서 **사유를 통째로 다른 말로 바꿔 써도 golden 이 한 행도 안 움직였다.** 제외를 정당화하는 유일한 근거가 동결 밖에 있었다. `target|why` 로 바꾸고 집합 연산 호출부는 1열만 자른다.
- **빈 목록 행이 후행 공백만 남겼다** — `emit` 의 `%-38s` 가 값 없는 행을 공백으로 채워, 공백을 터는 편집기 하나가 `--check` 를 붉힌다. `-` 로 명시했다(파일이 이미 `service`·`screen_inventory` 에 쓰는 관용이다). `ia_consumed` 추출에도 `tc_guard` 와 같은 0건 가드를 붙였다.
- **`ia_consumed` 근거가 41건 시점에서 멈춰 있었다 (양쪽 + 자체)** — 그 뒤 관리자 14건이 얹혔는데 목록형 스캔을 다시 하지 않았다. 전수로 다시 세니 화면 언급이 3건이 아니라 **9건**(Q29·Q36·Q38·Q40·Q43·Q45·Q47·Q49·Q55)이고, 41건 시점 기준으로도 그 "3건"은 틀린 수였다. **목록형은 여전히 0건이라 판정 `false` 는 바뀌지 않는다.** 가장 가까운 Q42(사이드바 메뉴 가시성)·Q07(메뉴 트리 소스)도 메뉴의 규칙과 출처를 묻지 열거를 묻지 않고, Q47 의 "목록"은 실패 **건**의 목록이지 화면의 목록이 아니다.
- **`.mcp.json` 이 범위를 오염시켰다 (양쪽)** — 머신 종속 Naver Whale 실행 경로 + **UTF-8 BOM**(엄격한 JSON 파서가 깨진다). m6 과 무관하며 계획서·노트·리포트 어디에도 없다. `git checkout HEAD --` 로 되돌렸다.

### 리뷰어 대조

| | Reviewer A (Opus) | Reviewer B (gpt-5.4) |
|---|---|---|
| 판정 | FAIL | FAIL |
| 잡은 것 | `.mcp.json` 3건 (BOM·경로·줄바꿈) | 위 6건 |
| 놓친 것 | SIGPIPE 전부 · 41건 stale · 승인 결속 · `why` 동결 | 검사 6 의 fail-open(자체 발견) · BOM |

A 는 criterion 7 을 PASS 주면서 근거로 *"41 questions"* 를 그대로 인용했고 criterion 6 을 *"No number drifts"* 로 판정했다 — 정확히 틀린 두 자리다. B 는 SIGPIPE 계열에서 selftest 와 `head -3` 을 잡았지만 **검사 6 의 fail-open** 은 놓쳤다. 두 리뷰 어느 쪽도 단독으로는 충분하지 않았다.

### 음성 대조 (라운드 1 추가 4종)

| # | 겨눈 층 | 조작 | 결과 |
|---|---|---|---|
| 11 | 승인 후 질문 문구 변조 | `Q01.q` 앞에 `MUTATED` | exit 1 — `approved_sha256 가 현재 질문 집합과 다르다` |
| 12 | `approved_sha256` 삭제 | 행 제거 | exit 1 — 같은 사유 |
| 13 | 승인 후 `src` 변조 (문구 불변) | `§MVP…` → `§Problem` | exit 1 — 같은 사유 |
| 14 | 위반 3건 초과 (`head -3` 경로) | `owner: 50-domain` 23건을 `50_domain` 으로 | exit 1 — `레코드 형태 위반 23건 (앞 3건)` · 급사 없음 |

`coverage_exceptions` 는 현재 빈 섹션이라 전체 경로로 밟을 수 없어 파서 층에서 대조했다 — `target: X` + `why: "A"` → `X|"A"`, `why` 만 `"B…"` 로 바꾸면 출력이 바뀐다(= golden 이 움직인다), `why` 없는 `target` 은 `MISSINGWHY` 로 exit 1.

`--check --bootstrap` · `--update --bootstrap` · `--selftest` 전부 PASS. golden 은 60행 유지, `onboarding.coverage_exceptions` 1행만 이동.

## santa 라운드 2 — fresh 리뷰어 2인

라운드 1 기억이 없는 새 리뷰어로 재실행. **A(Opus) PASS · B(gpt-5.4) FAIL** → NAUGHTY. B 가 4건을 더 잡았고 전부 **선언과 검사가 갈린 자리**였다.

### `questions:` 가 마지막 키라는 것이 전제로만 있었다

`oq_slice` 는 다음 최상위 키에서 멈춘다. 파서 주석은 *"questions: 는 파일의 마지막 최상위 키라 EOF 로 끝나는 것이 정상"* 이라고 **전제**를 적어 두고 그 전제를 세우는 검사를 두지 않았다. 목록 중간에 최상위 키가 하나 끼면 거기서 목록이 잘리고 **잘린 목록은 정상적으로 짧은 목록과 구별되지 않는다** — 뒤에 남은 질문도 잔여도 아무도 보지 않는다. `--check` 에서는 golden 의 `question_count` 가 잡지만 `--update` 는 그 잘린 상태를 기준선으로 축성한다.

같은 파일의 manifest 파서는 이것을 `BOUNDARY` 불변식(종료 행 존재)으로 막는다. 신규 파서만 스키마가 다르다는 이유로 뺐고, 그 자리에 **거울상 불변식**(`questions:` 뒤에 최상위 키 없음)을 두지 않았다. 검사 0 으로 신설했다. `questions:` 2회 등장도 같은 검사가 잡는다 — 두 번째가 최상위 키다.

### `approved_at` — 선언은 ISO, 검사는 null 여부뿐

파일은 이 값을 *"ISO 시각, 미승인이면 null"* 로 선언하는데 게이트는 `null` 이냐 아니냐만 봤다. 아무 문자열이나 승인 증거가 된다. 형태 검사 + 등장 1회 검사를 넣었다(후자는 `null` 과 시각이 동시에 성립해 분기가 어느 쪽이든 가는 것을 막는다 — manifest 의 `repos:` 1회 검사와 같은 형태다).

### `approved_at` 이 golden 밖이었다

`unapproved_count` 는 *"몇 건이 미승인인가"* 를 말하고 승인 **시각**은 어느 행에도 없었다. 값만 바꿔도 golden 이 한 행도 안 움직였다. `onboarding.approved_at` 행을 추가했다(golden 60 → **61**행).

### 레코드 구분자 계약을 파서로 내렸다

`q` 에 `|` 가 들어가면 필드 수가 흔들린다. 게이트 검사 1의 `NF != 7` 이 잡기는 하지만 그것은 **게이트 쪽 검사**라 `gen_report` 만 도는 경로에는 없다. `oq_records` 의 `flush()` 에서 7필드 전부를 검사하도록 내렸다 — 음성 대조 H 가 실제로 `gen_report` 경로에서 잡혔으니 빈 자리가 맞았다.

### 리뷰어 대조 (라운드 2)

| | Reviewer A (Opus) | Reviewer B (gpt-5.4) |
|---|---|---|
| 판정 | **PASS** (10/10) | **FAIL** (4개 criterion) |
| 잡은 것 | 없음 | 위 4건 + receipt 시점 불일치 |

A 는 두 라운드 모두 약했다. 라운드 1 에서는 stale 수치를 근거로 인용하며 PASS 를 줬고, 라운드 2 에서는 전건 PASS 를 줬다 — B 가 잡은 4건 중 하나도 못 봤다. **이 두 라운드에서 A 단독이었다면 전부 통과했다.** 모델 다양성이 값을 한 자리다.

B 의 나머지 지적 하나(**receipt 의 `head_sha` 가 `8d0b9cd` 로 승인 시각보다 이르다**)는 코드 결함이 아니라 순서 문제다 — 이 저장소는 santa 수렴 **후**에 receipt 를 갱신한다(m4 가 라운드 5~11 수렴 후 갱신한 것과 같다). 수렴 시점에 갱신한다.

### 음성 대조 (라운드 2 추가 5종)

| # | 겨눈 층 | 조작 | 결과 |
|---|---|---|---|
| 15 | 목록 중간 최상위 키 | Q30 앞에 `stray_key: 1` | exit 1 — `questions: 뒤에 최상위 키가 있다 … 목록이 잘린다` |
| 16 | `approved_at` 비 ISO | `"승인함"` | exit 1 — `null 도 ISO-8601 UTC 시각도 아니다` |
| 17 | `approved_at` 2회 | 행 복제 | exit 1 — `2회 등장한다 (정확히 1회여야 한다)` |
| 18 | `q` 안의 `\|` | Q01 문구에 삽입 | exit 1 — `PIPE q (id=Q01)`, **`gen_report` 경로에서 발화** |
| 19 | 승인 시각 변경 (동결 확인) | `2030-01-01T…` | exit 1 — `onboarding.approved_at` golden 행이 이동 |

`--check --bootstrap` · `--update --bootstrap` · `--selftest` 전부 PASS. golden 61행.

## santa 라운드 3 — 상한 도달, 에스컬레이션

**A(Opus) PASS · B(gpt-5.4) FAIL** → NAUGHTY. santa 는 최대 3라운드이므로 **이 시점에서 루프가 상한에 걸렸다.** 남은 지적은 2건이고 그중 1건을 고쳤다.

### 고침 — `ia_verdict` 가 게이트에도 golden 에도 없었다

`ia_consumed: false` 의 판정문은 **정확한 건수와 질문 ID 를 인용하는 규범 산문**이다. 그런데 게이트는 `ia_consumed`(bool)만 읽고 `ia_verdict`(근거)는 읽지 않았고 golden 도 동결하지 않았다 — 문장을 거짓으로 고쳐도 `--check` 가 초록이다. 이 판정문은 **실제로 한 번 굳어 승인된 집합을 안 덮은 전력**이 있다(41건 시점, 라운드 1 이 잡음). 같은 사고가 다시 나도 아무도 못 본다는 뜻이었다.

두 행을 넣었다.

| 행 | 무엇을 잡는가 |
|---|---|
| `onboarding.ia_verdict` | 판정문 자체. 한 글자만 바뀌어도 `--update` 를 강제한다 |
| `onboarding.screen_mention_ids` | 근거 **모집단**. `q` 에 "화면"이 든 질문 ID 를 기계로 뽑는다 |

기계 도출값이 판정문이 인용한 9건과 정확히 일치한다 — `Q29 Q36 Q38 Q40 Q43 Q45 Q47 Q49 Q55`. 이제 산문과 데이터가 같은 기준선에 묶인다.

**이 행은 판정이 아니라 드리프트 탐지기다.** *"목록형인가"* 는 사람이 판단하고, 이 행은 그 판단이 근거로 삼은 모집단이 움직였다는 사실만 드러낸다. `ponytail:` 부분문자열 휴리스틱이라 "화면"이라는 낱말 없이 인벤토리를 묻는 표현은 못 잡는다 — 거기까지 기계화하려면 질문에 태그가 하나 더 필요하고 그 역할은 `screen_inventory` 가 이미 한다(검사 5). 게이트에는 `ia_verdict` 0건 가드도 붙였다(`tc_guard` 가 template-config 에서 닫은 것과 같은 부류).

### 안 고침 — receipt 2종의 `head_sha` stale

B 지적이 맞다. 두 receipt 모두 `head_sha: 8d0b9cd` 인데 현재 HEAD 는 라운드 1·2 수정 커밋 뒤다.

고치지 않았다. receipt 는 `subject_hash`·`receipt_hash` 를 갖는 **게이트 산출물**이고 갱신 경로는 `/mccp:receipt-write` 다 — 손으로 JSON 을 고치면 계산 근거가 없는 해시를 지어내게 되고, 그것은 stale 보다 나쁘다(감사 흔적이 *틀렸다*에서 *거짓이다*로 바뀐다). 이 저장소 규약은 **santa 수렴 후 갱신**이며(m4 가 라운드 5~11 수렴 뒤 갱신한 것과 같다) 이번 루프는 수렴이 아니라 상한 도달로 끝났다. 갱신은 사용자 판단 뒤로 넘긴다.

### 리뷰어 대조 (라운드 3)

| | Reviewer A (Opus) | Reviewer B (gpt-5.4) |
|---|---|---|
| 판정 | **PASS** (10/10) | **FAIL** (3개 criterion) |

A 는 **3라운드 연속 무결점 판정**을 냈다. 라운드 3 에서는 *"every normative statement in the PRD and YAML has corresponding executable validation code"* 라고 단언했는데 B 가 같은 라운드에 `ia_verdict` 로 그 문장을 반증했다. 세 라운드에서 A 가 단독으로 잡은 것은 라운드 1 의 `.mcp.json` 뿐이다.

### 음성 대조 (라운드 3 추가 2종)

| # | 겨눈 층 | 조작 | 결과 |
|---|---|---|---|
| 20 | 판정문 변조 | `ia_verdict` 를 거짓 문장으로 | exit 1 — `onboarding.ia_verdict` golden 행 이동 |
| 21 | 판정문 삭제 | 행 제거 | exit 1 — `ia_verdict 를 추출하지 못했다 (0건)` |

`screen_mention_ids` 행 자체를 단독으로 밟는 대조는 만들지 못했다 — 질문 `q` 를 건드리면 **`approved_sha256` 이 먼저 걸려** exit 1 이 나고 golden 대조 층까지 가지 않는다. 승인을 갱신한 뒤라면 그 행이 움직인다. 이 순서 자체는 의도한 것이다(승인 결속이 더 앞단 불변식이다).

## santa 라운드 4 — 상한 해제 후 재개

사용자가 3라운드 상한을 올려 루프를 재개했다. **A(Opus) PASS · B(gpt-5.4) FAIL** → NAUGHTY. 지적 4건 전부 고쳤다.

라운드 3 의 마지막 수정(`ia_verdict` 동결)이 처음으로 독립 리뷰를 받았고, **그 수정 자체가 불완전했다는 것이 이 라운드의 결론이다.**

### 고침 (1) — 판정 근거는 세 곳에 있고 동결된 것은 하나였다

라운드 3 은 `ia_verdict` **한 행**을 얼렸다. 그런데 같은 판정의 근거는 세 곳에 적혀 있다.

| 자리 | 라운드 3 이후 상태 |
|---|---|
| `onboarding-questions.yaml` 의 `ia_verdict` | golden 동결 ✅ |
| 같은 파일의 주석(9건 ID 를 다시 열거하고 건별로 논증) | 아무도 안 읽음 ❌ |
| PRD §미해결·의사결정 대기의 `ia-doc@1` 판정 항목(9건 ID 를 또 열거) | 아무도 안 읽음 ❌ |

B 가 앞의 둘 중 yaml 주석을 지적했고, 검증 중 **PRD 쪽 세 번째 사본**을 추가로 찾았다. 라운드 3 의 수정은 "한 사본을 얼려 나머지 사본이 자유롭게 어긋나게 두는" 형태였다.

- `onboarding.ia_rationale_ids` — yaml **주석 전체**의 `Q##` 토큰 집합
- `onboarding.prd_ia_verdict_ids` — PRD 판정 항목 1행의 `Q##` 토큰 집합

**합집합으로 합치지 않았다.** 합치면 한쪽에서 사라진 토큰이 다른 쪽에 남아 값이 안 움직인다 — 지금 닫는 결함이 정확히 그 형태다.

주석은 판정 블록만 자르지 않고 **파일 전체의 `^#` 행**을 훑는다. 블록 경계를 또 하나의 파서 계약으로 만들면 그 경계가 틀렸을 때 조용히 0건이 되고, 0건 침묵이야말로 이 게이트가 반복해서 닫아 온 부류다. 전체를 훑으면 승인 이력 주석의 `Q29~Q41` 같은 범위 표기도 함께 잡히는데 그것도 동결 대상으로 옳다.

**동결만으로는 부족해서 게이트 검사 5b 를 함께 넣었다.** golden 은 "근거 문장이 바뀌었다"를 잡지만, 질문이 늘어 화면 언급이 새로 생기면 `--update` 가 세 행을 함께 갱신해 조용히 통과시킨다 — **41건 시점 판정문이 승인된 55건을 안 덮은 그 사고가 정확히 그 경로였다.** 검사는 `screen_mention_ids ⊆ 두 근거 집합` 한 방향이다. 근거가 화면 언급 아닌 질문(Q42·Q07 같은 "가장 가까운 것")을 함께 논하는 것은 정상이므로 역방향은 요구하지 않는다.

### 고침 (2) — 하류 PRD 참조를 아무도 안 읽었다

우산 PRD 는 *"5개 PRD 전부가 자기 Success Metrics 에서 이 파일을 이름으로 참조하므로 태그 질문이 늘면 종료 조건이 자동으로 따라온다"* 를 **필수 수정 0건의 근거**로 든다. 그런데 게이트는 `.claude/prds/` 의 **파일명**만 셌고 내용을 읽지 않았다. 참조를 지워도 `--check` 가 초록이고, 그러면 그 PRD 의 종료 조건은 이 파일과 끊긴 채 남는다.

검사 3b(같은 주제라 검사 3 에 붙였다) + golden `onboarding.downstream_refs`.

### 고침 (3) — 리포트 자체가 stale 인용원이었다

Summary·Assessment·Tasks·Validation 표가 *"질문 28 · 검사 6층 · golden 60행"* 을 **"Actual"** 로 계속 제시했다. 그 시점 HEAD 는 55 · 7층 · 66행이었다(현재는 69행).

이것이 실제로 오판을 만든 적이 있다 — **santa 라운드 1 의 Reviewer A 가 이 리포트에서 stale 수치를 현재 값으로 인용해 한 criterion 을 PASS 로 판정했다.** 누적 서술은 그 자체로 stale 인용원이며, "아래 수치는 1차 기준"이라는 한 줄 각주는 표 안에서 읽히지 않는다.

문서 머리에 **현재 HEAD 상태** 상자를 세우고, Assessment 표를 `1차 실측` · `최종(HEAD)` 두 열로 갈랐다. Tasks·Validation 표에는 시점 각주를 달았다. 1차 기록은 지우지 않았다 — 경위가 이 문서의 값이다.

### 음성 대조 (라운드 4 추가 5종)

| # | 겨눈 층 | 조작 | 결과 |
|---|---|---|---|
| 22 | yaml 주석 근거 | `^#` 행의 `Q45` 를 `Q98` 로 | exit 1 — `주석의 판정 근거가 거론하지 않는다: Q45` |
| 23 | PRD 판정 근거 | 판정 행의 `Q47` 을 `Q98` 로 | exit 1 — `PRD 의 ia-doc@1 판정이 거론하지 않는다: Q47` |
| 24 | 하류 참조 | `30-contracts-limits` 에서 파일명 참조 제거 | exit 1 — `이름으로 참조하지 않는 문서 PRD 가 있다: 30-contracts-limits` |
| 25 | 판정 항목 소실 | PRD 판정 행 삭제 | exit 1 — `정확히 1행이 아니다` |
| 26 | 판정 항목 중복 | PRD 판정 행 2회 | exit 1 — `정확히 1행이 아니다` |

25·26 이 같은 메시지인 것은 의도다. 0행과 2행은 **둘 다 "근거를 특정할 수 없다"** 이며, 0행을 조용히 빈 집합으로 넘기면 5b 가 무력화된다.

### 리뷰어 대조 (라운드 4)

| | Reviewer A (Opus) | Reviewer B (gpt-5.4) |
|---|---|---|
| 판정 | **PASS** (10/10, critical 0) | **FAIL** (criterion 3·4·8·10) |

**A 가 4라운드 연속 무결점 판정을 냈다.** 이번에는 프롬프트에 *"A 슬롯은 3라운드 연속 PASS 를 냈고 매 라운드 상대 리뷰어가 실결함을 찾았다. PASS 하는 criterion 마다 그것을 강제하는 코드 행·게이트 검사·golden 행을 지목하라"* 를 명시적으로 넣었는데도 바뀌지 않았다. A 는 criterion 3 에 *"All normative sentences have corresponding gate checks OR golden rows"* 를 다시 썼고 — **라운드 3 에서 반증당한 것과 같은 문장이다** — B 가 같은 라운드에 다시 반증했다.

B 도 sandbox 제약으로 스크립트를 실행하지 못해(`mktemp` 가 `/tmp` 를 못 만든다) 전 criterion 이 코드 정독 기반이었다고 스스로 밝혔다. 그런데도 실결함 4건을 냈다.

**4라운드 통틀어 A 단독 발견은 라운드 1 의 `.mcp.json` 1건뿐이다.** 프롬프트 강화로는 안 고쳐진다는 것이 이번 라운드의 관측이다.

## santa 라운드 5 — 라운드 4 의 수정이 지적 대상이 됐다

**A(silent-failure-hunter/Opus) PASS · B(gpt-5.4) FAIL** → NAUGHTY. B 지적 1건 + 그 검증에서 나온 1건 + A 의 suggestion 1건, 총 3건 수정.

### 고침 (1) — 검사 3b 가 규범보다 넓었다

라운드 4 가 넣은 `oq_downstream_refs` 는 하류 PRD **파일 전체**를 `grep 'onboarding-questions.yaml'` 했다. 규범은 *"자기 §Success Metrics 에서 이 파일을 이름으로 지목한다"* 다.

| PRD | Success Metrics 밖의 별개 언급 |
|---|---|
| `20-foundation-access` | §Users(29행) |
| `50-domain` | §반드시 지킬 것(82행) |

이 둘은 **지표 행을 지워도 초록이었다.** 규범은 "종료 조건이 이 파일에 걸려 있다"이지 "어딘가 이름이 나온다"가 아니다.

`## Success Metrics` 절만 잘라 그 안에서 대조한다. **절 표제는 정확히 1회여야 한다** — 0회면 슬라이스가 빈 채로 나와 "참조 없음"과 구별되지 않고, 2회면 어느 절을 본 것인지 말할 수 없다. 둘 다 조용한 탈락으로 떨어지며 호출부 메시지가 두 사유를 함께 적는다.

**닫으려던 부류를 닫는 코드에서 다시 만들었다.** 검사를 쓸 때 규범 문장의 한정어를 구현으로 옮겼는지 보는 것이 그 자리의 마지막 단계다.

### 고침 (2) — 그 규범 문장은 사실이 아니었다

(1)을 검증하다 나왔다. 문구는 *"하류 5개 PRD 전부가 자기 Success Metrics **첫 행**에서 이 파일을 이름으로 지목한다"* 였다.

| PRD | onboarding 행 위치 |
|---|---|
| 20 · 30 · 40 · 50 | 첫 데이터 행 |
| **60-history-deprecated** | **7행 중 마지막 행** |

m6 1차부터 다섯 라운드 동안 아무도 대조하지 않았다. **문장이 있고, 검사가 없고, 사실도 아니다** — 반복 부류의 가장 순수한 형태이며 두 리뷰어 모두 놓쳤다. yaml 주석과 이 리포트 §Summary 를 정정했다.

순서가 교훈이다: 규범을 검사로 내릴 때 **"이 문장이 참인가"를 먼저 잰다.** 검사를 먼저 쓰면 틀린 문장을 코드로 굳힌다.

### 고침 (3) — `ia_verdict` 등장 횟수 (Reviewer A suggestion)

`approved_at` 은 라운드 2 에서 `-eq 1` 검사를 얻었는데 `ia_consumed`·`ia_verdict` 는 못 얻었다. `ia_verdict` 를 두 번 적으면 게이트의 `grep -qE` 는 통과하고 `gen_report` 의 `sed -n p` 가 두 행을 잡아 **golden 값이 개행을 품은 채 축성된다.** `ia_consumed` 는 게이트 비교가 간접적으로 막지만 진단이 엉뚱한 곳을 가리킨다("선언과 실제가 다르다" — 실제 원인은 중복이다). 둘 다 `-eq 1` 을 붙였다.

**A 슬롯이 5라운드 만에 처음으로 쓸모 있는 것을 냈다** — verdict 는 또 PASS 였고 critical 은 0 이었지만 suggestion 1건이 실결함이었다.

### 음성 대조 (라운드 5 추가 5종)

| # | 겨눈 층 | 조작 | 결과 |
|---|---|---|---|
| 27 | 절 범위 | `20-` 의 Success Metrics 지표 행만 삭제, §Users 언급 유지 | exit 1 — `§Success Metrics 에서 … 참조하지 않는 문서 PRD 가 있다: 20-foundation-access` |
| 28 | 절 표제 소실 | `## Success Metrics` → `## 성공 지표` | exit 1 — 같은 메시지(표제 사유 병기) |
| 29 | 판정문 중복 | `ia_verdict` 2회 | exit 1 — `ia_verdict 가 2회 등장한다` |
| 30 | 판정 중복 | `ia_consumed` 2회 | exit 1 |

27 이 라운드 4 코드에서는 **초록이었다.** 그것이 이 라운드의 결함이다.

### 리뷰어 대조 (라운드 5)

| | Reviewer A (silent-failure-hunter/Opus) | Reviewer B (gpt-5.4) |
|---|---|---|
| 판정 | **PASS** (10/10, critical 0, suggestion 1) | **FAIL** (criterion 1·3·4·10) |

**A 슬롯을 다른 에이전트 계열로 바꿨는데도 PASS 였다.** 라운드 4 의 관측(프롬프트 강화로 안 고쳐진다)에 따라 `code-reviewer` → `silent-failure-hunter` 로 교체하고, 이 게이트의 실결함이 전부 silent failure 부류라는 사실과 구체 사례 7건을 프롬프트에 넣었다. 5라운드 연속 PASS.

A 는 criterion 3 을 *"'5 PRD files reference onboarding-questions.yaml' → 940-944행 comm -23 이 하나라도 빠지면 실패"* 로 PASS 했는데, **B 가 같은 라운드에 바로 그 검사가 규범보다 넓다는 것을 보였다.** 또 5b 를 *"comm -23 checks both directions"* 라고 적었는데 실제로는 의도적으로 한 방향이다.

값을 낸 것은 suggestion 1건뿐이다 — 그러나 그것은 실결함이었고, 다섯 라운드 만에 A 슬롯의 두 번째 단독 발견이다.

## santa 라운드 6 — 두 라운드 연속 "검사가 규범보다 넓다"

**A(Opus, 좁힌 3과제) PASS · B(gpt-5.4) FAIL** → NAUGHTY. 지적 2건 수정.

### 고침 (1) — 절까지 좁혔는데 아직 넓었다

라운드 5 는 `oq_downstream_refs` 를 파일 전체 → `## Success Metrics` **절**로 좁혔다. B 가 다음 층을 짚었다: **절 안의 산문 한 문장이나 코드펜스도 지표 행과 동등하게 통과한다.** 규범은 *"지표 표의 한 행이 이 파일을 지목한다"* 이고, 종료 조건을 싣는 것은 행이다.

```awk
f && /^```/ { fence = !fence; next }
f && !fence
```
로 코드펜스를 걷어내고 `grep -q '^|.*onboarding-questions\.yaml'` 로 **행**만 본다.

**두 라운드 연속 같은 모양이다** — 라운드 5 도 "검사가 규범보다 넓다"였고 라운드 6 도 그렇다. 좁히는 방향이 맞았는데 한 번에 규범 문장 끝까지 가지 않았다. 규범이 *"§Success Metrics **표의 한 행**"* 이라고 말하면 구현도 절·행 두 한정어를 다 옮겨야 한다.

### 고침 (2) — 같은 번호를 유지한 채 논증을 뒤집을 수 있었다

라운드 4 가 근거 사본 둘을 `Q##` **토큰 집합**으로 동결했다. B 의 지적: Q47 의 설명을 *"화면 인벤토리를 묻는다"* 로 뒤집어도 토큰이 그대로라 **네 golden 행이 한 줄도 안 움직인다.** 판정을 떠받치는 것은 번호가 아니라 논증인데 동결된 것은 번호뿐이었다.

산문 자체를 해시로 동결한다.

| 행 | 대상 |
|---|---|
| `onboarding.ia_rationale_sha256` | `ia_consumed:` 바로 위에 붙은 주석 블록 32행 |
| `onboarding.prd_ia_verdict_sha256` | PRD `ia-doc@1` 판정 항목 1행 전문 |

**ID 행을 없애고 해시만 두지 않았다.** 해시는 *"무언가 바뀌었다"* 까지만 말하는데 실제로 터진 사고는 **어느 질문이 근거에서 빠졌는가**였다(41건 판정문). golden diff 가 그 답을 그대로 보여야 한다 — 해시가 완결성을, ID 행이 가독성을 맡는다. `exclude_inputs` 가 같은 짝이다.

같은 김에 `oq_rationale_ids` 의 범위를 **주석 블록으로 통일**했다. 전에는 파일 전체 주석이었고 해시는 블록이라 둘이 다른 텍스트를 가리켰다 — *"해시는 움직였는데 ID 는 그대로"* 의 해석 부담이 생기고, 그 어긋남 자체가 이 게이트가 닫아 온 부류다. 두 범위의 ID 집합은 오늘 동일해서 golden 값은 안 움직였다.

golden 66 → **68행**(onboarding 14 → 16).

### 음성 대조 (라운드 6 추가 4종)

| # | 겨눈 층 | 조작 | 결과 |
|---|---|---|---|
| 32 | 절 안 산문 | 지표 행 삭제 + 같은 절에 산문 언급 추가 | exit 1 |
| 33 | 코드펜스 | 지표 행 삭제 + 같은 절 ``` 안에 `\| … \|` 표 행 위장 | exit 1 |
| 34 | 근거 산문 | ID 유지한 채 yaml 주석 문장 변조 | exit 1 — `ia_rationale_sha256` 이동 |
| 35 | PRD 근거 산문 | ID 유지한 채 PRD 판정 항목 문장 변조 | exit 1 — `prd_ia_verdict_sha256` 이동 |

32·33 은 라운드 5 코드에서 **초록이었다.**

### 리뷰어 대조 (라운드 6)

| | Reviewer A (Opus) | Reviewer B (gpt-5.4) |
|---|---|---|
| 판정 | **PASS** (10/10, critical 0) | **FAIL** (criterion 1·3·4·10) |

A 에게 광범위 리뷰 대신 **좁힌 3과제**만 줬다 — (1) 산문의 사실 주장을 파일로 전수 검증 (2) 라운드 5 의 절 슬라이서를 scratch 사본에서 파괴 (3) 문서가 적은 모든 수치를 파일에서 재도출.

**여전히 PASS 였고, 과제 (1) 을 스스로 틀렸다.** A 는 `60-history-deprecated` 의 지목 위치를 *"row 5 (last of 5 metrics)"* 로 보고했는데 그 표는 **7행**이다. 사실 검증을 시키자 새 오류를 만들었다. 과제 (2) 는 *"prose mention of target string — correctly found by grep"* 을 **정상 동작으로 보고**했다 — B 가 같은 것을 결함으로 짚은 바로 그 자리다. 무엇이 규범인지를 읽지 않고 코드가 하는 일을 확인했다.

**6라운드 연속 PASS, 단독 발견 2건.**

## santa 라운드 7 — Reviewer A 의 첫 FAIL

**A(Opus) FAIL · B(gpt-5.4) FAIL** → NAUGHTY. 지적 5건 수정. **일곱 라운드 만에 A 가 처음으로 실결함을 냈다.**

### 고침 (1) — `src` 에 형태 검사가 아예 없었다 (Reviewer A)

계약은 `src: "insuniverse-docs-<owner>.prd.md §<절>"` 이고 한정어가 **넷**이다 — 따옴표 · 파일명 · `§` · **절 이름**. 검사는 접두어 `"insuniverse-docs-<owner>.prd.md §` 하나만 봤다.

| 변조 | 라운드 6 결과 |
|---|---|
| `"…prd.md §"` (절 이름 삭제) | **통과** |
| `"…prd.md §Problem` (닫는 따옴표 삭제) | **통과** |

`$7`(q)에는 `^".+"$` 가 있는데 `$6`(src)에는 없었다. **바로 옆 필드에 있는 검사가 이 필드에 없었고 일곱 라운드 동안 아무도 못 봤다.**

지표 #2 는 *"이 질문을 자기 문서만 보고 답할 수 있는가"* 이고 그 판정의 시작점이 절 이름이다. 절이 비면 출처가 문서까지만 가리킨다. `$6` 형태 검사 + `§` 뒤 1바이트 초과 검사를 넣었다.

### 고침 (2~4) — 검사 3b 가 세 라운드째 좁혀지고 있다 (Reviewer B)

| 라운드 | 범위 | 뚫린 곳 |
|---|---|---|
| 4 | 파일 전체 | 서론·개정 이력의 언급 |
| 5 | `## Success Metrics` 절 | 절 안의 산문·코드펜스 |
| 6 | 절 안의 `^\|` 행, ``` 펜스 제외 | **절 뒤쪽에 붙인 두 번째 표** · **`~~~` 펜스** |
| 7 | 절의 **첫 표 하나**, ``` 와 `~~~` 둘 다 제외 | (아래 ponytail 참조) |

**여기서 멈춘다.** 이 검사가 증명하는 것은 *"지표 표에 이 파일을 대는 행이 있다"* 까지다. 그 행이 실제로 의존을 선언하는지(`| note | onboarding-questions.yaml 는 폐기됨 |` 같은 행)는 **구조로 판정할 수 없다** — 문장의 뜻은 사람이 읽는다. 거기까지 기계화하려면 지표 행에 기계 판독 키를 달아야 하고, 그것은 하류 5개 PRD 의 표 형식을 바꾸는 일이라 m6 범위 밖이다. 코드에 `ponytail:` 로 천장을 적었다.

### 고침 (5) — 구조 이상이 "참조 안 함"으로 뭉개졌다 (Reviewer B)

`oq_downstream_refs` 가 파일 부재·표제 0회/2회에서 조용히 `continue` 했다. 그러면 호출부 집합 대조가 *"§Success Metrics 에서 참조하지 않는다"* 로 보고하고 **진짜 원인이 메시지에서 사라진다.** 라운드 5 에 괄호로 병기해 뒀던 것을 자기 사유의 하드 실패로 분리했다. 호출부 둘(`gen_report` · 게이트)도 실패 전파형으로 바꿨다 — 전에는 파이프가 종료 상태를 삼켰다.

### 고침 (6) — `version` 을 아무도 안 읽었다 (Reviewer B)

yaml 이 `version: 1` 로 스키마 판을 선언하는데 게이트도 golden 도 읽지 않았다. `version: 2` 로 올려도 v1 파서가 조용히 계속 돌고, **그때 나오는 값은 결정적이면서 틀리다.**

golden 행이 아니라 **리터럴 고정**으로 뒀다 — 동결은 *"바뀌었다"* 를 말하지만 이 자리의 규범은 *"1 이 아니면 이 파서를 쓰면 안 된다"* 다. 등장 1회 검사도 함께.

### 음성 대조 (라운드 7 추가 6종)

| # | 겨눈 층 | 조작 | 결과 |
|---|---|---|---|
| 36 | src 절 이름 | `§Problem` → `§` | exit 1 — `src 에 § 뒤 절 이름이 없다` |
| 37 | src 따옴표 | 닫는 `"` 삭제 | exit 1 — `src 형태(따옴표)` |
| 38 | 두 번째 표 | 지표 행 삭제 + **첫 표 밖**(절 뒤쪽)에 새 표를 붙여 거기서 이름을 댐 | exit 1 |
| 39 | `~~~` 펜스 | 지표 행 삭제 + `~~~` 안에 표 행 위장 | exit 1 |
| 40 | 표제 2회 | `## Success Metrics` 중복 | exit 1 — **표제 %s회** (전에는 "참조 안 함") |
| 41 | 스키마 판 | `version: 1` → `version: 2` | exit 1 — `version 이 1 이 아니다` |

### 리뷰어 대조 (라운드 7)

| | Reviewer A (Opus) | Reviewer B (gpt-5.4) |
|---|---|---|
| 판정 | **FAIL** (criterion 2·3) | **FAIL** (criterion 1·2·3·4·6·10) |

A 에게 **과제를 하나로 줄였다** — *"규범 문장의 한정어 중 코드가 구현하지 않은 것을 찾아라. 변조 입력을 못 대면 발견이 아니다."* 라운드 5·6 을 끝낸 결함 부류를 그대로 지목하고, 검증 불가능한 지적을 금지했다.

**그 라운드에 A 가 처음 FAIL 을 냈고 지적이 옳았다.** 일곱 라운드 · 네 가지 프롬프트 전략 · 두 가지 에이전트 계열 끝에 값을 낸 것은 **과제를 하나로 좁히고 반증 가능성을 요구한 것**이다. 광범위 리뷰 지시가 이 슬롯에서 실패해 온 이유를 설명한다.

## santa 라운드 8 — 규범과 관측을 갈라 적었다

**A(Opus) PASS · B(gpt-5.4) FAIL** → NAUGHTY. 지적 3건 수정.

### 고침 (1) — 표 앞에 행을 끼워 넣는 우회 + 강제하지 않을 규범

라운드 7 은 검사 3b 를 절의 **첫 표**로 좁혔다. B 가 두 가지를 짚었다.

**(a) 헤더·구분자도 표 행으로 셌다.** 표 **앞에** `| note | onboarding-questions.yaml |` 한 줄을 끼우면 그것이 첫 표의 일부가 돼 통과한다 — 그 행은 지표가 아니라 표 위의 장식이다. 이제 헤더(1행)와 구분자(2행)를 빼고 **데이터 행**에서만 찾는다. 구분자가 2번째가 아니거나 데이터 행이 0건이면 표가 아니므로 자기 사유로 실패한다.

**(b) yaml 주석이 행 위치를 규범처럼 적고 있었다** — *"4개는 첫 데이터 행이고 60-history-deprecated 는 마지막 행이다"*. 라운드 5 가 거짓 문장(*"첫 행에서"*)을 고치면서 관측을 그 자리에 그대로 남겼는데, 규범 문단 안에 있으면 규범으로 읽힌다.

**행 위치는 강제하지 않는다.** 표를 정당하게 재배열해도 종료 조건은 그대로다. 규범은 *"데이터 행 하나가 이 파일을 이름으로 지목한다"* 이고, 위치는 그 시점 관측일 뿐이다. 주석을 **규범 / 관측** 두 문단으로 갈라 적었다.

이것이 이 루프에서 처음으로 **"검사를 더 좁히지 않고 규범을 정확히 적어서" 닫은 지적**이다. 다섯 라운드 동안 검사만 좁혀 왔는데, 규범이 실제보다 넓게 적혀 있으면 검사는 영원히 못 따라간다.

### 고침 (2) — 파일 머리의 파서 계약 블록이 동결 밖이었다

라운드 6 이 얼린 것은 `ia_consumed` **바로 위** 주석 블록이다. 파일 머리의 계약 블록 — *"형태가 파서 계약이다"* · *"키 중복·미지 키·`- id:` 앞의 필드는 전부 파싱 실패다"* · 하류 참조 규범 — 은 동결 밖에 남아 **거짓으로 고쳐도 초록이었다.**

`onboarding.comments_sha256` — 이 파일 **모든** 주석의 해시. 판정 근거 해시를 이것으로 대체하지 않았다: 이 행은 *"이 파일의 규범 산문이 바뀌었다"* 만 말하고, ia 근거는 PRD 에 사본이 있어 **어느 쪽이 움직였는지**를 따로 말해야 한다.

### 고침 (3) — 리포트가 자기 자신과 모순됐다

머리 상자는 *"golden 68행 / onboarding 16행"* 인데 19행과 §마일스톤 6 종료 는 *"14행 / 전체 66행"* 이었다. 라운드 6 에서 상자와 표만 갱신하고 본문 두 곳을 놓쳤다.

**라운드 4 가 "리포트가 stale 인용원이다"로 만든 장치를 라운드 6 이 다시 어겼다.** 수치가 여섯 곳에 흩어져 있는 한 갱신은 계속 새는데, 이 리포트는 누적 문서라 한 곳으로 모을 수 없다. 라운드 8 에서는 449행의 현재형 서술도 과거형으로 바꿨다 — 그 문장은 라운드 4 시점 값을 현재처럼 적고 있었다.

### 음성 대조 (라운드 8 추가 4종)

| # | 겨눈 층 | 조작 | 결과 |
|---|---|---|---|
| 42 | 표 앞 장식 행 | 지표 행 삭제 + 헤더 **위**에 `\| note \| onboarding-questions.yaml \|` | exit 1 |
| 43 | 표 형태 | 구분자 행 삭제 | exit 1 — `표(헤더 + 구분자 + 데이터 행) 형태가 아니다` |
| 44 | 계약 블록 | 파일 머리 주석의 계약 문장 변조 | exit 1 — `comments_sha256` 이동 |
| 45 | 주석 소실 | 전 주석 삭제 | exit 1 — `주석이 0행이다` |

### 리뷰어 대조 (라운드 8)

| | Reviewer A (Opus) | Reviewer B (gpt-5.4) |
|---|---|---|
| 판정 | **PASS** (10/10, critical 0) | **FAIL** (criterion 1·2·3·4·8·10) |

A 에게 라운드 7 과 같은 형태로 한 단계 깊은 과제를 줬다 — 레코드 7필드 계약을 필드별로 감사하고, 변조를 못 대면 발견이 아니다. **PASS 로 돌아왔다.** 라운드 7 의 성공이 과제 형태 때문인지 그 라운드에 마침 잡기 쉬운 결함(`src`)이 있었기 때문인지는 이 한 번으로 갈리지 않는다.

A 는 criterion 8 을 PASS 하면서 리포트 수치를 인용했는데, **B 가 같은 라운드에 그 리포트가 자기 자신과 모순된다는 것을 보였다.** 라운드 1 과 같은 실패다.

## santa 라운드 9 — 파일 이름만 보고 "누구 몫인가"를 안 봤다

**A(Opus) PASS · B(gpt-5.4) FAIL** → NAUGHTY. 지적 3건 수정.

### 고침 (1) — 지표 행이 **자기** 몫을 거는지 아무도 안 봤다

여섯 라운드에 걸쳐 검사 3b 를 좁혀 왔는데(파일 → 절 → 표 행 → 첫 표 → 데이터 행) **전부 "이름이 나오는가" 축이었다.** B 가 다른 축을 짚었다: `20-foundation-access` 의 지표 행을 *"`onboarding-questions.yaml` 중 `50-domain` 태그 질문 전부 O"* 로 바꿔도 통과한다. 그 PRD 는 자기 질문에 종료 조건을 걸지 않은 것인데 게이트는 초록이다.

**좁히기는 범위 축에서만 일어났고 의미 축은 손대지 않았다.** 규범은 *"자기 태그 질문을 종료 조건으로 건다"* 인데 검사는 *"파일 이름이 나온다"* 였다.

판정은 **"다른 문서 PRD 의 태그를 가리키지 않는다"** 로 뒀다. 자기 태그 **명시**를 요구하지 않는 이유는 데이터가 그렇지 않기 때문이다 — 실측:

| PRD | 지표 행에 자기 태그 리터럴 |
|---|---|
| 20 · 30 · 40 · 60 | 있음 |
| **50-domain** | **없음** — *"해당 서비스 태그 질문"* |

50-domain 은 서비스 축으로 판정되므로 그 표현이 옳다. 하류 PRD 의 표 문구를 이 게이트 때문에 바꾸는 것은 **소유 경계를 넘는 일**이라 하지 않았다. 태그를 하나도 안 적은 행이 통과하는 것은 `ponytail:` 로 천장에 적었다.

### 고침 (2) — 구분자 정규식이 하이픈 0개를 통과시켰다

라운드 8 이 넣은 `^\|[[:space:]:|-]+\|[[:space:]]*$` 는 `|     |     |     |` 를 구분자로 받는다. 칸마다 하이픈 하나 이상을 요구하도록 고쳤다 — `^\|([[:space:]]*:?-+:?[[:space:]]*\|)+[[:space:]]*$`.

**라운드 8 에서 새로 쓴 정규식이 라운드 9 에 결함으로 돌아왔다.** 문자 클래스 `[…-]` 는 "이 문자들로 이루어진다"를 말하지 "이 문자가 있다"를 말하지 않는다.

### 고침 (3) — 동결한 주석이 전체가 아니었다

라운드 8 의 `comments_sha256` 은 `^#` 만 잡았다. yaml 주석 **121행 중 41행이 들여쓴 주석**(`  # ── 20-foundation-access (8) — 산출 문서 8종과 1:1 ──` 같은 배분 근거)이라 동결 밖이었다.

**라운드 8 은 "블록 단위로 얼리면 안 얼린 블록이 남는다"를 이유로 전체 주석을 얼렸는데, 그 "전체"가 전체가 아니었다.** `^[[:space:]]*#` 로 고쳤다 — 파서가 주석을 건너뛸 때 쓰는 것과 같은 패턴이다. 처음부터 그것을 썼어야 했다.

### 음성 대조 (라운드 9 추가 3종)

**이 라운드부터 흡수한 지적을 매 건 반사실로 검증한다** — 같은 조작을 **수정 전 커밋(`a814024`)의 `verify.sh`·`golden.txt`·yaml** 에 걸어 실제로 놓치는지 실행으로 확인한 뒤, 수정 후로 잡히는지 본다. 주장으로 남기면 그 주장이 다음 라운드의 지적거리가 된다.

| # | 겨눈 층 | 조작 | 수정 전 | 수정 후 |
|---|---|---|---|---|
| 46 | 소유 축 | `20-` 의 지표 행을 `50-domain` 태그를 가리키게 | **exit 0 (놓침)** | exit 1 — `다른 문서 PRD 의 태그를 가리킨다` |
| 47 | 가짜 구분자 | 구분자 행을 `\|   \|   \|   \|   \|   \|` 로 | **exit 0 (놓침)** | exit 1 — `표 형태가 아니다` |
| 48 | 들여쓴 주석 | `questions:` 아래 그룹 주석 변조 | **exit 0 (놓침)** | exit 1 — `comments_sha256` 이동 |

### 리뷰어 대조 (라운드 9)

| | Reviewer A (Opus) | Reviewer B (gpt-5.4) |
|---|---|---|
| 판정 | **PASS** (critical 0) | **FAIL** (criterion 1·2·3·4·6·10) |

A 에게 코드 감사가 아니라 *"HEAD 에서 거짓인 문장을 찾아라"* 하나만 줬다 — 이 루프에서 수확이 가장 컸던 부류이고 전부 추론이 아니라 대조로 나왔던 것이다. **결과는 PASS 였고, 게다가 지정한 출력 형식(10 criteria)을 지키지 않았으며, 자기 보고서에 `head_sha: aaa46a2` 를 적었다 — 그 시점 HEAD 는 `a814024` 다.**

**거짓 문장을 찾는 과제에서 자기 보고서에 거짓을 넣었다.** 라운드 7 의 성공(과제 1개 + 반증 의무)은 재현되지 않았다.

## santa 라운드 10 — 라운드 9 가 천장이라 적은 자리가 천장이 아니었다

**A(Opus) PASS · B(gpt-5.4) FAIL** → NAUGHTY. 지적 3건 수정.

### 고침 (1) — 자기 태그 명시를 요구할 수 있었다

라운드 9 는 의미 축을 열면서 *"다른 문서 PRD 의 태그를 가리키지 않는다"* 까지만 넣고, 자기 태그 **명시**는 `ponytail:` 천장으로 적었다 — 근거는 `50-domain` 지표 행에 태그 리터럴이 없다는 것이었다.

B 가 그 천장을 밀었다: 무태그 일반 문구(`onboarding-questions.yaml 질문 전부 O`)도 통과하며, 그러면 **그 지표는 어느 질문 집합에 걸려 있는지를 말하지 않는다.**

면제 대상을 **데이터에서 유도**하면 나머지 넷에는 요구할 수 있다 — `service` 필드를 가진 질문의 owner 가 서비스 축 owner 이고, 그것은 게이트 검사 4 가 이미 세우는 사실이다. 코드에 `50-domain` 을 리터럴로 적지 않는다: 그 리터럴 자체가 검사 없는 규범이 된다. 2종 이상이면 실패한다.

**천장을 적을 때는 "지금 유도할 수 없다"인지 "유도 방법을 안 찾았다"인지를 갈라야 한다.** 라운드 9 의 천장은 후자였다.

### 고침 (2) — 부분일치가 틀린 거부를 만들었다

라운드 9 의 `grep -qF -- "$other"` 는 `docs/50-domain-x/notes.md` 같은 문자열에도 걸린다. fail-closed 지만 **틀린 거부**다. 경계 매칭 `(^|[^0-9a-z-])$other([^0-9a-z-]|$)` 으로 바꿨다.

### 고침 (3) — 엄격성이 계약에 안 적혀 있었다

`src`·`q` 의 `^".+"$` 는 닫는 따옴표 뒤 공백을 거부한다 — 정상 YAML 이지만 거부이며, 그것이 **의도**다. 공백을 잘라 받으면 *"무엇이 값인가"* 가 파서 구현에 달리고 golden 해시가 구현 세부를 동결한다.

B 는 이것을 결함으로 올렸다. 결함이 아니라 **적히지 않은 의도**였다. 계약 주석에 못 박았다 — 적히지 않은 엄격성은 적히지 않은 천장과 같은 부류로, 매 라운드 지적거리가 된다.

### 음성 대조 (라운드 10 추가 3종) — 전부 반사실 검증

| # | 겨눈 층 | 조작 | 수정 전 | 수정 후 |
|---|---|---|---|---|
| 49 | 자기 몫 | 지표 행을 무태그 일반 문구로 | **exit 0 (놓침)** | exit 1 — `자기 태그를 적지 않는다` |
| 50 | 부분일치 오탐 | 행에 `docs/50-domain-x/notes.md` 추가 | **exit 1 (틀린 거부)** | exit 0 (정상 통과) |
| 51 | 진짜 외래 태그 | 행에 `` `50-domain` `` 추가 | — | exit 1 — `다른 문서 PRD 의 태그를 가리킨다` |

50 은 **역방향 반사실**이다 — 수정 전이 틀리게 거부했고 수정 후가 정상 통과한다. 51 이 그 완화로 진짜 위반까지 새지 않았음을 받친다.

구분자 정규식 회귀도 확인했다: 정당한 6종(`|---|---|` · `| --- | --- |` · `|:---|---:|` · `|:-:|` · `|-|-|` · `|---|`) 전부 통과, 가짜 2종(`|   |   |` · `| a | b |`) 거부.

### 리뷰어 대조 (라운드 10)

| | Reviewer A (Opus) | Reviewer B (gpt-5.4) |
|---|---|---|
| 판정 | **PASS** (10/10, critical 0) | **FAIL** (criterion 1·3·10) |

A 에게 라운드 9 의 미검토 코드 3개소를 지목하고 각각 파괴하라고 지시했다. A 는 *"trailing whitespace handled correctly"* 와 *"grep -qF … preventing ReDoS"* 를 PASS 근거로 적었는데, **B 가 같은 라운드에 그 둘을 각각 결함으로 짚었다**(공백 거부 · 부분일치). A 가 옳게 검증한 것도 하나 있다 — 중첩 `while read` 가 별도 서술자를 열어 정상 동작하고 `foreign` 이 PRD 마다 초기화된다는 것. 그것은 실제로 맞다.

## santa 라운드 11 — 양쪽 실질 0건

**A(Opus) PASS · B(gpt-5.4) FAIL** → 형식상 NAUGHTY. **코드 변경 없음** — B 의 critical 3건이 전부 사실이 아니다.

### 기각 — `src` 형태 검사 부재 주장

B: *"`oq_records()` 가 `src` 형태를 검사하지 않으며 HEAD 의 yaml 에 이미 닫는 따옴표 없는 `src` 가 있다(108·123·133·138·143·155행 등)."* criterion 1·3·6·8·10 이 전부 이 주장에 얹혀 있다.

| 확인 | 결과 |
|---|---|
| `src` 행 총수 | 55 |
| `^    src: ".+"$` 정본 만족 | **55 / 55** |
| 지목된 108행 | `src: "insuniverse-docs-20-foundation-access.prd.md §MVP — 산출 문서 8종"` — 닫는 따옴표 있음 |
| 게이트의 `src` 형태 검사 | `verify.sh:1032` `$6 !~ /^".+"$/` (라운드 7 에 넣었다) |
| 반사실: 108행 닫는 따옴표 삭제 | **exit 1** — `src 형태(따옴표): Q01\|…` |
| 같은 변조가 golden 에 닿는가 | `set_sha256` 이 움직인다 — 게이트 검사 **밖에서도** 잡힌다 |

세 지적이 같은 오독에서 파생됐다 — 긴 다국어 행의 **잘린 출력을 "따옴표 없음"으로 읽었다.**

기각 서술을 라운드 12 에서 한 번 좁혔다. 초판은 *"게이트가 잡는다"* 로만 적었는데, 형태 검사는 `run_onboarding_gate` 안에 있고 `gen_report` 는 `oq_records` 를 직접 부른다. **깨진 `src` 는 게이트를 타지 않는 경로에서도 `set_sha256` 을 움직여 golden 이 잡는다** — 라운드 12 Reviewer B 가 짚은 정밀도이고, 기각의 결론은 바뀌지 않는다. 그 위에 얹힌 *"리포트가 거짓 반사실을 주장한다"*(criterion 8·10)도 함께 무너진다. 라운드 7 의 대조 37번은 재실행으로 재확인했다.

**이 기각 판정은 라운드 12 리뷰어가 직접 평가한다** — 기각을 저자 판단으로만 남기면 그것이 검사 없는 규범이 된다.

### 관측 — 리뷰어의 도구 한계

B 는 열한 라운드 내내 스크립트를 실행하지 못했다(sandbox 가 `mktemp -d` 차단). 그 제약 아래서도 매 라운드 실결함을 냈지만, 이번에는 **정독으로 판정할 수 없는 것(값이 파일에 실제로 어떻게 있는가)을 정독으로 판정**했다.

### 리뷰어 대조 (라운드 11)

| | Reviewer A (Opus) | Reviewer B (gpt-5.4) |
|---|---|---|
| 판정 | **PASS** (10/10, critical 0) | **FAIL** (critical 3 — **전부 기각**) |

A 에게 `ponytail:` 천장 주석 전수 감사를 줬다(라운드 10 의 교훈: 천장 주장도 검사 없는 규범이다). A 는 두 천장을 모두 *"correctly stated"* 로 판정했고 반증을 대지 못했다. 수치 인용은 이번에도 부정확했다(*"Golden file contains 29 rows"* — 실제 payload 66행).

## santa 라운드 12 — 동시 PASS. 종료

**A(Opus) PASS · B(gpt-5.4) PASS** → **NICE.** 열두 라운드 만의 첫 동시 통과이며 코드 변경 0건이다.

두 리뷰어에게 같은 두 과제를 줬다 — (1) 라운드 11 의 **기각 판정을 직접 평가**하고 (2) `run_onboarding_gate` 의 검사와 `gen_report` 의 golden 행이 **다른 함수**라는 사실에서, 게이트를 안 타고 golden 에 값이 닿는 실행 경로가 있는지 호출 그래프로 답하라.

### 기각 판정 — 양쪽이 독립적으로 지지

둘 다 파일에서 직접 세었다: `src` 55행 전수가 닫는 따옴표를 갖고, 108행도 그러하며, 형태 검사는 `verify.sh:1032` 에 있다. B 는 *"the author's rejection is substantively correct"*, A 는 *"CORRECT — B's premise is false"*.

**기각을 저자 판단으로만 남기지 않은 것이 이 라운드의 값이다.** 그리고 둘 다 기각의 **서술이 좁다**는 같은 정밀도를 짚었다(위 §라운드 11 에 반영).

### 잔여 논점 — 설계이지 결함이 아니다

두 리뷰어가 독립적으로 같은 구조를 찾았다: 인자 없는 **리포트 모드**는 `gen_report` 만 돌고 `validate_candidate` 를 타지 않으므로 `run_onboarding_gate` 의 형태 검사가 돌지 않는다.

| 모드 | `gen_report` | `run_onboarding_gate` |
|---|---|---|
| 인자 없음(리포트) | O | **X** |
| `--check` | O | O |
| `--update` | O | O |
| `--selftest` · `--dump-exclude-inputs` | 해당 없음 | 해당 없음 |

결함이 아닌 이유: 리포트 모드는 **정보용**이고 기준선을 축성하지 않는다. 판정 모드 둘은 전부 게이트를 타며, 형태가 깨지면 그 경로에서도 `set_sha256` 이 움직여 golden 이 잡는다. B 는 이것을 *"real only in structure, not effect"* 로 판정했다.

두 경로의 파싱 계약을 일치시키자는 제안(형태 검사를 `oq_records` 에도)은 **백로그로 이월했다** — 수렴 라운드에서 미검토 코드를 더하면 그 코드가 다음 라운드의 지적거리가 되고, 그것이 라운드 4~11 을 만든 패턴이다.

### 리뷰어 대조 (라운드 12)

| | Reviewer A (Opus) | Reviewer B (gpt-5.4) |
|---|---|---|
| 판정 | **PASS** (10/10, critical 0) | **PASS** (10/10, critical 0) |

A 의 수치 인용은 이번에도 틀렸다(*"owner counts: 20/30 (6 each), 40 (5), 50 (20), 60 (3) — sum 55"* 는 40 이고, *"golden … 29 lines total"* 은 69 다). **열두 라운드 통틀어 A 의 수치는 한 번도 신뢰할 수 없었다** — 그러나 이 라운드의 판정은 호출 그래프 추적이었고 그 부분은 정확했다.

## 종료 판정 — SANTA NICE

동시 PASS 를 종료 조건으로 쓰지 않는다(이 저장소 실적: m2 5라운드 · m3 6라운드 · m4 4라운드, 전부 동시 PASS 0회). **실재 지적의 곡선**으로 판정한다.

| R | 실재 | 성격 |
|---|---|---|
| 1 | 3+1 | 방어층 무력화(SIGPIPE fail-open) · 승인 미결속 — **구조** |
| 2 | 4 | 목록 절단 무탐지 · 승인 형태 · 동결 누락 — **구조** |
| 3 | 1 | 판정문 미동결 |
| 4 | 4 | 근거 사본 3곳 중 1곳만 동결 · 하류 참조 미검사 — **구조** |
| 5 | 3 | 검사가 규범보다 넓음 · **규범 문장이 거짓** |
| 6 | 2 | 같은 부류 1단계 · 근거 산문 미동결 |
| 7 | 5 | **`src` 형태 검사 부재**(전면) |
| 8 | 3 | 계약 블록 미동결 · 규범/관측 분리 |
| 9 | 3 | **의미 축 신규**(누구 몫인가) |
| 10 | 3 | 축 완결(천장 철회) · 정밀도 · 문서화 |
| 11 | **0** | 기각 3(전부 오독) |
| 12 | **0** | 동시 PASS |

R9 가 연 축을 R10 이 닫았고, R11·R12 가 연속 0건이다. m4 기준선(R4 에서 상수 3줄 1건 뒤 종료)보다 평평하다. **두 개의 평평한 점 + 동시 PASS 로 종료한다.**

라운드 4 부터 흡수한 지적은 **매 건 반사실로 검증**했다 — 같은 조작을 수정 전 커밋의 스크립트·golden·yaml 에 걸어 놓치는지(또는 라운드 10 의 50번처럼 틀리게 거부하는지) 실행으로 확인했다.

## 마일스톤 6 종료

`unapproved_count = 0`. 초안 제출 → 사용자 검토 2회 확장 → 전건 승인 → santa 라운드 1~5 반영까지 한 사이클로 닫혔다. PRD 6행 `complete`.

**게이트 최종 형태 — 검사 7층.** 0 경계(`questions:` 마지막 키) · 1 레코드 형태 + `src`↔`owner` 결속 · 2 규모 하한 20 · 3 owner ∪ 예외 = 문서 PRD 5종(양방향) **+ 그 5종이 이 파일을 이름으로 참조** · 4 `50-domain` service ∪ 예외 = 3종(양방향) · 5 `ia_consumed` ↔ `screen_inventory` + `ia_verdict` 존재 **+ 판정 근거 2자리가 화면 언급 질문 전수를 덮음** · 6 승인 상태(형태 · 1회 · 반쪽 승인 · `approved_sha256` 집합 결속 · PRD 6행 `complete` 연동). onboarding golden **17행**(전체 52 → **69행**)이 그 위에서 "무엇으로 통과했는지"를 동결한다.

## Next Steps

- [x] 사용자 승인 (질문 55건 · `ia_consumed` 판정)
- [x] 승인 반영 → `--update --bootstrap` → PRD 6행 `complete`
- [ ] **마일스톤 7** (`00-index` + `ownership` + `findings/` 개설) — §하류 PRD 통지 표를 `ownership.md` 로 이관하는 것이 그 마일스톤의 작업 항목이다
- [ ] `security_skipped=true` 해소 — Agent tool 이 허용된 세션에서 `security-reviewer` 재실행 (현재 `/mccp:pr` fail-closed)
- [ ] `STATE.md chain_aborted=true` · m5 santa-loop 대기 — 커밋/PR 전에 정리 필요
