# Implementation Report: 12 · 사실 인덱스 · 런타임 축 — 마일스톤 0 (신 기준점 규모 재측정)

**Plan**: `.claude/plans/insuniverse-docs-12-index-runtime.plan.md`
**Branch**: `feat/index-runtime` (worktree `.worktrees/index-runtime`)
**Date**: 2026-08-05

## Summary

4축 규모(스케줄러 · 벤더 분기 · 약관 표면 · CI 파일 수)를 신 기준점 SHA 에서 재측정하는 무의존 추출기
`extract-runtime-scale.mjs` 를 커밋하고, 그라운딩에서 나온 불일치 5건을 `findings/12-index-runtime.md` 에
기록하고, PRD Evidence 표 · Success Metric 2건 · Open Question 1건을 갱신했다.

**계획이 예상한 대로 되지 않은 것 하나** — 구값과의 차이를 "stg 진행량 / exclude 결함" 으로 가르는 것은
**도출 불가능**하다. 구 측정 정의가 복원 불가라 신 정의에서 잰 exclude 민감도를 구값 차이에서 대수적으로
분리할 근거가 없다. 계획이 Codex F1 을 흡수해 이미 이 결론을 담고 있었고, 구현은 그대로 따랐다 —
차이는 전량 `unattributable`, exclude 민감도는 신 정의 내 별도 값.

### 측정된 값 (신 기준점, 2026-08-05)

| 축 | 값 |
|---|---|
| 스케줄러 | `inventory` **17** / `declared` **15** / `activation_evidence` **0** (`deployment-declared`) |
| 벤더 분기 | `filtered` **1,053회 / 152파일** (`raw` 1,304회 / 170파일) |
| 약관 표면 | `filtered` **2,082회 / 156파일** (`raw` 5,990회 / 203파일) |
| CI 파일 수 | server 2 / site 4 / admin 5 (구값과 동일) |
| exclude 민감도 (신 정의 내) | 벤더 251회/18파일 · 약관 3,908회/47파일 |
| 구값과의 차이 | **전량 `unattributable`** |

## Assessment vs Reality

| Metric | Predicted (Plan) | Actual |
|---|---|---|
| Complexity | Medium | Medium |
| Files Changed | 3 (신규 2 · 수정 1) | 3 (신규 2 · 수정 1) — 계획과 동일 |
| Task 1 어서션 | 8건 | 8/8 PASS |
| Task 2 어서션 | 4블록 + 3필드 + 단조성 | 10/10 PASS |

## Tasks Completed

| # | Task | Status | Notes |
|---|---|---|---|
| 1 | 스케줄러 축 (4기제 독립 파싱) | 완료 | 8개 어서션 전부 PASS. 단일 N 없음 |
| 2 | 벤더·약관 축 (이중 측정) | 완료 | `raw`/`filtered` × occurrence/file. 정의를 출력에 박음 |
| 3 | 부분 파싱 fail-closed + selftest + 결정성 | 완료 | 33개 selftest 검사, 그중 13개가 fail-closed 검사 |
| 4 | `findings/12-index-runtime.md` | 완료 | 5건 × 6항목 완비 (기계 검증) |
| 5 | PRD 갱신 + 00-protocol 회부 | 완료 | 회부 3건 신설. 마일스톤 0 은 `in-progress` 유지 |

## Validation Results

| Level | Status | Notes |
|---|---|---|
| Static Analysis | PASS | `node --check` OK. 프로젝트에 type-check/lint 도구 없음 (package.json 부재) — 해당 없음 |
| Unit Tests | PASS | `--selftest` 33건. 프레임워크 0 (규약 준수) |
| Build | N/A | 빌드 대상 아님 (독립 실행 `.mjs`) |
| Integration | N/A | 서버 없음 |
| 재현성 (플랜 V2) | PASS | `--json` 2회 `diff` 빈 출력 |
| 기준점 동결 (플랜 V3) | PASS | 서브모듈 3/3 HEAD 가 manifest `repos[].head` 와 문자열 일치 |
| 기존 추출기 회귀 (플랜 V4) | PASS | `extract-screen-codes` · `extract-menu-codes` selftest PASS |
| 경계 침범 (플랜 V5) | **조건부 PASS — 아래 참조** | `reproduce:` 블록 무변경. 검사 명령 자체는 false positive |

### Design Grounding

N/A (no design trigger) — `impeccable-detect --mode implement` → `skill_available:false` · `design_signal:false`.
산출물이 `.mjs` + 마크다운 + PRD 표라 렌더링 표면이 0 이다. Phase 3.6/3.7 은 게이트 미충족으로 no-op.

## Files Changed

| File | Action | Lines |
|---|---|---|
| `docs/_meta/verify/extract-runtime-scale.mjs` | CREATED | +662 |
| `docs/_meta/findings/12-index-runtime.md` | CREATED | +107 |
| `.claude/prds/insuniverse-docs-12-index-runtime.prd.md` | UPDATED | +116 / -26 |

`docs/_meta/extraction-manifest.yaml` — **이 마일스톤은 편집하지 않았다** (경계 유지).
`docs/_meta/index/` — 만들지 않았다 (마일스톤 1 소관).

## Deviations from Plan

**D1 · 스캔 universe 를 "작업트리 walk" 에서 "git 추적 파일 전수" 로 바꿨다.**
- WHAT: `scanRepos` 가 디렉터리 walk 대신 `git ls-files -z` 를 쓴다.
- WHY: walk 는 미추적·무시 파일까지 집어 든다. 실제로 `insuniverse-admin/.claude/state/` (mccp 하네스 state)
  가 서브모듈 작업트리 안에 있었다. 그러면 측정 universe 가 `repos[].head` 로 동결된 내용이 아니게 되고
  깨끗한 클론에서 다른 값이 나온다 — Success Metric "재현성 편차 0" 이 성립하지 않는다.
- 값 영향: **0**. 현 체크아웃에서는 두 방식의 파일 수가 3,905 로 동일하고 키워드 히트도 없었다.
  정의를 고친 것이지 값을 고친 것이 아니다.

**D2 · 키워드별 개별 스캔을 계열별 묶음 정규식 + 단일 읽기 패스로 바꿨다.**
- WHAT: 113MB × 18키워드 × 2패스 → 파일당 1회 읽기 + 정규식 3개, `raw`/`filtered` 를 같은 개수에서 집계.
- WHY: 최초 구현이 120초를 넘겨 백그라운드로 밀렸다. 재현성 검증이 2회 실행을 요구하므로 실용 불가.
- 값 영향: **0** (전후 값 동일 확인). 실행 시간 >120초 → 3.6초.
- 부수 효과: `filtered` 가 `raw` 의 부분집합에서 파생되므로 `filtered <= raw` 가 구조적으로 성립한다
  (그래도 명시 검사는 유지).

**D3 · 벤더/약관 스캔 범위를 `src_roots` 가 아니라 저장소 전수로 잡았다.**
- WHAT: manifest `src_roots` 로 좁히지 않는다.
- WHY: `exclude_glob` 대상(`planning/` · `artillery/` · `pdfjs-dist/` · `_statics/` 등)이 전부 `src_roots`
  **밖**에 있다. `src_roots` 로 좁히면 exclude 가 구조적 no-op 이 되어 민감도가 항상 0 이 되고,
  그것은 "파서가 빈 목록을 반환했을 때" 와 구분되지 않는다 (Codex F1 이 지목한 바로 그 위조).
- 값 영향: 민감도가 실측된다 (벤더 251회 · 약관 3,908회).

**D4 · 법적 키워드 집합에서 `재동의` 를 뺐다.**
- WHAT: PRD 가 열거한 대상 중 `재동의` 를 키워드로 넣지 않는다.
- WHY: `동의` 의 부분문자열이라 occurrence 합산이 이중 계수된다. 집합이 서로 부분문자열이 아님을
  `assertNoKeywordOverlap` 으로 **강제**해 이 부류를 정의 단계에서 막았다. `재동의` 는 `동의` 에 포함돼 계수된다.
- 값 영향: 이중 계수 없음.

## Issues Encountered

**I1 · 플랜 Validation 5 (`경계 침범 0`) 가 false positive 를 낸다.**
- 증상: `git diff --name-only | grep extraction-manifest.yaml` 가 매칭 → `BOUNDARY VIOLATION` 출력.
- 원인: 이 검사는 **작업트리 전체**를 본다. manifest 는 이 마일스톤 시작 **이전부터** 수정돼 있었다
  (세션 시작 시점 `git status` 스냅샷에 이미 `M`). 내용은 `known_gates` v2→v3 추가 — 00-protocol 소유
  변경이며 PRD 닫힌 질문이 "반영됨 (2026-08-05)" 으로 이미 기록한 것이다.
- 판정: **경계 위반 아님.** 이 마일스톤이 지켜야 할 불변식(`reproduce:` 블록과 추출기 등재를 건드리지
  않음)은 성립한다 — `git diff ... | grep -E '(reproduce|extract-runtime)'` 매칭 0건으로 확인.
- 절차: `plan-conflict-detector` 를 규정대로 실행했다. 작업트리 전체를 입력으로 주면 `conflict:true`
  (`file-expansion`)가 나오지만, 열거된 4파일은 전부 ① 플랜의 Files to Change 안이거나 ② 마일스톤 시작
  전부터 존재한 다른 PRD 의 미커밋 작업이다. 이 마일스톤이 도입한 파일만으로 재실행하면 `conflict:false`.
  `origin/main..HEAD` (커맨드 명세 입력)로도 `conflict:false`.
- 남는 것: 플랜 Validation 5 의 명령을 "마일스톤 시작 이후 변경분" 기준으로 고쳐야 정확하다.
  값·산출물에는 영향 없음.

**I2 · Implement-Codex 리뷰 섹션 주입이 상위 receipt 를 stale 로 만든다.**
- 증상: 게이트 2.5.4 가 지시한 대로 플랜에 `## Codex Implementation Review` 를 주입하자 `plan_hash` 가
  바뀌어 `mccp-plan-codex` receipt 가 stale 판정.
- 처리: `mccp-plan-codex` 를 **동일 verdict(`divergent`) · 동일 라운드**로 재기록해 해시만 갱신했다.
  판정을 세탁하지 않았다 — verdict 는 그대로 `divergent` 이고, 그 결과 `/mccp:pr` 의 cross-gate dedupe 는
  여전히 fail-closed 라 PR-Codex 가 반드시 돈다.

## Tests Written

| Test | Tests | Coverage |
|---|---|---|
| `extract-runtime-scale.mjs --selftest` | **37건** | exclude_glob 파서(8) · exclude 매칭(4) · 파일명→종 이름(4) · dockerfile case 파싱 + 부분 파싱 fail-closed(7) · CI TYPES 파싱(4) · 키워드 계수 + 이중 계수 방지(9) |

fail-closed 검사 16건이 "그럴듯한 틀린 값" 을 막는 쪽에 집중돼 있다 — 라벨 손상 · 종결자 손상 ·
라벨 중복 · `esac` 소실 · `case` 다중 출현 · `exclude_glob` 블록 0개/2개/항목 0건/중복/**블록 중간
비목록 라인**/**인라인 문법** · `TYPES` 0건/중복/블록 부재 · 부분문자열 키워드 집합.

## Santa Loop (adversarial dual review) — Round 1

Reviewer A: Claude Opus (`code-reviewer` agent) · Reviewer B: GPT-5.4 (`codex exec --sandbox read-only`).
두 리뷰어 모두 컨텍스트 격리, 13개 기준 루브릭 공통.

| 기준 | A | B |
|---|---|---|
| 1 Correctness | PASS | **FAIL** |
| 3 Error handling | PASS | **FAIL** |
| 8 Fail-closed parsing | PASS | **FAIL** |
| 11 Ownership boundary | **FAIL** | **FAIL** |
| 12 Findings format | PASS | **FAIL** |
| 13 Claim verification | PASS | **FAIL** |
| 2·4·5·6·7·9·10 | PASS | PASS (4·5 는 11 파생으로 FAIL) |

**verdict: NAUGHTY** (양쪽 FAIL). 병합·중복 제거 후 처리:

| # | 지적 | 출처 | 판정 | 조치 |
|---|---|---|---|---|
| 1 | `parseExcludeGlob` 이 **부분 목록**을 조용히 반환 (블록 중간 비목록 라인에서 `break`) | B | **타당** | 해당 라인에서 `throw`. 인라인(flow) 문법도 명시 실패. fixture 3건 추가 |
| 2 | 스캔 루프 `catch { continue }` 가 읽기 실패를 삼켜 계수를 낮춤 | B | **타당** | 파일명 + 원인 적시 후 `throw`. 추적 파일이 안 읽히면 작업트리가 동결 SHA 와 어긋난 것이므로 측정 중단이 맞다 |
| 3 | findings 앵커가 전부 `파일:행` 이 아님 | B | **부분 타당** | F-01·F-04 에 실제 행 부여. F-02 는 **부재 관측**이라 가리킬 행이 없어 집합 앵커로 두고 헤더에 형식 사유 명시 (행 번호를 지어내지 않는다) |
| 4 | `extraction-manifest.yaml` 편집 = 소유 경계 침범 | **A+B** | **전제 오류** | 아래 참조 |
| 5 | `--json` 이 재현되지 않음 (git dubious-ownership) | B | **환경 산물** | codex 샌드박스가 다른 사용자 컨텍스트로 도는 탓. 이 워크트리에서는 2회 실행 `diff` 빈 출력. 에러 메시지에 `safe.directory` 안내 추가 |

### Round 2

스코프를 **커밋 `f123c84`** 로 한정해 재실행(더티 트리가 아니라 커밋을 본다 — #4 를 논쟁이 아니라
구조로 제거). 리뷰어는 fresh, 라운드 1 지적은 전달하지 않음.

| 기준 | A (Opus) | B (GPT-5.4) |
|---|---|---|
| 11 Ownership boundary | PASS ✅ | PASS ✅ |
| 3 Error handling · 12 Findings format · 13 Claim verification | PASS ✅ | PASS ✅ |
| 1 Correctness · 8 Fail-closed | PASS | **FAIL** |
| 5 Internal consistency | PASS | **FAIL** |
| 나머지 | PASS | PASS |

A: **PASS** (critical 0). B: **FAIL** (critical 3). 게이트 → NAUGHTY. **A 가 놓친 코드 결함 2건을 B 가
잡았다** — 모델 다양성이 실제로 값을 한 지점.

세 건 전부 재현 확인 후 수정:

| # | 지적 | 재현 | 조치 |
|---|---|---|---|
| 1 | `parseExcludeGlob` 이 홑따옴표 항목의 따옴표를 값에 삼킴 | `- 'a/'` → `["'a/'"]`, `isExcluded("a/x.ts") = false` | 인용 3종(`"` · `'` · 무인용)을 명시적으로 갈라 읽는다. 빈 값은 throw. **throw 없이 틀린 값**이 나오던 경로라 라운드 1 의 부분목록 수정으로는 안 잡혔다 |
| 2 | `countKeywords` 가 대문자 키워드에서 귀속 붕괴 | `countKeywords("TOSS toss",["TOSS"])` → `{TOSS:1, toss:NaN}` | 대소문자 접기(`foldKey`) 기반 역매핑. 정의로 되돌지 않는 매칭은 throw. `assertNoKeywordOverlap` 도 접은 값 기준으로 중복·부분문자열 검사 |
| 3 | PRD 자기모순 2곳 | Risks 행이 "늘었으면 stg 진행량 / 줄었으면 exclude 결함" 을 여전히 지시(경고 3 과 충돌) · Evidence 행이 "정본이 CI 와 dockerfile 두 곳" 을 여전히 주장(닫힌 질문과 충돌) | 두 문장을 취소선 + 정정으로 교체. 새 틀만 얹고 낡은 문장을 안 지운 것이 원인 |

selftest 37 → **44건**. 값 전부 불변 (vendor 1,053/152 · legal 2,082/156 · inventory 17 · `exclude_glob` 11, sha `7a8cf216…`).

### Round 3 (cap 도달)

스코프 `f123c84..e8d33ff` 두 커밋. 리뷰어 fresh.

| | A (Opus) | B (GPT-5.4) |
|---|---|---|
| verdict | **PASS** (critical 0) | **FAIL** (critical 1) |

B 지적 1건 — **타당, 수정함**:

`parseCiTypes` 가 `indexOf(")")` 로 블록 끝을 잡아 **주석 안의 `)` 를 종결자로 오인**한다.
재현: `TYPES=(\n  aa # )\n  bb\n)\n` → `["aa"]`, throw 없음.
`ci_declared` 를 조용히 낮추는 경로이므로 추출 결함이 배포 정책 불일치로 오독된다.
줄 구조 파싱으로 교체 — 주석을 **먼저** 벗기고 종결자는 `)` 단독 줄로만 인정. fixture 4건 추가.

**A 는 이 기준(8 Fail-closed)을 PASS 로 판정하며 "parseCiTypes cannot silently accept partial parse"
라고 명시했으나, 실측으로 반증됐다.** 라운드 2·3 연속으로 A 가 놓친 코드 결함을 B 가 잡았다 —
A 의 PASS 를 단독 통과 근거로 쓰면 안 된다는 실증이다.

selftest 44 → **48건**. 값 전부 불변 (12/12 어서션 PASS).

### 반복된 실패 양식 (3라운드 공통)

세 라운드의 코드 결함이 전부 같은 뿌리다 — **구간을 문자 검색으로 정하고, 값에서 구분자를 안 벗겼다.**

| 라운드 | 결함 | 뿌리 |
|---|---|---|
| 1 | `parseExcludeGlob` 이 블록 중간에서 `break` → 부분 목록 | 종료 조건을 구조가 아니라 "매칭 실패" 로 정함 |
| 2 | `- 'a/'` 의 따옴표를 값에 삼킴 | 스칼라를 인용 문법별로 안 갈랐음 |
| 3 | `indexOf(")")` 가 주석 안 `)` 를 종결자로 오인 | 구간 끝을 줄 구조가 아니라 문자로 찾음 |

dockerfile 파서만 구조 불변식(`;;` 수 == 라벨 수)을 받았고 나머지 둘은 못 받았다. 계획의
fail-closed 요구를 **한 파서에만** 적용한 것이 실질 원인이다.

**#4 에 대하여 — 라운드 1 에서 두 리뷰어가 독립적으로 같은 오판을 했다.** 둘 다 `git diff` 가 manifest 를 포함한다는
사실만 보고 "이 마일스톤이 편집했다"고 결론지었다. 실제로는 **이 마일스톤 착수 이전부터 워킹트리에
있던 변경**이며(세션 시작 `git status` 스냅샷에 이미 `M`), 내용은 00-protocol 이 이 PRD 의 **회부를
이행한** `known_gates` v2→v3 다. PRD 닫힌 질문이 이미 "반영됨 (2026-08-05)" 으로 기록하고 있었다.
이 마일스톤의 편집분에는 manifest 가 없고 `reproduce:` 블록도 무변경이다. 리뷰 프롬프트에 기준선을
주지 않은 것이 원인이므로 라운드 2 프롬프트에 명시한다. 커밋은 이 마일스톤 7개 경로로 한정해
브랜치 히스토리에 남지 않게 했다.

## Codex Implementation Review 결과

Codex R1 verdict `needs-attention`, HIGH 2건 — 둘 다 R1 에서 흡수:
- **F1** hand-rolled YAML 스캔이 빈 목록을 조용히 반환하면 `filtered == raw` 위조 → 블록 정확히 1개 +
  항목 ≥1 강제, 아니면 `exit 1`. `exclude_glob_count`(11) · `exclude_glob_sha256` 를 출력에 노출.
- **F2** 키워드 2개 매칭 파일의 파일 수 이중 계수 → `per_keyword[k].files` 와 `files_total`(합집합) 분리 +
  2키워드 1파일 fixture 로 실증.

receipt `codex_verdict=divergent` (구조화 verdict `needs-attention` 의 기계 매핑). 흡수는 완료했으나
verdict 를 임의로 `converged` 로 바꾸지 않았다 — dedupe 가 fail-closed 로 남아 PR-Codex 가 돈다.

## Next Steps

- [ ] **회부 ① — manifest `reproduce:` 에 `extract-runtime-scale.mjs` 등재** (00-protocol 소관).
      **반영 전까지 마일스톤 0 은 `complete` 가 아니다.**
- [ ] 회부 ② — `schedules.yaml` cron 열이 `external-unknown` 임을 부모 PRD 에 반영
- [ ] 회부 ③ — 배포 증거(EventBridge 규칙 · 실 task-def) 확보 경로
- [ ] 플랜 Validation 5 명령을 "마일스톤 시작 이후 변경분" 기준으로 정정 (I1)
- [ ] `/mccp:code-review` 또는 `/mccp:pr`
