# notes — insuniverse-docs-11-index-code-m6-r12

계획: `.claude/plans/insuniverse-docs-11-index-code-m6-r12.plan.md`

> **왜 plan 이 아니라 여기인가.** `mccp-plan-codex` receipt (`insuniverse-docs-11-index-code-m6-r12.json`)
> 의 `plan_hash` 가 계획 본문을 attest 한다. Implement 리뷰 절을 계획 본문에 주입하면 그 해시가
> 즉시 stale 이 되어 `/mccp:pr` 체인이 무너진다. m5a-fix-2 · m5b-r3 와 같은 처리.

## Codex Implementation Review

- 호출: `node C:/Users/madsc/.claude/plugins/cache/mccp/mccp/1.27.2/scripts/lib/codex-invoke.js adversarial-review` (fail-closed Bash wrapper, v0.2.2)
- 라운드 수: **2** (R1 = 게이트 위치상 Phase 3 이전 · R2 = 산출 코드 대상. `MCCP_GATE_ROUND_CAP` 기본 1이지만
  R1 이 대상 부재로 아무것도 검증하지 못했으므로 **미검증을 수렴으로 읽지 않기 위해** 재실행했다)
- Codex verdict: `needs-attention` → 게이트 어휘로 `divergent` (structured `.result.verdict`,
  free-text fallback 아님). 그대로 receipt 에 기록한다.
- 합치 결론: **지적 1건은 게이트 순서의 산물이지 구현 결정에 대한 지적이 아니다.** 래퍼의
  `adversarial-review` 는 focus 와 무관하게 **브랜치 diff** 를 대상으로 삼는데, Implement-Codex
  게이트는 명세상 Phase 3 EXECUTE **이전**에 돈다. 그래서 diff 에 구현이 없는 것이 정상이고,
  Codex 의 권고("구현 커밋을 체크아웃한 뒤 다시 돌려라")는 게이트 위치를 뒤집어야 성립한다.
  산출된 diff 에 대한 적대적 리뷰는 `/mccp:pr` 의 PR-Codex 가 실제로 수행한다.

### 2.5.2 — 이 게이트에 올린 implement-time 결정 (계획이 미리 못박지 않은 것)

1. 모듈 분할 — 판정기가 정본(후보 자격 술어 · id 규칙 · 3낱말 어휘 · `CANONICAL_PAYLOAD_FIELDS` ·
   `PRESERVE` allowlist)을 export 하고 생성기가 import 한다. 3자 대조의 **셋째 축(판정기 독립
   재열거)** 은 그 공유 export 를 경유하지 않는다.
2. 입력 파싱 — 저장소 기존 리더(`jsonl-doc.mjs` · 기존 YAML 리더)를 생성기·판정기가 공용한다.
   독립성은 **저수준 바이트 파싱**이 아니라 **분류·집계 로직**에서 성립한다.
3. CLI 표면 — 생성기 `--index-dir/--out-dir/--report/--dry-run/--selftest`, 판정기
   `--file/--provisional-file/--index-dir/--repos-root/--selftest`. `--allow-*` 없음(큐레이션
   소실 규칙은 무조건 중단이라 탈출구가 없다).
4. 후보 id 키 도출 및 충돌 처리 — 파일 내 중복 id, 두 파일 교차 id 는 전부 중단.
5. `status_undetermined_reasons[]` 의 `reference_count_absent` 도출 — 축 이름 하드코딩이 아니라
   **적재된 원 레코드에 `active_reference_count` 가 실재하는지** 를 빌드 시점에 본다.
6. selftest 하네스 — 임시 디렉터리 인라인 픽스처, 프레임워크 없음, 케이스마다 `ok`/`FAIL` 한 줄.
   변조 픽스처 열거는 `CANONICAL_PAYLOAD_FIELDS` 에서 **파생**한다.
7. 오류 형태 — `catch { ok = false }` 금지, 원인을 실은 throw + 최상위 catch + 비영 종료.
   쓰기는 temp+rename 전부-아니면-전무.

### YAGNI Triage

| Finding | Severity | Verdict | Why |
|---|---|---|---|
| F1 대상 diff 에 마일스톤 6a 구현이 없다 (`.claude/plans/…m6-r12.plan.md` 195–202 인용, confidence 1.0) | HIGH | REJECT_YAGNI | **사실 확인은 했다** — HEAD 는 계획 전용 커밋 `e0d5f33` 이고 워킹트리는 clean 이며 후보 스크립트는 어느 ref 에도 없다. 그러나 이것은 결함이 아니라 **게이트 위치의 정의**다. Implement-Codex 는 Phase 2.5 에서, 즉 Phase 3 EXECUTE 이전에 돈다(`prp-implement.md` Phase 2.5 머리말: "runs after Phase 2 PREPARE and before Phase 3 EXECUTE"). 권고를 따르려면 구현이 게이트보다 먼저 있어야 하므로 순서가 뒤집힌다. 산출 diff 에 대한 적대적 리뷰는 이 워크플로에서 **누락되는 것이 아니라** `/mccp:pr` 의 PR-Codex 로 이관돼 있다. backlog 로도 내리지 않는다 — 이월할 미결 작업이 아니라 이미 다른 게이트가 소유한 검사다 |

- Deferred to backlog: 0
- Open Questions: 없음 (ACCEPT_NOW HIGH/CRITICAL 0건 → escalate 미발동, R2 없음)
- §0 auto-CRITICAL 목록(보안 경계 · 원자적 상태 · 스키마 파손) 해당 0건
- Codex session 참조: thread `01a0123f-2a0f-7682-8e28-e5abcb2536b5`

## R2 — 산출 코드를 대상으로 한 재실행 (2026-08-18)

R1 의 권고("구현 diff 가 생긴 뒤 다시 돌려라")를 **실제로 이행**했다. 구현이 워킹트리에 존재하는
상태에서 파일 경로를 명시해 재실행했고, 이번에는 실질 지적 **6건**이 나왔다. R1 의 0건은 수렴이
아니라 대상 부재였음이 확인된다.

- Codex verdict: `needs-attention` → 게이트 어휘 `divergent`
- 요약(Codex): "현재 스냅샷은 통과하지만, 게이트는 의미적으로 틀린 조인·분류·누락된 YAML 모집단·
  거짓 gap 문면을 여전히 받아들일 수 있다. 큐레이션과 원자적 쓰기 보장도 끝까지 성립하지 않는다."
- **인용 지점 6건 전부 실코드와 대조했고 전부 사실이었다.** 셋(F1·F2·F3)은 공통 부류다 —
  **오늘 데이터에서만 우연히 참인 가정을 게이트가 강제하지 않는다.** 실측으로 오늘 값은 전부
  성립하지만(조인 356↔356 1:1 · whitelist·forbidNonWhitelisted 둘 다 true · 입력 13종 중
  `role_matrix` 를 가진 것은 policy-anchors 하나), 그 성립을 검사하는 코드가 없었다.

### YAGNI Triage (R2)

| Finding | Severity | Verdict | Why |
|---|---|---|---|
| F1 access 조인이 1:1 을 강제하지 않는다 | HIGH | ACCEPT_NOW | 앵커를 `Map` 에 그냥 덮어써서 둘 이상의 endpoint 가 같은 앵커를 가리켜도 마지막 것이 조용히 이긴다. 고아 앵커도 통과하고, 두 원 id 의 **축 키 동일성**을 아무도 안 본다. 역방향 집합 검사는 "어딘가 담겼다" 만 증명하므로 **잘못된 짝**이 전 게이트를 통과한다. 앵커 토큰이 마지막 심볼 조각뿐이라 동명 핸들러도 못 가른다. 판정기·생성기 양쪽에 중복·고아·축 키 불일치 중단을 넣고 문서 수준 검사(sources 2건 · 축 키 1종)를 더했다. 음성 픽스처 3종 |
| F2 전역 whitelist 가 꺼져 있어도 `global_whitelist_only` 를 찍는다 | HIGH | ACCEPT_NOW | validator 없는 745행을 후보로 **남기는 근거 전체**가 `meta.global_validation` 의 두 플래그(R7 F0)인데, 술어가 그 플래그를 읽지 않고 라벨만 붙였다. 상류가 뒤집힌 날 745행이 집행 지점이 아닌 채 잠정 후보로 남는다. 두 플래그가 참이 아니면 **값을 고르지 않고 중단**한다 — 그때의 분류는 오늘 근거가 없는 설계 결정이고 고르는 순간 지어낸 값이다(UI2). 독립 재구성은 같은 검사를 **자기 손으로** 구현한다(export 된 것을 부르면 셋째 축이 첫째 축이 된다). 음성 픽스처 2종 |
| F3 공용 YAML 리더가 모집단을 통째로 버릴 수 있다 | HIGH | ACCEPT_NOW | 항목 배열 키를 **파일 내용**으로 골랐다(`role_matrix` 가 배열이면 그것). 다른 입력이 실수로 `role_matrix: []` 를 얻으면 그 파일의 전 항목이 사라지고, 생성기·판정기가 같은 리더를 쓰므로 **둘 다 같은 0건에 합의**해 게이트는 초록이다. 키를 **파일명으로 결속**하고 반대 키 존재를 거부한다. 더해서 상류가 스스로 적은 `meta.entry_count` 헤더와 실제로 읽힌 수를 대조한다 — 실측 13종 전부 그 헤더를 가진다. 음성 픽스처 3종 |
| F4 `gap` 문면이 검증되지 않는다 | MEDIUM | ACCEPT_NOW | 판정기가 "비어 있지만 않으면 통과" 였다 — 5,188 을 5 로 손으로 고쳐도 통과한다. `gap` 은 결손을 기계 판독 가능하게 남기라는 UI2 의 산출물이므로 이건 그 계약의 구멍이다. 산문 파서를 만들지 않고 **facts 를 독립 재구성에서 뽑고 템플릿을 한 벌만 두고 렌더 결과를 정확 비교**한다(`gapFacts`/`renderGap` 을 판정기가 export, 생성기가 import). 템플릿은 사실을 담지 않으므로 공유해도 3자 대조가 안 무너진다. 두 파일 `gap` 동일성도 검사. 음성 픽스처 2종 |
| F5 큐레이션 보존 기전이 실제로는 작동할 수 없다 | MEDIUM | ACCEPT_NOW | **가장 날카로운 지적.** 보존 대상 도메인이 `CANONICAL_PAYLOAD_FIELDS` 였는데 그 필드들은 판정기가 입력에서 **재계산**한다 — 사람이 적은 값은 재계산값과 언제나 어긋나 그 산출물은 검증을 통과할 수 없다. 즉 "필드가 생기는 순간 유효해진다" 는 계획의 약속이 구조적으로 성립하지 않았고, selftest 는 build 까지만 보고 verify 를 안 태워서 그 사실이 가려져 있었다. 생성 필드와 **서로소인** `CURATED_FIELDS` 닫힌 목록을 신설(6a 시점 빈 목록, 모듈 적재 시 서로소·부분집합 강제)하고, **build → serialize → verify 완주** 픽스처를 더했다 |
| F6 산출물 두 개가 원자적으로 커밋되지 않는다 | MEDIUM | DEFER_TO_BACKLOG | 사실이다. 다만 `writeDocs` 는 **마일스톤 1 소유의 공용 자산**이고 다른 축 6종이 이미 쓰며, 그 파일의 주석이 이 잔여를 이미 명시한다. 처방(버전 디렉터리 + 포인터 원자 교체)은 계약 변경이라 계획 「계약 변경 규칙」이 별도 correction 마일스톤으로 보낸다. **검출은 실증했다** — 한쪽만 새 것인 쌍을 만들어 판정기에 넣으니 `meta.sibling.set_sha256` 불일치로 거부됐다. 못 잡는 잔여는 **id 집합이 같고 payload 만 다른 쌍**이며 backlog 에 그대로 적었다 |

- Deferred to backlog: 1 → `.claude/plans/codex-findings-backlog.md`
- Open Questions: 없음
- §0 auto-CRITICAL 목록(보안 경계 · 원자적 상태 · 스키마 파손) 해당 0건 — F6 은 **원자성**을 건드리지만
  공용 자산의 기존·문서화된 잔여이고 검출 경로가 실증됐으므로 신설 CRITICAL 이 아니다
- 흡수 후 회귀: 판정기 selftest **45 → 59 cases**, 생성기 **29 → 32 cases**, 기존 축 15종 회귀 0건,
  실산출 수치 전부 불변(확정 1,991 · 잠정 3,208 · status 11/5,188 · 후보 밖 2,588), 재현성 유지

> **흡수의 반사실 검증.** 다섯 건 각각에 대해 **그 수정이 없으면 실패하는** 음성 픽스처를 뒀다 —
> 앵커 중복·고아·축 키 불일치 / whitelist·forbidNonWhitelisted 각각 거짓 / 다른 입력의
> `role_matrix` 획득·헤더 계수 불일치(YAML·JSONL) / gap 수치 손편집·두 파일 gap 분기 /
> 큐레이션 목록이 생성 필드를 담음·PRESERVE 가 목록 밖·보존 산출물의 verify 완주.
> 검사를 지우면 그대로 초록이 되는 픽스처는 두지 않았다.

## R3 — 실행했으나 결과 없음 (2026-08-18)

흡수가 구멍을 닫았는지 · 새 구멍을 냈는지 · 지적 실질 곡선이 꺾였는지를 보려고 R3 을 같은 래퍼로
띄웠으나 **완료 전에 중단됐다**(killed, 출력 0바이트). 결과가 없으므로 **없다고 적는다** —
흡수 5건에 대한 독립 재리뷰는 이 세션에 존재하지 않는다.

- 이 세션이 흡수분에 대해 가진 증거는 **반사실 음성 픽스처**(그 수정이 없으면 실패하는 검사)와
  전 회귀 통과뿐이다. 그것은 "내가 의도한 구멍을 내가 막았다" 를 보이지만 "다른 구멍이 없다" 는
  보이지 못한다.
- 그래서 receipt 의 `codex_verdict` 는 **`divergent` 로 남긴다.** R2 가 `needs-attention` 이었고
  그 뒤 독립 확인이 없었으므로, 흡수했다는 사실만으로 `converged` 로 올리는 것은 receipt 가
  하지 않은 일을 했다고 적는 것이다.
- 산출 diff 에 대한 적대적 리뷰는 `/mccp:pr` 의 PR-Codex 가 소유한다. 거기서 R2 흡수분이
  실제로 검토된다.
> **래퍼 한계 기록.** `codex-invoke.js adversarial-review` 는 `--focus` 를 프롬프트에 싣지만
> 리뷰 **대상**은 항상 브랜치 diff 다. 그래서 구현 전 게이트에서는 위 F1 같은 지적이 구조적으로
> 재발한다. R1 에서 2.5.2 의 7개 결정 자체에 대한 반론은 0건이었고, 그 사실을 "수렴" 으로
> 읽지 않았다 — 대상이 비어 있었으므로 **미검증**이 정직한 상태였다. R2 가 그 판단을 사후적으로
> 확증했다: 같은 코드에 대상만 주자 HIGH 3건이 나왔다.

### Security Reviewer

> security-reviewer unavailable, skipped (auto-fallback): Agent(Task) 도구 금지 — 세션 지시.
> m5a-fix-2 · m5b-r3 와 같은 처리.

수동 확인 (사람이 읽을 수 있게 기록):

- 신뢰 경계 없음 — 이 마일스톤이 만드는 것은 운영자가 넘긴 `--index-dir` 밑의 **이미 커밋된**
  인덱스 산출물을 읽어 JSONL 두 개를 쓰는 오프라인 도구다. 네트워크·인증·비밀·SQL·명령 실행
  경로가 신설되지 않는다. 소스 저장소 스캔도 하지 않는다(계획 Summary).
- 경로 처리 — 새 경로 합성 지점은 `--index-dir` / `--out-dir` join 뿐이고 둘 다 운영자 인자다.
  사용자 입력(HTTP·폼)에서 경로가 오는 지점 없음. 앵커 검사는 기존 `checkAnchor` 재사용.
- 역직렬화 — JSON/YAML 파싱만이며 `eval` · `Function` · 동적 `import()` 를 신설하지 않는다.
- `RegExp` — 새 정규식은 전부 리터럴 상수. 파일 내용에서 정규식을 만들지 않는다(ReDoS 표면 불변).
- 새 외부 의존 0건.
- 다루는 **주제**가 접근 제어 앵커·검증 계약이지만, 그것은 읽는 데이터의 내용이고 이 도구의
  실행 표면이 아니다. 산출물은 정책 **후보 목록**이며 집행에 쓰이지 않는다.

### Design Review

> impeccable silent-skip (`reason=no-signal`): 이 마일스톤은 렌더 표면
> (`.tsx`/`.jsx`/`.vue`/`.svelte`/`.astro`/`.css`/`.scss`/`.html`/`.claude/cache/*`)을 건드리지
> 않는다 — Node `.mjs` 두 개와 JSONL·마크다운뿐. `impeccable-detect` 가 `design_signal=false`
> 를 냈고 그대로 receipt 에 `impeccable_silent_skip=true` 로 싣는다.
> 디자인 표면이 0건이므로 stage-aware routing · critique retry loop · design-grounding capture
> 는 전부 미발동이고 Phase 3.6 · 3.7 은 no-op 이다.

## 실행 기록

- 구현 보고서: `.claude/PRPs/reports/insuniverse-docs-11-index-code-m6-r12-report.md`
- selftest: `verify-policy-candidates.mjs` **64 cases PASS** · `build-policy-candidates.mjs` **32 cases PASS**.
  (R2 봉인 시점 45/29 에서 R2 흡수분 + code-review 흡수분이 더해진 값이다.)
  기존 축 15종 selftest 회귀 **0건**.
- 라이브 완주: 확정 1,991 · 잠정 3,208 · status 확정 11 / 미확정 5,188 · 후보 밖 2,588 — 판정기 exit 0.
  계획의 예측 수치가 한 건도 어긋나지 않았으나 어디에도 하드코딩하지 않았다(R8 F0).
- 재현성 2회 `diff` 양쪽 빈 출력. `set_sha256` 확정 `cba21ce179f38a48…` · 잠정 `a5787b2e67565ea6…`.
- 상류 판정기 6종 전부 exit 0.
- Acceptance 기계 검사 29항목 ALL PASS.
- `plan-conflict-detector`: `conflict: false` — 변경 파일이 계획 `Files to Change` 6종과 정확히 일치한다.
- 계획 본문은 건드리지 않았다 — `mccp-plan-codex` receipt 의 `plan_hash` 가 유효하게 유지된다.

## Code Review — 로컬 미커밋 변경 (2026-08-18)

`/mccp:code-review` Local Review Mode. CRITICAL·HIGH **0건**, 커밋 차단 없음. MEDIUM 1 · LOW 3 전부 수용해 같은 편집에서 닫았다.

| # | 등급 | 지적 | 조치 |
|---|---|---|---|
| CR-1 | MEDIUM | **`meta` 헤더 필드 집합만 안 닫혀 있었다.** 행은 `allowedFields` 로 닫아 낯선 키를 거부하는데 헤더는 아는 키만 검사해 낯선 키가 조용히 통과했다 — 실산출 헤더에 `bogus_meta_field` 를 더해도 exit 0(행 `status` 변조는 즉시 거부). 형제 판정기 6종도 같아 회귀는 아니고 저장소 공통 공백 | `META_FIELDS` 닫힌 목록 14종 + 헤더 루프. `meta.sibling` 도 2키로 닫고, `meta.inputs`·`meta.excluded_axes` 는 **차이가 있는데 아는 필드로 하나도 못 짚으면 generic error** 를 올리게 해 항목 안 낯선 필드·순서 변경까지 닫았다. selftest 5건 추가 |
| CR-2 | LOW | **앵커 둘째 토큰 채널이 죽어 있었다.** `anchorTokens(key, [key.split(".").pop()])` 가 넘기던 값은 언제나 중복(`thing.thg_name` → `thg_name` 두 번)이거나 합성 쓰레기(`thing#idx_thg` → `thing#idx_thg`)라 한 번도 발화하지 않았는데, 주석은 "컬럼은 drizzle 프로퍼티명도 대안 토큰으로 준다" 고 약속하고 있었다 | `extra` 인자와 호출부 인자를 함께 삭제하고, 실제로 통과시키는 것이 SQL 이름(drizzle 선언 행에 함께 있다)이라는 사실을 주석에 적었다. 되살릴 조건(drizzle `casing`)과 fail-closed 라는 성질도 같이 남겼다 |
| CR-3 | LOW | **backlog 신규 항목의 "못 잡는 잔여" 예시가 이미 닫혀 있었다.** "id 집합이 같고 payload 만 다른 쌍(`gap`·`status` 만 바뀐 재생성)" 은 검사 (l) 전수 재계산 + `meta.gap` 재렌더 대조가 잡고, 입력이 갈리면 `meta.inputs[].sha256` 이 잡는다. 입력·코드가 같으면 생성이 결정적이라 찢어진 쌍이 애초에 바이트 동일이다 | `codex-findings-backlog.md` 의 잔여 서술을 **소비자 쪽**(판정기를 안 돌리고 두 파일을 직접 읽는 경로)으로 정정했다. correction 마일스톤 범위가 잘못된 전제 위에 서지 않게 하는 것이 목적 |
| CR-4 | LOW | **CLI 플래그 값 검증이 없었다.** `flag()` 가 다음 토큰을 무조건 값으로 먹어 `--index-dir --report` 가 `indexDir="--report"` 로 조용히 떨어졌다 | 양쪽 파일의 `flag()` 가 다음 인자가 없거나 `--` 로 시작하면 사유를 실어 throw. 두 `main()` 이 이미 try/catch 로 감싸여 exit 1 로 떨어진다 |

검토했으나 결함이 아닌 것: CRLF(`.gitattributes` 가 `docs/_meta/index/** text eol=lf` 로 이미 못박음 — 신규 2파일도 그 glob 안) · 파일 길이 801·1606행(형제 판정기 `verify-tests` 4,218 · `verify-api-contracts` 1,897 과 같은 계열) · `classifyInner` 두 벌 중복(3자 대조의 셋째 축, 의도된 중복) · worktree 에서 `DEFAULT_REPOS_ROOT` 가 안 맞는 것(형제 6종 전부 같고 서브모듈이 빈 것이 원인, 헤더에 `--repos-root ../..` 명시) · 단일 namespace id 충돌(생성기·재구성 양쪽에서 throw, fail-closed).

수정 후 재검: 판정기 selftest **64 PASS** · 생성기 selftest **32 PASS** · 실산출 판정 exit 0(1,991 / 3,208 불변) · 상류 판정기 6종 exit 0 · `bogus_meta_field` 재변조 **거부 확인** · 재생성 산출물이 커밋본과 **바이트 동일**(판정 로직·CLI 파서만 건드렸으므로 산출물 불변).
