# Implementation Report: 00-protocol 마일스톤 3 — verify 스크립트 + golden 수치

## Summary

`extraction-manifest.yaml` 이 동결한 값(저장소 SHA 3 · exclude 두 목록 · screen_codes 282 · menu 하드코딩 18)을 다시 만들어 대조하는 셸 스크립트 1개(`docs/_meta/verify/verify.sh`, 539행)와 그 출력의 동결본(`golden.txt`, 21행)을 만들었다. 프레임워크는 도입하지 않았다 — 판정은 `diff` 의 빈 출력과 exit code 다. 같은 스크립트가 doc-kit lint 전수 실행을 겸해, CI 가 없는 이 저장소에서 우산 PRD 제약 #7 을 강제하는 유일한 실행 지점이 된다.

## Assessment vs Reality

| Metric | Predicted (Plan) | Actual |
|---|---|---|
| Complexity | Medium | Medium |
| Files Changed | 4 (CREATE 2 · UPDATE 2) | 3 (CREATE 2 · UPDATE 1) — PRD 는 plan 작성 시점에 이미 갱신되어 있었다 |
| 신규 코드 | 셸 1 | 셸 1 (539행) + golden 21행 |
| 신규 의존 | 0 | 0 (bash · sed/awk/comm/diff · 기존 node 추출기) |

## Tasks Completed

| # | Task | Status | Notes |
|---|---|---|---|
| 1 | verify.sh 골격 + 리포트 생성 | 완료 | cwd 비의존 경로 해석 · `set -euo pipefail` · 모드 5종 |
| 2 | 저장소 SHA 대조 행 | 완료 | 구현 중 사양 위반 자체 발견 — `MISSING` 을 싣기만 하고 exit 1 을 안 했다. 수정 후 위조 경로로 검증 |
| 3 | manifest 파서 불변식 5종 | 완료 | 고유 진단 토큰 `VERIFY_INV_*` 5종 + fail-fast |
| 4 | exclude 정합 판정 | 완료 | wide/anchored/ext 3분류 · 판정 3건. 변조 시나리오 4종으로 양방향 확인 |
| 5 | 추출기 산출값 편입 + selftest 게이트 | 완료 | `jq` 미사용(node 로 JSON 필드 추출) |
| 6 | lint 전수 게이트 (fail-closed) | 완료 | 0건 = exit 1. `--bootstrap` 은 명령줄에 드러나는 한시 우회 |
| 7 | `--selftest` 파서 결함 fixture | 완료 | fixture 6종 ↔ 토큰 5종 1:1. 초안의 "불변식 2 또는 5" 모호 fixture 재설계 |
| 8 | golden.txt 동결 + `--check` 대조 | 완료 | `validate_candidate` 분해 · 고정 헤더 3행 · 원자 교체 |
| 9 | manifest `reproduce` 갱신 | 완료 | 정확히 1행 추가, CRLF 보존 |

## Validation Results

| Level | Status | Notes |
|---|---|---|
| Static Analysis | N/A | 셸 스크립트 1개. 이 저장소에 셸 린터 설정 없음 |
| Unit Tests | Pass | `--selftest` 6 fixture (프레임워크 없음, 기존 추출기 관례 미러) |
| Build | N/A | 빌드 산출물 없음 |
| Integration | Pass | 실 manifest · 실 서브모듈 3개 대상 end-to-end |
| Edge Cases | Pass | 아래 20건 |

### 실행 판정 20건 전부 통과

| 구분 | 검사 | 결과 |
|---|---|---|
| 모드 | `--check --bootstrap` | exit 0 |
| 모드 | 맨몸 `--check` (lint 0건) | exit 1 — fail-closed 확인 |
| 모드 | `--selftest` | exit 0 |
| 모드 | 알 수 없는 옵션 | exit 2 |
| 모드 | `--bootstrap` 단독 | exit 2 |
| 지표 #2 | 2회 실행 `diff` | 빈 출력 |
| 파서 | 정상 manifest | 토큰 0건 통과 |
| 파서 | 키 2회 등장 | `VERIFY_INV_KEY_UNIQUE` 단독 |
| 파서 | 종료 행 없음(EOF) | `VERIFY_INV_BOUNDARY` 단독 |
| 파서 | 항목 형태 변조 | `VERIFY_INV_ITEM_SHAPE` 단독 |
| 파서 | 목록 비움 | `VERIFY_INV_NONEMPTY` 단독 |
| 파서 | 섹션 안 형제 키 | `VERIFY_INV_NO_RESIDUAL` 단독 |
| 지표 #3 | `exclude.parity` | OK |
| 정합 | glob 에서 `dist/` 삭제 | 판정 1 MISMATCH |
| 정합 | `*/_statics/*` 삭제 (Codex F2 시나리오) | 판정 1 MISMATCH |
| 정합 | anchored `tmp/foo/*` 추가 | 판정 3 MISMATCH |
| 정합 | pathspec_only 에서 `*.svg` 삭제 | 판정 2 MISMATCH |
| golden | 페이로드 1행 변조 | exit 1 + 드리프트 메시지 |
| golden | 헤더 1행 변조 | exit 1 + 헤더 불일치 메시지(드리프트와 구분) |
| golden | 파서 깨진 상태에서 `--update` | 쓰기 거부, golden sha256 무변경 |
| golden | 삭제 후 `--update` 재생성 | exit 0, 원본과 바이트 동일 |
| Task 2 | 서브모듈 경로 위조 | `actual_head MISSING` + exit 1 |
| 외부 | 추출기 `--selftest` 2종 · `lint-docs --selftest` | 전부 exit 0 |

### Design Grounding

N/A — 디자인 트리거 미발화. `skill_available=true`(`MCCP_IMPECCABLE_SKILL=available` 정정), `design_signal=false`, `signal_files=[]`. 산출물이 셸 스크립트 1 + 텍스트 동결본 1 + YAML 1행이라 렌더링 표면이 0이다. Phase 3.6(clarify/distill/polish)·Phase 3.7(grounding verify) 둘 다 no-op.

## Files Changed

| File | Action | Lines |
|---|---|---|
| `docs/_meta/verify/verify.sh` | CREATE | +539 |
| `docs/_meta/verify/golden.txt` | CREATE | +21 |
| `docs/_meta/verify/.gitattributes` | CREATE | +11 (계획 외 — 아래 이슈 5) |
| `docs/_meta/extraction-manifest.yaml` | UPDATE | +1 / −0 |
| `.claude/plans/insuniverse-docs-00-protocol-m3.plan.md` | UPDATE | 게이트 산출물 (Codex 리뷰 섹션 · 흡수 개정) |

배타 소유 경로(`docs/_meta/verify/`) 밖 `docs/` 파일 생성 0.

## Deviations from Plan

### 1. `--update --bootstrap` — 금지에서 "가시적 허용"으로

- **WHAT**: Codex 가 예방적으로 권고해 흡수했던 "조합 exit 2 금지"를 철회하고 허용했다. 대신 맨몸 `--update` 를 lint 0건에서 실패시킨다.
- **WHY**: 금지를 구현해 돌려보니 마일스톤 3 자체가 막혔다. 마일스톤 3~6 은 `docs/` 에 타입 문서가 0건이라 `--update` 가 lint 게이트에서 반드시 실패하고, 그러면 `golden.txt` 를 만들 경로가 존재하지 않는다 — 수용 기준이 구조적 달성 불가가 된다. 실질 위험은 Codex 자신이 *"it would not literally freeze an empty-lint value because lint is outside golden"* 로 판정했다. 금지의 실제 목적("조용한 우회 차단")은 우회 플래그를 명령줄에 강제로 드러내는 것으로 그대로 유지된다.
- plan-conflict-detector: `conflict=false` (경미 편차로 흡수). plan 본문 §`--update --bootstrap` 에 정정 근거를 남겼다.

### 2. Task 7 fixture 재설계 (Codex F2 흡수분)

- **WHAT**: 초안의 "순서 뒤바꿈 → 불변식 2" fixture 를 폐기하고 "EOF 로 열린 섹션" fixture 로 교체했다.
- **WHY**: 들여쓰기 기반 종료 행 판정에서는 섹션 순서를 바꿔도 각 섹션이 여전히 제 종료 행을 가져 아무 불변식도 걸리지 않는다. fixture 가 아무것도 검증하지 못한다.

### 3. plan 이 아직 `completed/` 로 아카이브되지 않았다

- **WHAT**: Phase 5 의 plan 아카이브 단계를 수행하지 않았다.
- **WHY**: `mccp-implement-codex`·`mccp-plan-codex` receipt 가 `.claude/plans/…` 경로의 plan 해시에 결속되어 있다. 지금 옮기면 `/mccp:pr` 의 receipt 검증이 경로·해시 불일치로 막힌다. PR 병합 후에 옮긴다.

## Issues Encountered

### 해결됨

1. **manifest 가 전 행 CRLF** — 이 머신의 Cygwin `sed`/`awk` 는 CR 을 조용히 벗기지만 MSYS2·Linux 는 벗기지 않아 파싱값 끝에 `\r` 이 붙는다. `golden.txt` 내용과 sha256 이 머신마다 갈려 지표 #1(신규 클론 복원)과 충돌한다. 읽는 지점에서 `tr -d '\r'` 1회 정규화. Codex 지적이 아니라 구현 중 자체 발견.
2. **repos 파싱 필드 오프셋** — `- name:` 행과 `path:`/`head:` 행의 awk 필드 번호가 다른데 같게 썼다. 첫 실행에서 즉시 실패.
3. **BOUNDARY fixture 미발화** — 옮긴 블록에 뒤따르는 0열 주석이 같이 딸려가 그 주석이 종료 행 노릇을 했다. 키 행 + 항목 행만 옮기도록 수정.
4. **Task 2 사양 위반** — `MISSING` 을 리포트에 싣기만 하고 exit 1 을 하지 않았다. "빈 문자열로 통과시키지 않는다"는 Task 2 의 핵심이 무력화된 상태였다. 수정 후 위조 경로로 검증.
5. **`core.autocrlf=true` 가 산출물을 CRLF 로 떨어뜨린다** — 커밋 직후 git 경고로 드러났다. index 는 LF 인데 이 저장소 설정상 신규 clone 은 `w/crlf` 로 체크아웃된다(기존 `.mjs`·manifest 가 이미 그 상태). 결과는 두 가지 모두 치명적이다 — CRLF `verify.sh` 는 POSIX 에서 shebang 이 `bash\r` 로 읽혀 **실행 자체가 안 되고**, CRLF `golden.txt` 는 항상 LF 인 리포트와 **전 행 드리프트**를 낸다. 지표 #1(신규 클론 복원)이 정확히 여기서 깨진다. 배타 소유 경로 안에 `docs/_meta/verify/.gitattributes` 를 두어 두 파일만 `text eol=lf` 로 고정했다(`git ls-files --eol` 로 `attr/text eol=lf` · `w/lf` 확인). manifest 는 CRLF 로 둔다 — 읽는 지점에서 정규화하므로 손댈 이유가 없고, 건드리면 전 행 diff 가 난다.

### 미해결 — 이 마일스톤 범위 밖, 사용자 판단 필요

**`.claude/prds/insuniverse-docs-00-protocol.prd.md` 워킹트리 손상 (커밋 금지)**

이 세션 시작 전부터 `M` 상태였고 이 작업의 산물이 아니다. HEAD 대비 `+49 / −189`.

- **마일스톤 표가 2개 들어 있다** — 구 6행 표(전부 `pending`, Plan 열 없음)가 현행 7행 표와 나란히 남아 있다
- 커밋된 M1/M2 진행 기록이 구버전으로 되돌아갔다 — `@Roles` 확정값 `136` → `~~137~~` 되돌림, "서브모듈 재구성 실사" 문단 삭제, 선행조건 `doc-template-kit M1~M7 complete` 삭제, worktree 병렬 정책 되돌림
- 마일스톤 3 행 자체는 정상이다 (`in-progress` + Plan 셀)

지금 커밋하면 189행 회귀가 그대로 올라간다. 되돌림 중 의도된 것이 있는지 판별할 수 없어 손대지 않았다. 정상 경로는 `git checkout -- .claude/prds/insuniverse-docs-00-protocol.prd.md` 후 마일스톤 3 행만 다시 `in-progress` + Plan 셀로 고치는 것이지만, 다른 의도적 편집이 섞여 있다면 유실되므로 사용자 확인이 필요하다.

## Tests Written

| Test File | Tests | Coverage |
|---|---|---|
| `docs/_meta/verify/verify.sh` `--selftest` | 9 fixture | 파서 불변식 5종 양방향 (정상 통과 + 결함 검출, 기대 토큰 배타 단언). 재검 R1·R3 흡수분 3종 포함 |
| 상동 `--check` 내장 | 결정성 1 · 정합 3판정 · 추출기 selftest 2 · lint 1 | 게이트 전 경로 |

별도 테스트 파일을 만들지 않았다 — 이 저장소의 관례(`extract-*.mjs`, `lint-docs.mjs`)가 `--selftest` 를 스크립트 안에 두는 것이고, 플랜의 "프레임워크 미도입" 수용 기준과 맞는다.

## 재검 (2026-08-05, 2차 `/mccp:prp-implement` 진입)

1차 실행은 `codex_verdict=divergent` 로 끝나 escalation 이 걸려 있었다. 열린 항목은 "흡수 후 plan 이 Codex 재검토를 받지 않았다" 하나였다. 이번에는 plan 이 아니라 **산출된 `verify.sh` 자체**를 4라운드로 재검했다. 최종 R4 `approve` (findings 0) → `codex_verdict=converged`, escalation 해소.

실재 지적 3건 흡수 + 오탐 1건 반증. 상세는 plan 본문 §재검 R1~R4.

| 라운드 | 결과 | 조치 |
|---|---|---|
| R1 | needs-attention (HIGH 1 · MEDIUM 1) | 둘 다 흡수 |
| R2 | needs-attention (HIGH 1) | 오탐 — 섹션 경계 trace 로 반증, R3 에서 Codex 수용 |
| R3 | needs-attention (HIGH 1) + R2 반증 수용 | 흡수 |
| R4 | **approve, findings 0** | — |

### 코드 변경 2건

1. **`slice_section` 잔여 분류를 이분법으로** — 초안은 들여쓰기가 항목 깊이와 **정확히 같은** 행만 보고 나머지를 조용히 버렸다. `exclude_pathspec` 안의 `    unexpected: value` 가 아무 토큰도 안 띄우고 통과했다 — 불변식 5(`NO_RESIDUAL`)가 존재하는 이유 그 자체가 새는 구멍이다. 게다가 `--update` 는 이 파서만 통과하면 구 golden 을 읽지 않고 기준선을 축성하므로, 잘못 파싱된 값이 그대로 동결된다. 이제 범위 안 비공백 행은 **항목이거나 잔여**이고 제3의 분류가 없다. 항목보다 깊은 행은 호출부가 허용 형태를 명시한 목록에서만, 그 형태에 맞을 때만 통과한다 — 평평한 두 최상위 목록은 아무것도 허용하지 않고, 중첩 두 목록은 `^      why: .` 하나만 허용한다. 키 이름까지 박은 이유는 R3 지적대로 `[a-z_]+` 로 두면 같은 깊이의 임의 키가 통과해 구멍이 깊이만 옮겨 앉기 때문이다.
2. **golden 교체 원자화** — 임시파일이 `mktemp` 디렉터리에 있었다. 리눅스·컨테이너·마운트된 worktree 에서는 별도 파일시스템이라 `mv` 가 rename 이 아니라 복사+삭제로 떨어지고, 중단되면 golden 이 반쯤 쓰인 채 남는다. `--update` 는 구 golden 을 보관하지 않으므로 그 상태는 복구 불가다. `GOLDEN_TMP="$GOLDEN.new.$$"` 로 golden 옆에 만들어 같은 파일시스템 rename 이 되게 했다. EXIT trap 이 잔재를 지운다.

### security-reviewer (사용자 승인 후 실행)

critical 0건. 지적 3건 전부 흡수. 상세는 plan 본문 §Security Reviewer.

가장 값어치 있던 지적은 **문서가 틀렸다**는 것이었다. 1차 자체 판정은 `VERIFY_INV_ITEM_SHAPE` 의 `"([^"]*)"` 가 `$(`·백틱을 파싱 단계에서 거부한다고 적었는데, 그 정규식이 막는 것은 내부 큰따옴표뿐이다. 코드는 안전하지만(값이 큰따옴표 문맥에서만 쓰이고 `eval` 없음) **방어선을 잘못 지목**한 상태였고, 그대로 두면 이후 리팩터가 진짜 방어선을 무심코 걷어낸다.

나머지 둘은 코드 수정: `GOLDEN_TMP` 를 예측 가능한 `$$` 에서 `mktemp` 로(심링크 TOCTOU 제거, golden 옆 유지라 rename 원자성은 그대로), `repos:` 소스 행 형태 검사 3종 추가.

`repos:` 가드는 첫 구현이 틀려 한 번 다시 짰다 — awk 산출물을 검사했더니 `path: bad path/admin` 이 `$2` = `bad` 로 잘려 형태 검사를 멀쩡히 통과했다. 이 스크립트가 닫으려는 "조용히 틀린 값" 부류가 가드 자체에서 재현됐다. 검사를 잘리기 전 소스 행으로 옮겨 해결.

### 재실행 판정

| 검사 | 결과 |
|---|---|
| `--selftest` | 9 fixture 전부 PASS (신설 3종은 `NO_RESIDUAL` 배타 단언) |
| `--check --bootstrap` | exit 0 |
| 맨몸 `--check` | exit 1 (fail-closed 유지) |
| `golden.txt` 재생성 | 삭제 후 `--update` 로 **바이트 동일** — 실 manifest 에 오탐 0 |
| 과들여쓰기 결함 manifest 로 `--update` | exit 1 + `VERIFY_INV_NO_RESIDUAL`, golden sha256 무변경 |
| exclude 정합 변조 3종 | 전부 exit 1 (판정 1·1+3·3) — 회귀 없음 |

## Next Steps

- [ ] **PRD 워킹트리 손상 처리 판단** (커밋 전 필수)
- [ ] `/mccp:pr`
- [ ] PR 병합 후 plan 을 `.claude/PRPs/plans/completed/` 로 이동
- [ ] 마일스톤 7 종료 시 `--bootstrap` 사용 중단 — 그 시점부터 맨몸 `--check` 가 상시 게이트
