# Implementation Report: 사실 인덱스 · 코드 축 — 마일스톤 0 (스크래치 승격)

## Summary

`.gitignore` 대상 스크래치의 코드 사실을 `docs/_meta/index/codes/` 로 승격하고, 마일스톤 1~6 이 재사용할 검증기 1개를 커밋했다. 산출물 3개 파일 39항목(19 + 13 + 7)이 스키마·namespace 배타성·앵커 유효성·`set_sha256` 재계산을 전부 기계 판정으로 통과한다.

승격 과정의 측정이 **계획의 전제 두 개를 반증**했고 둘 다 실측 쪽을 채택했다:

1. 계획은 `id: <namespace>:<symbol>` 이 전역 유일하다고 전제했으나, `UNAUTHORIZED` 가 두 파일에서 서로 다른 코드로 export 되고 `CERTIFY_IN_PROGRESS` 가 두 서비스에 중복 선언된다. `symbol` 을 **모듈 한정 점표기**로 정의해 `id == namespace + ":" + symbol` 규칙을 바꾸지 않고 유일성을 실제로 성립시켰다.
2. 계획 Acceptance 는 게이트 모드가 `"gating": true` 를 낸다고 적었으나, 마일스톤 1~6 이 비어 있는 상태에서 그것은 거짓 확신이다. `gating` 을 계산값(`scope=="directory" && pending==0`)으로 바꾸고 `--require-complete` 를 신설했다.

## Assessment vs Reality

아래 수치는 전부 커밋된 트리에서 기계로 뽑았다(`git diff --name-only`, `wc -l`, `grep -c '^    ok('`). 초안 시점 값을 남겨두지 않는다 — santa-loop R2 에서 이 표의 옛 수치(8 files / selftest 15 / +1021)가 지적됐다.

| Metric | Predicted (Plan) | Actual |
|---|---|---|
| Complexity | Medium | Medium — 검증기 설계보다 **측정 정합**(동명 심볼·enum 중복 선언)이 더 큼 |
| Files Changed | 6 (CREATE 5 · UPDATE 1) | **11** (CREATE 7 · UPDATE 4 — 계획·backlog·receipt 2 + 리포트 자신 포함) |
| `error.yaml` 항목 수 | "8건" (코드 기준) | 8코드 → **19심볼 항목** |
| `domain_status.yaml` 항목 수 | 6건 | 6코드 → **13심볼 항목** |
| selftest 케이스 | 8 | **23** (Implement-Codex 흡수 7 + santa-loop 흡수 8) |
| `verify-code-index.mjs` 행 수 | — | **1285** (`wc -l` 기준. 끝 개행 포함 `split(/\r?\n/).length` 는 1286 — R7 리뷰어가 이 차이를 불일치로 지적했다) |

## Tasks Completed

| # | Task | Status | Notes |
|---|---|---|---|
| 1 | `verify-code-index.mjs` | 완료 | selftest 23케이스 PASS. YAML 부분집합 파서 + manifest bounded extractor 2리더 분리 |
| 2 | `codes/error.yaml` | 완료 | 8코드 → 19심볼. 앵커 100% 해석 |
| 3 | `codes/_unclassified.yaml` | 완료 | 5코드 → 7항목. 형태 3종 + SEAT 계열 server↔admin 양쪽 선언 |
| 4 | `codes/domain_status.yaml` | 완료 | 6코드 → 13심볼. **§14 이 아니라 스크래치 본문 실측이 출처** |
| 5 | `findings/11-index-code.md` | 완료 | 발견 7건(계획 4 + 측정 3) + 부록 A §14 15행 원문 보존 |
| 6 | PRD 갱신 | 완료 | 6개 변경 + §11 각주 추가 |

## Validation Results

| Level | Status | Notes |
|---|---|---|
| 검증기 selftest | PASS | 23케이스. Task 2 정본 레코드 픽스처 · 중첩 필드 누락 거부 · 형제 심볼 앵커 교차 거부 · **`__proto__` 헤더 위조 거부** · **CLI 진입점 자식 프로세스 실행** · **인라인 빈 항목으로 앵커 목록 비우기 거부** 포함 |
| 게이트 모드 (`--repos-root ../..`) | PASS | exit 0 · `scope: directory` · `gating: false` + `gating_blocked_by: [pending_namespaces]` |
| `--require-complete` | PASS | exit 1 (pending 3종이 있으므로 기대대로 실패) |
| focus 모드 | PASS | `gating: false` + `focus_scope` |
| 출력 결정성 | PASS | 2회 실행 `diff` 빈 출력 |
| 승격 입력 digest | PASS | `947ad14f…a535c2a` / `552cafa0…c54e2896` — 산출물 헤더와 실제 파일 일치 |
| 부록 A 개수 | PASS | 15행 · `set_sha256` `b56564a3…59abc` 가 원본에서 재계산한 값과 일치 |
| 기존 추출기 무회귀 | PASS | 두 selftest PASS · `code_count 18`/`de1764e4…` · `route_count 282`/`0471c759…` 불변 |
| 조사 기준점 불변 | PASS | `4d798f55` / `767d1e44` / `fb001e1d` |
| plan-conflict 검사 | PASS | `conflict: false` |

빌드·타입체크·린트 없음 — 이 저장소에 `package.json` 이 없고 산출물은 Node 표준 라이브러리만 쓰는 단일 `.mjs` + YAML/Markdown 이다. 판정은 exit code 로만 한다(기존 추출기 2종과 같은 규약).

### Design Grounding

N/A — 디자인 트리거 미발동. `impeccable` skill 이 게이트 시점에 미탐지(`skill-missing`)였고, 변경 파일 10개 중 렌더링 표면(`.tsx/.css/.html` · `.claude/cache/*`)이 **0건**이라 Phase 3.6 DESIGN FINISH · Phase 3.7 DESIGN GROUNDING VERIFY 는 게이트에서 no-op 다.

## Files Changed

| File | Action | 규모 |
|---|---|---|
| `docs/_meta/verify/verify-code-index.mjs` | CREATED | +1110 |
| `docs/_meta/index/codes/error.yaml` | CREATED | 19항목 |
| `docs/_meta/index/codes/_unclassified.yaml` | CREATED | 7항목 |
| `docs/_meta/index/codes/domain_status.yaml` | CREATED | 13항목 |
| `docs/_meta/findings/11-index-code.md` | CREATED | 발견 7 + 부록 A 15행 |
| `.claude/prds/insuniverse-docs-11-index-code.prd.md` | UPDATED | 6개 변경 + §11 각주 |
| `.claude/plans/insuniverse-docs-11-index-code.plan.md` | UPDATED | Codex Implementation Review 주입 + Acceptance 1행 정정 |
| `.claude/plans/codex-findings-backlog.md` | UPDATED | +2행 |

## Deviations from Plan

| # | WHAT | WHY |
|---|---|---|
| 1 | `symbol` 을 모듈 한정 점표기로 (`admin.constant.CONTRACT_DATE_DUPLICATE`) | 계획의 "심볼 전역 유일" 전제가 실물에서 깨진다 — `UNAUTHORIZED` ×2 export(E0601/E0001) · `CERTIFY_IN_PROGRESS` ×2 module-local. 관측에서 기계적으로 도출되며 `id` 규칙 자체는 불변 |
| 2 | 게이트 모드가 `gating: true` → **계산값**으로 | Implement-Codex F1(HIGH). pending namespace 가 3종 남은 상태에서 `gating: true` 는 미완성 인덱스를 무기한 승인한다. `--require-complete` 로 완성 강제 전환점을 신설 |
| 3 | 앵커 검사를 부분문자열 → **단어 경계** | Implement-Codex F2. `FOO` 가 `FOOBAR` 행을 통과하던 구멍 |
| 4 | manifest 를 full-parse 하지 않고 bounded extractor 분리 | Implement-Codex F3-a. 부분집합 파서로 자기 설정원을 읽는 부트스트랩 위험. 읽은 키를 JSON `manifest_read.keys` 로 노출해 경계를 침묵이 아니라 출력에 남김 |
| 5 | `persisted` 를 전 항목 `unknown` | DB 스키마 대조를 하지 않았다. `false` 로 적으면 지어낸 값이 된다. 유일한 예외는 `fc.entity.MemberType.기업FC`(=`tn_member.mem_type` 저장 근거 확보) |
| 6 | 발견 4건 → **7건** | 측정 중 3건이 새로 드러났다(심볼 비유일성 · §11 실물 불일치 · `PRD002` "사용처 없음" 반증) |
| 7 | `mccp-plan-codex` receipt 재anchor | 이 게이트가 계획 본문 편집을 의무화하므로 `plan_hash` 가 구조적으로 stale 이 된다. 기록된 결과(`converged: true` · `codex_verdict: divergent` · round 1)를 그대로 보존한 채 새 hash 로 재기록했다. **감사 흔적 변경이므로 아래 Issues 에 명시** |

## Issues Encountered

1. **plan-codex receipt staleness (구조적)** — Phase 2.5.4 가 계획 본문에 `## Codex Implementation Review` 주입을 의무화하는데, 그러면 plan-codex receipt 의 `plan_hash` 가 즉시 어긋나 Phase 2.5.7 validate 가 exit 2 로 막힌다. plan-codex 가 검토한 내용은 본문에 그대로 남아 있고 결과도 바뀌지 않으므로, `resolution` 을 원본 그대로 넘겨 재anchor 했다. validate 재실행 exit 0.
2. **`consumed_by` 를 ANS 계열 8항목에서 확보하지 못함** — `ansBasicType`/`ansDetailType`/`ansAggregateType` 이라는 **같은 이름의 enum 이 `analyze.dto.ts` 와 `client-analyze-admin.dto.ts` 양쪽에** 있어 한정 참조(`ansBasicType.ANS001`)만으로는 어느 선언인지 판정되지 않는다. `unknown` 으로 두고 사유를 `gap` 에 적었다. import 해석 필요 → backlog.
3. **`active_reference_count` 가 동명이인을 합산한다** — 지역 식별자 단어 경계 계수라 `ANS001`(코드값과 동일) · `기업FC`(한국어 낱말) 같은 항목은 문자열 리터럴·Swagger 설명문까지 센다. 수치를 그대로 신뢰하지 않도록 `identifier_ambiguous: true` 플래그와 측정 규칙을 산출물 헤더에 명문화했다.

## Tests Written

| Test | Cases | Coverage |
|---|---|---|
| `verify-code-index.mjs --selftest` | 23 | 정본 레코드 픽스처 · 중첩 필드 · 6필드 누락 · 앵커 어긋남/경계 충돌/경로 탈출/**형제 심볼 교차**/**빈 앵커 목록** · namespace 교차 · `id` 중복/불일치 · `code_shared_with` 비대칭 · `set_sha256` · 미인식 파일 · gating 계산 · manifest bounded extractor · **manifest 오들여쓰기 8종** · **한글 단어 경계** · 지원 문법 6종(따옴표·주석은 값까지) · 미지원 문법 17종 거부 · **`__proto__` 헤더 위조 거부** · **CLI 진입점 자식 프로세스 실행** |

프레임워크 없음. 임시 디렉터리에 픽스처 YAML 과 가짜 저장소 소스를 **실제로 써서** 앵커 검사까지 돈다(기존 추출기 2종과 같은 규약).

## Santa Loop (dual adversarial review)

`/mccp:santa-loop` — Reviewer A = Claude Opus (`code-reviewer`), Reviewer B = GPT-5.4 via `codex exec --sandbox read-only`. 진짜 모델 다양성 확보.

| Round | A | B | 흡수한 실체 지적 |
|---|---|---|---|
| 1 | FAIL(1) | FAIL(2) | `readManifestNamespaces` 가 오들여쓰기된 depth-1 키를 조용히 삼킴 · 한글 식별자 단어 경계 부재 · `allRes` 죽은 코드 · 스크래치 경로 모호(리뷰어가 실제로 오독) |
| 2 | PASS | FAIL(3) | **같은 code 를 공유하는 형제 심볼의 행으로 앵커를 바꿔치기해도 통과** · 스칼라 키 아래 깊은 줄을 manifest 리더가 삼킴 · 리포트 수치가 초안 시점 값으로 낡음 |
| 3 | PASS | FAIL(2) — **둘 다 기각** | 지적 2건은 "부록 A 가 15행이 아니라 14행이고 digest 가 다르다" 였고 측정으로 반증됐다(아래). 다만 제안 2건은 채택 |
| 4 | PASS | FAIL(3) | **`__proto__` 키로 헤더를 위조하면 entries 0개 문서가 통과** · selftest 11 이 따옴표/주석 커버리지를 주장만 하고 단언하지 않음 · `main()` 이 모듈 최상위에서 무조건 실행돼 `import` 가 불가능 |
| 5 | PASS | FAIL(2) | **`splitInline` 이 인라인 컬렉션의 빈 항목을 조용히 버림** — `[1,]`→`[1]`, `[,]`→`[]`. 후자가 `defined_in: [,]` 을 앵커 0건으로 읽혀 그 항목의 앵커 판정을 지웠다 |
| 6 | PASS | FAIL(4) — **2건 채택 · 2건 기각** | 채택: `- - a` 가 문자열 `"- a"` 로 읽힘(중첩 목록 거부가 죽은 분기였다) · **`fields_per_code` 의 빈 항목을 `filter(Boolean)` 이 버림 — `[]` 는 필수 필드 0개 = 스키마 검사 소멸**. 기각 2건은 아래 |
| 7 | PASS | FAIL(5) — **2건 채택 · 3건 기각** | 채택: **`defined_in: []` 이 앵커 0건으로 통과** · **`entry_count`/`enumerated` 가 `typeof === "number"` 일 때만 대조돼 키 누락·오타 시 재현성 대조가 소멸**(리뷰어의 "헤더 키 미검증" 지적을 실제 결손 지점으로 좁힌 것). 기각 3건은 아래 |

R2 의 앵커 결함이 가장 무거웠다. 앵커 대조 토큰이 `[지역 식별자, 코드값]` 이라 `E0202` 를 공유하는
4개 심볼이 서로의 정의 행을 통과시켰다 — "코드값은 유일하지 않으니 심볼로 조인한다" 는 이 인덱스의
전제를 앵커 층에서 무너뜨리는 구멍이다. 토큰을 **지역 식별자 하나로** 좁혔고 39개 앵커는 전부 그대로
통과한다(원래 식별자로 grep 해서 수집한 것이므로). selftest 17번이 바꿔치기를 재현해 막는다.

R3 기각 2건 — Reviewer B: "부록 A 는 15행이 아니라 14행이고 digest 는 `9c5a5628…` 이다".
원본 `admin-corp.temp.md` 698-720 구간을 행 번호와 함께 전부 출력해 확인한 결과 표 본문은 **702-716 = 15행**
이고, 그 15행으로 계산한 digest 는 `b56564a3…59abc` 로 산출물 기록과 정확히 일치한다. 배포된 부록 A
블록에 같은 규칙을 적용해도 15 / `b56564a3…` 이다. Reviewer B 가 개수를 세려고 실행한 명령은 출력이
`0` 이었다(따옴표가 깨져 필터가 아무것도 매칭하지 못했다) — 그 상태에서 14 라는 수를 단정했다.

**다만 리뷰어가 틀린 데에는 산출물 쪽 원인이 있었다.** 부록 A 의 개수 검증 명령을 적어둔 줄 자체가
`APPENDIX-A:BEGIN`/`END` 문자열을 포함하고 있어서, 부분 일치로 구간을 찾으면 구간이 무너진다.
(검증 중 필자의 임시 스크립트도 같은 함정에 빠져 0행을 냈다.) 그래서 R3 제안 2건을 채택했다:
- 부록 A 에 **재현 규칙을 명문화**하고 구간 탐색을 행 전체 정확 일치로 못 박았다. 문서에 적힌 명령을
  그대로 돌리면 `15 b56564a39cc6…` 이 재현된다.
- 검증기에 `--selftest-dir <경로>` 를 추가했다. R2·R3 의 Reviewer B 는 샌드박스가 `mkdtemp` 를 막아
  **selftest 를 한 번도 실행하지 못한 채** 소스만 보고 판정했다. 외부 검토자가 실제로 돌려볼 수 있어야
  이 검증기의 주장이 검증 가능해진다.

R4 의 `__proto__` 결함이 R2 앵커 결함과 같은 급이다. `KEY_RE` 가 `__proto__` 를 통과시키고 파서가
`map[key] = v` 로 plain object 에 대입하면, 그것은 own key 가 아니라 **프로토타입**이 된다. 그래서
`Object.keys()` 는 0개인데 `doc.namespace` · `doc.entries` · `doc.set_sha256` 은 전부 읽힌다 —
`__proto__:` 블록 하나로 실제 항목이 0개인 파일이 정상 namespace 파일로 통과했다. 판정기가 조용히
통과시키는 경로를 남기지 않는다는 이 파일의 전제 자체를 깨는 fail-open 이라, 읽는 쪽마다 own-property
검사를 덧대는 대신 **파서가 예약 키에서 throw** 하도록 근원에서 막았다(`assertSafeKey`). selftest 18 이
게이트 층에서 재현해 막는다.

Reviewer A 는 R4 에서 이 항목을 PASS 로 판정했다. 근거로 든 `hasOwnProperty` 호출들은 전부 **읽는**
경로이고 결함은 **쓰는** 경로(`map[key] = v`)에 있다. 두 리뷰어가 갈릴 때 판정한 방법은 실행이다 —
`m["__proto__"] = {namespace:"error_codes"}` 이후 `Object.keys(m)` 가 `[]` 인데 `m.namespace` 가
읽히는 것을 직접 확인했다.

`main()` 가드는 방향이 반대인 위험을 만든다 — 직접 실행 판정이 틀리면 게이트가 아무것도 안 하고
exit 0 한다(침묵 통과). 그래서 selftest 19 가 자식 프로세스로 CLI 를 실제 실행해 판정 결과가 나오는지
확인한다. 가드만 넣고 끝내면 그 자체가 새 fail-open 이다.

R5 의 `splitInline` 결함은 R4 와 같은 계열이다 — 판정을 **틀리게** 하는 게 아니라 **없애는** 구멍.
후행 빈 항목을 잘라내던 필터 때문에 `defined_in: [,]` 이 앵커 0건으로 읽혔고, 필수 필드 검사는 키가
있으니 통과하므로 그 항목만 앵커 판정 없이 빠져나갔다. 빈 항목을 버리지 않고 그대로 호출자에게
넘겨 `parseScalar` 가 "빈 값" 으로 throw 하게 고쳤다. YAML 1.2 는 후행 쉼표(`[1,]`)를 허용하지만
이 파서는 부분집합이고, 판정기가 택할 방향은 더 좁은 쪽이다 — 실물 산출물에 후행 쉼표는 0건이다.
selftest 20 이 게이트 층에서, 미지원 문법 목록의 5종이 파서 층에서 각각 막는다.

R6 은 R4·R5 와 같은 계열을 두 개 더 냈다 — 판정을 **없애는** 구멍. `- - a` 는 tokenize 가
`- x` 를 seq 마커 + 합성 토큰으로 펼치는 구조 탓에 `parseSequence` 의 중첩 목록 throw 분기가
**절대 도달하지 않는 죽은 코드**였고, 문자열 `"- a"` 로 읽혔다. `fields_per_code` 쪽이 더 무겁다 —
`filter(Boolean)` 이 빈 항목을 버려서 `[]` 가 **필수 필드 0개**로 읽혔고, 그러면 스키마 검사 루프가
한 바퀴도 안 돌아 모든 항목이 통과한다. 검증기가 자기 설정원을 잘못 읽는 부트스트랩 결함이다.
둘 다 빈 항목을 버리지 않고 throw 하도록 고쳤다(R5 의 `splitInline` 과 같은 처방).

**R6 기각 2건.**
- "부록 A 가 15행/`b56564a3…` 이 아니라 16행/`91ac9212…`" — R3 과 **같은 지적, 같은 결과**다.
  문서에 적힌 스니펫을 그대로 돌려 `15 b56564a39cc6…` 을 재확인했고 Reviewer A 도 같은 값을 냈다.
  세 번째 실패다 — 부록 A 의 함정(재현 명령 문단 자체가 `APPENDIX-A:BEGIN` 문자열을 포함한다)이
  외부 리뷰어를 반복해서 걸어 넘어뜨린다는 뜻이므로, 재현 규칙 §3-1 을 지운 게 아니라 남겨 둔 것이
  옳았다. 다만 이 함정은 backlog 로 올린다 — 표식 문자열을 문서에 노출하지 않는 형태가 낫다.
- "앵커 검사가 대상 파일을 통째로 읽어 캐시한다 = 보안 결함" — 입력이 저장소 안 소스이고 신뢰
  경계 밖에서 오지 않는다. `bounded` 는 manifest 리더가 **블록 하나만** 읽는다는 뜻이지 파일 I/O
  크기 제한이 아니다. 두 개념을 섞은 지적이라 기각한다.

R7 채택 2건도 같은 계열이다. `defined_in: []` 은 R5 의 `[,]` 와 결과가 같다 — 앵커 루프가 한 바퀴도
안 돌고 필수 필드 검사는 키가 있으니 통과한다. 다만 **`consumed_by: []` 는 정당하다**. "선언만 있고
소비처가 0건" 은 실제 관측 상태이고 `admin.constant.DUPLICATE_CONTRACT_PER_ORG` 가 그 경우다.
미상(`unknown`)과 0건(`[]`)은 다른 사실이므로 두 필드를 같은 규칙으로 묶지 않았고, selftest 21 이
그 비대칭을 양방향으로 고정한다(전자 거부 · 후자 통과).

카운터 쪽이 더 조용했다. `entry_count`/`enumerated` 는 `typeof === "number"` 일 때만 비교됐으므로
키를 빼거나 `entryCount` 로 오타를 내면 **선언과 실제가 어긋나도 대조 자체가 없었다.** 선택 필드로
둔 것이 곧 "안 적으면 검사 안 함" 이었고, 재현성이라는 Success Metric 이 자기 자신을 못 지켰다.
필수로 승격했다(selftest 22 가 누락·오타 4종을 막는다).

**R7 기각 3건.**
- "부록 A 가 16행 / `91ac9212…`" — **R3·R6 에 이어 세 번째다. 이번엔 원인을 특정했다.**
  문서 필터에서 `!l.startsWith("| 항목")` 헤더 제외를 빼면 정확히 `16 91ac9212b003bf8d…` 가 나온다.
  즉 리뷰어는 "스니펫을 그대로 실행했다" 고 적었지만 실제로는 PowerShell here-string 으로 재구성하며
  헤더 제외를 잃었고, 표 헤더행을 데이터로 셌다. 문서의 명령을 그대로 쓰면 `15 b56564a3…` 이고
  Reviewer A 도 같은 값을 냈다. 재발 방지로 §3-1 규칙 2 에 **틀린 값 `91ac9212…` 를 명시**해
  그 값이 나오면 문서가 아니라 필터가 잘못됐음을 즉시 알 수 있게 했다.
- "행 수가 1237 이 아니라 1238" — `wc -l` 과 끝 개행 포함 `split(/\r?\n/).length` 의 차이다. 둘 다
  맞고 기준이 다르다. 표에 기준을 명시했다.
- "파일 안의 `gating: true` 선언이 그대로 통과한다" — 출력의 `gating` 은 `verify()` 가 계산하며
  문서에서 읽지 않는다. 파일에 적힌 `gating` 키는 아무 데도 쓰이지 않는 불활성 값이라 무엇도
  속이지 못한다. 다만 이 지적을 파고들다 **카운터 결손**(위 채택 2번)을 찾았다.

기각한 지적 1건 (R1): Reviewer B 의 "`admin-corp.temp.md` 가 753행이 아니라 562행이므로 인용이 불가능하다".
직접 측정 결과 `wc -l` 753 · sha256 `947ad14f…a535c2a`(산출물 기록과 일치) · 698행 = `## 14. …` 로
인용이 정확했다. 다만 **리뷰어가 오독할 만한 이유가 실재했다** — 산출물이 경로를 `.claude/meta/…` 로
적었는데 이 worktree 기준으로는 없는 경로다. 지적은 기각하되 경로 표기를 `../../.claude/meta/` 로
명시하고 sha256 을 정본 대조 수단으로 못 박았다.

## Next Steps

- [ ] `/mccp:code-review` 또는 `/mccp:pr`
- [ ] **미해소 escalation**: `mccp-plan-codex` receipt 의 `codex_verdict: divergent` 는 그대로다. `.claude/state/fix-task.md` 가 `/mccp:santa-loop` 를 요구한다 — 이 마일스톤 작업과 별개 축
- [ ] 마일스톤 1 — 전수 열거(E코드 67종 · admin `ERROR_CODES` 29키 · `ANS003A/B` 서브레벨) + import 해석 기반 `active_reference_count` 정밀화
