# Implementation Report: 사실 인덱스 · 코드 축 — 마일스톤 1

**Plan**: `.claude/plans/insuniverse-docs-11-index-code-m1.plan.md`
**PRD**: `.claude/prds/insuniverse-docs-11-index-code.prd.md` (Delivery Milestones 1행)
**Branch**: `feat/index-code`
**조사 기준점**: server `4d798f55` · site `767d1e44` · admin `fb001e1d`

## Summary

`codes/*.yaml` 을 3파일 39항목에서 **7파일 619항목**으로 전수 확장했다. 본체는 YAML 이 아니라
생성기 `build-code-index.mjs` 1개다 — 손으로 적으면 재현성 지표(추출 2회 `diff` 빈 출력)가 처음부터
성립하지 않기 때문이다. 검증기에는 무검사이던 `cross_source` 축과 `menu_db` 결손 4필드 검사를 넣었고,
`symbol` 을 저장소 한정 완전 경로로 승격했다.

**수치의 정본은 `--report` 출력이다.** 계획의 추정치는 근거로 쓰지 않았다.

## Assessment vs Reality

| Metric | Predicted (Plan) | Actual |
|---|---|---|
| Complexity | Large | Large — 예상대로. 다만 난이도의 소재가 달랐다(아래) |
| Files Changed | 13 | 12 (계획의 `extraction-manifest.yaml` 은 읽기 전용이라 애초에 제외) |
| 항목 수 | **의도적으로 목표 미기재** | 619 (`--report` 출력) |
| 예상 못 한 결함 | — | 6건 (findings 발견 8~13) |

계획이 예상한 난이도는 "700항목을 손으로 못 쓴다" 였다. 실제 난이도는 **정규식이 축 소속을 판정할
수 없다**는 것이었고(발견 9·10), 이것은 전수 생성을 실제로 돌려서야 드러났다.

## Tasks Completed

| # | Task | Status | Notes |
|---|---|---|---|
| 0 | `symbol` 저장소 한정 승격 | 완료 | 39항목 마이그레이션 + `CANON_ENTRY`/`CANON_IDS` + PRD Scope + findings 발견 5 를 함께 이동. selftest 23·24·25 가 강제 |
| 1 | `cross_source` + `menu_db` 4필드 검증 | 완료 | 계획보다 **넓게** 구현 — 키를 `namespace+code` 가 아니라 `code` 로 잡아 축을 가로지르는 중복까지 덮는다(이유는 아래 Deviations) |
| 2 | `extract-screen-codes` 라우트 앵커 | 완료 | `extract()` 에 `anchors` 추가. 반환 계약은 추가만 했고 동결값 282/`0471c759…` 유지 |
| 3 | 심볼 스캐너 | 완료 | 4형태 + `resultCode` 변종. `--report` 가 수치 정본 |
| 4 | `active_reference_count` 1패스 계수 | 완료 | 저장소별 1패스, `BOUND` 를 검증기에서 **import**(복제 아님) |
| 5 | 큐레이션 병합 | 완료 | exact-id 전용. 39항목 `promoted_from`/`evidence_grade` 전수 잔존 확인 |
| 6 | 6개 YAML 생성 | 완료 | 실제로는 **7파일**(menu 축이 hardcoded/db 2파일) |
| 7 | findings + PRD + backlog | 완료 | findings 발견 8~13 · §2 개정 요청 3건 · backlog 4건 |

## Validation Results

| Level | Status | Notes |
|---|---|---|
| selftest (4종) | 통과 | menu · screen · build(29) · verify(30) |
| manifest 동결값 | 통과 | screen 282/`0471c759…` · menu 18/`de1764e4…` |
| 게이트 | 통과 | `ok:true` · `pending_namespaces: []` · `gating:true` · files=7 |
| 재현성 | 통과 | 생성 2회 `git diff --exit-code` 빈 출력 |
| 큐레이션 잔존 | 통과 | `promoted_from`=39 · `evidence_grade`=39 · `note`=15 |
| 부록 A digest | 통과 | 15 / `b56564a3…` 재현 |

빌드·타입체크·린트·유닛테스트 단계는 **없다** — 이 저장소에 `package.json` 이 없고 산출물이
의존성 없는 `.mjs` 스크립트와 `.yaml` 문서다. 각 스크립트의 `--selftest` 가 그 자리를 대신한다
(프레임워크 없음, 종료 코드로 판정 — 기존 규약 그대로).

### 측정 리포트 (acceptance 의 수치 근거)

```
  insuniverse-server   files=721  raw=885  decls=279  symbolless=606  extractor_owned=0
  insuniverse-site     files=447  raw=723  decls=1    symbolless=722  extractor_owned=0
  insuniverse-admin    files=555  raw=571  decls=29   symbolless=542  extractor_owned=31

  error_codes          entries=114  distinct=67   in_source=67  symbolless_codes=0
  domain_status_codes  entries=108  distinct=59   in_source=71  symbolless_codes=12
  vendor_codes         entries=44   distinct=22   in_source=45  symbolless_codes=23
  _unclassified        entries=43   distinct=38   in_source=38  symbolless_codes=0
  screen_codes         entries=282  distinct=282
  menu_codes           entries=28   distinct=28

  다중도: 1심볼 105 · 2심볼 59 · 3심볼 5 · 4심볼 15 · 5심볼 1 · 6심볼 1
  총 항목 수: 619
```

`in_source` 와 `distinct` 의 차이가 곧 "소스에 있으나 선언이 없어 항목이 되지 못한 코드값" 이고,
각 파일 `gap` 이 그 수와 예시를 적는다. **두 수를 같은 것으로 보면 그 차이가 "누락" 으로 오해된다.**

## Files Changed

| File | Action | Notes |
|---|---|---|
| `docs/_meta/verify/build-code-index.mjs` | CREATE | 생성기. 스캐너 + 계수기 + 병합 + YAML 쓰기 |
| `docs/_meta/verify/verify-code-index.mjs` | UPDATE | `cross_source` · `menu_db` 4필드 · 축 2종 구분 · 픽스처 7개 추가(23→30) |
| `docs/_meta/verify/extract-screen-codes.mjs` | UPDATE | 라우트 앵커 + CLI 진입 가드 |
| `docs/_meta/verify/extract-menu-codes.mjs` | UPDATE | CLI 진입 가드 |
| `docs/_meta/index/codes/error.yaml` | UPDATE | 19 → 114항목 |
| `docs/_meta/index/codes/domain_status.yaml` | UPDATE | 13 → 108항목 |
| `docs/_meta/index/codes/_unclassified.yaml` | UPDATE | 7 → 43항목 |
| `docs/_meta/index/codes/vendor.yaml` | CREATE | 44항목 (`CF-` 22코드) |
| `docs/_meta/index/codes/screen.yaml` | CREATE | 282항목 |
| `docs/_meta/index/codes/menu_hardcoded.yaml` | CREATE | 18항목 |
| `docs/_meta/index/codes/menu_db.yaml` | CREATE | 10항목 (`complete:false` + 결손 4필드) |
| `docs/_meta/findings/11-index-code.md` | UPDATE | 발견 8~13 · §2 개정 요청 3건 |
| `.claude/prds/insuniverse-docs-11-index-code.prd.md` | UPDATE | Scope 심볼 정의 + Milestone 1행 (2곳, 형제 PRD 불간섭) |
| `.claude/plans/codex-findings-backlog.md` | UPDATE | 4건 추가 |

## Deviations from Plan

1. **`cross_source` 를 계획보다 넓게 구현했다.** 계획은 "같은 namespace 의 서로 다른 파일 2개"
   였는데, 실측에서 `MEM002`·`POP001`·`SUB00x` 5종이 **축을 가로질러** 중복했다(도메인 상태이면서
   메뉴 코드). 좁은 규칙으로는 이것이 배타성 위반으로 잡혀 게이트가 막힌다. 키를 코드값 하나로
   바꿔 두 경우를 같은 규칙으로 다룬다 — PRD 의 "소스 축 중복은 `cross_source` 로 드러남" 과 맞다.

2. **namespace 를 두 종류로 나눴다** (계획에 없던 개념). 선언 축(모양으로 소속 결정)과 추출기
   축(출처로 소속 결정). 계기는 `menu_codes` 정규식이 서버 에러 상수 7종을 통과시켜 `_unclassified`
   에서 쫓아내는데 갈 곳이 없던 것이다(발견 9). 정규식만으로 축 소속을 판정할 수 없다는 사실이
   드러난 지점이며, 이것 없이는 게이트를 통과시킬 방법이 규칙을 약화시키는 것뿐이었다.

3. **`complete` 를 측정값으로 만들었다.** 계획은 파일별로 손으로 정하는 것을 전제했으나, 손으로 적은
   주장은 다음 실행에서 낡고 낡은 주장은 결손을 가린다. 소스에 등장한 코드값 중 항목이 되지 못한
   것이 0이면 `true` 다.

4. **screen 심볼도 저장소 한정으로 통일했다** (Implement-Codex F1). 계획 Task 6 은 `admin.page_routes./…`
   처럼 저장소 접두어 없는 별개 문법을 규정했는데, 그러면 Task 0 이 selftest 로 못박는 불변식이
   선언 픽스처에서만 성립하고 생성되는 282항목에서 깨진다.

5. **Task 0 마이그레이션을 생성기 최초 실행보다 앞선 별도 단계로 못박았다** (Implement-Codex F2).
   그렇게 하지 않으면 Task 5 의 "기존 id 가 없으면 중단" 가드가 최초 실행에서 39건 전부에 걸려
   마일스톤이 시작조차 못 한다. 레거시 접미 매칭 shim 은 만들지 않았다 — 영구히 남을 우회로다.

6. **`stg-db:` 유사-저장소 접두어를 도입했다** (계획에 없음). `menu_db` 항목의 선언은 저장소 밖
   live DB 행이라 저장소 이름을 붙일 수 없다. 없는 저장소를 지어내지 않기 위한 표기이며 규약화는
   backlog.

## Issues Encountered

전부 **검증 장치가 먼저 잡았다** — 이 마일스톤에서 셋 다 실제로 작동했다.

| 문제 | 잡은 장치 | 해결 |
|---|---|---|
| 스캐너가 사용처를 선언으로 읽어 거짓 항목 269건 (`case 'ANS001':` · Swagger `example:` · 탭 배열) | id 전역 충돌 판정 (쓰기 0건 중단) | 구간 경계를 "자기 문장의 끝" 으로 좁히고, 값이 코드인 임의 필드 규칙을 enum·지목 테이블로 한정 |
| `client.constant.ts` 선언 전체 누락 (`resultCode`/`resultMsg` 표면) | 큐레이션 병합의 "기존 id 없음" 중단 | 형태 1 에 `resultCode` 추가 |
| 여러 줄 선언의 앵커가 `code:` 행을 가리켜 앵커 판정 전부 탈락 | 검증기 앵커 검사 | 앵커를 선언 행으로 |
| 주석 처리된 사본이 진짜 선언과 같은 심볼 생성 | id 전역 충돌 판정 | 스캔 전 주석 제거 (**행 번호 보존**) |
| 중첩 테이블에서 부모 키가 다른 같은 코드가 한 심볼로 붕괴 | id 전역 충돌 판정 | 키 경로를 추적하는 walker |
| 추출기를 `import` 하면 그쪽 CLI 가 남의 argv 로 돌고 `process.exit` | 생성기 selftest 출력에 `selftest PASS` 가 두 번 | 두 추출기에 진입점 가드 추가 |
| 렌더-재파싱 대조가 키 순서에 민감해 정상 왕복을 전부 불일치로 판정 | round-trip 검사 자체 | 매핑 키 순서를 무시하는 비교로 (배열 순서는 의미이므로 유지) |
| 검증기 요약에서 3개 파일이 조용히 빠짐 | `--json` 의 `files.length` 대조 | 검사 건너뛰기와 보고 건너뛰기를 분리 |

## Tests Written

프레임워크 없음. 기존 규약대로 각 스크립트의 `--selftest` 에 인라인 픽스처로 넣었다.

| 파일 | 케이스 | 새로 추가한 축 |
|---|---|---|
| `verify-code-index.mjs` | 30 (기존 23 + 7) | 디렉터리 충돌 · 저장소 충돌 · `localId` 불변식(screen 포함) · `cross_source` 정상/누락/불일치/거짓표기 · `menu_db` 4필드 |
| `build-code-index.mjs` | 29 (신규) | 4형태 + `resultCode` 변종 · 사용처 배제 · 구간 경계 · 주석 제거 · 중첩 경로 · 단어 경계 계수 · round-trip · 병합 보존/중단 · id 충돌 시 쓰기 0건 |
| `extract-screen-codes.mjs` | 기존 + 1 | 앵커 행 번호 |

## Next Steps

- [ ] `/mccp:code-review` 또는 `/mccp:santa-loop` — 이 마일스톤은 검증 장치가 스스로를 검사한 비중이 커서 외부 리뷰 가치가 높다
- [ ] `00-protocol` 에 §2 규약 개정 요청 3건 전달 (backlog HIGH)
- [ ] 마일스톤 2 (`policy-anchors.yaml`)

## 남긴 결손 (숨기지 않은 것)

- `symbolless_codes`: domain_status 12 · vendor 23 — 소스에 등장하나 선언이 없어 항목이 못 된 코드값. 각 파일 `gap` 에 개수와 예시.
- `menu_db`: 전수 104행 중 10종만 열거 (PRD 확정 결손). 검증기가 4필드 존재를 강제.
- `persisted`: 큐레이션으로 확정된 것 외 전부 `unknown`. DB 스키마 대조 미수행.
- `active_reference_count`: 지역 식별자 단어경계 개수라 동명이인이 합산된다(`identifier_ambiguous: true`). 정밀 계수는 backlog.
- `parent` (screen): 경로 접두 기준이며 라우트 파일의 중첩 구조를 파싱한 값이 아니다.
