# Implementation Report: 70 · 정책 문서 뷰어 — 마일스톤 3 (신뢰 등급 가시화)

## Summary

`docs/viewer/build-docs.mjs` 가 내는 항목 카드에 **직교하는 신뢰 등급 축 둘**을 세웠다.

1. **status 축** — `docs/_meta/template-config.yaml` 의 `statuses` 8종과 `status_precedence` 에서
   시각 무게를 유도하고(`--status-weight` CSS 사용자 정의 속성), `placement` 3어휘로 항목을
   **같은 페이지 안 별도 지면 3개**로 격리했다. 계수 0 인 지면도 키와 표기를 남긴다.
2. **앵커 신선도 축** — 마일스톤 5 의 `anchor-freshness.json` 을 `--freshness` 선택 입력으로 받아
   `{doc, axis, item}` 3열로 조인하고 버킷 계수를 **별개 요소**로 냈다. 미지정이면 전 항목에
   미측정을 표기한다.

`flows[].status_source` 승계는 승계원 문서·항목·status 와 지배 판정을 표면에 드러낸다. 검색
인덱스에 `placement` 를 더하고 status·placement 패싯 필터를 붙였다(클래식 스크립트 · `fetch(` 0건).

**핵심 실측**: `body` 배치 `active-prod` 인데 앵커가 rot 인 항목이 **56건**이다. 두 축을 한 배지로
접으면 그 56건이 어느 쪽으로도 안 읽힌다 — 직교는 취향이 아니라 실측이다(게이트 `#11`).

## Assessment vs Reality

| Metric | Predicted (Plan) | Actual |
|---|---|---|
| Complexity | Large | Large — 생성기 +약 480행 · 게이트 하네스 신설 1,180행 |
| Confidence | — | 게이트 `check-all` **15/15 green** (커밋 후). 앞 창 하네스도 15/15 |
| Files Changed | 5 | 5 (계획서 §Files to Change 와 정확히 일치) |

## Tasks Completed

| # | Task | Status | Notes |
|---|---|---|---|
| 1 | 기준선 확보 | [done] Complete | `#1` — merge-base `3ac47e5e` 바이트 대조 |
| 2 | 게이트 하네스 골격 | [done] Complete | `#2` — 서브커맨드 18종 · `TEMPS` + `process.on("exit")` |
| 3 | status 어휘와 배치를 정본에서 유도 | [done] Complete | `#3` — 코드 리터럴 473건 중 어휘 리터럴 0 |
| 4 | placement 파티션과 격리 지면 | [done] Complete | `#4`·`#5` — 378 = 247 + 131 + 0 |
| 5 | 원본 격리와의 양방향 대조 | [done] Complete | `#6` — 126 == 126 · 대조 축 밖 5건 출력 |
| 6 | 시각 위계 | [done] Complete | `#7` — 반사실(precedence 양끝 교환) 추가. **이탈** — 아래 |
| 7 | 승계 렌더 | [done] Complete | `#8`·`#9` — 대상 문서 2종(17 + 3) |
| 8 | 앵커 신선도 배지 | [done] Complete | `#10`·`#11` — 949 앵커 · 378/378 조인 |
| 9 | 검색 패싯 필터 | [done] Complete | `#12` — 378/378 두 필드 · `fetch(` 0건 |
| 10 | 결정성과 fail-closed 계약 | [done] Complete | `#13` — **자기 기전** 판정으로 강화. 아래 §흡수 |
| 11 | PRD 전이와 이월 판정 | [done] Complete | `#14` — 두 자리 동시 전이 · 정정 4건 · 앵커 행 불변 |
| 12 | 앞 창 하네스 귀속 | [done] Complete | `#15` — M2 완주 15/15. 귀속 판정을 인과 신호로 고쳤다 |

## Validation Results

| Level | Status | Notes |
|---|---|---|
| Static Analysis | [done] Pass | `node --check` · 이 저장소는 lint 대상이 `docs/**` 마크다운이며 `verify.sh` 가 진다 |
| Unit Tests | [done] Pass | 게이트 하네스 15검사 + `--selfprobe` 16쌍이 이 저장소의 테스트 형태다 |
| Build | [done] Pass | `node docs/viewer/build-docs.mjs --freshness …` → 문서 24 · 항목 378 · HTML 25 |
| Integration | [done] Pass | 앞 창 하네스(M2) 완주 15/15 · 그 과정에서 `#8` 파손 1건 잡아 되돌렸다 |
| Edge Cases | [done] Pass | 음성 프로브 15건 전건 rc 1 (notes §음성 프로브 실행 기록) |
| 브라우저 실측 | [done] Pass | Whale CDP 직결 18건 전건 통과 · 스크린샷 5장 — 계획서 `(미검증)` 축 하나를 닫았다 |

### Design Grounding

**N/A (no design trigger)** — `impeccable-detect --mode implement` 가 `design_signal:false` ·
`silent_skip:true` · `reason:no-signal` 을 냈다. 원인은 구조적이며 마일스톤 2 와 같다: 렌더 표면이
`docs/viewer/build/` 아래에 나고 `.gitignore` 59행의 `build/` 가 추적 diff 와 untracked 목록
**양쪽에서** 그것을 뺀다. 생성기 자신은 `.mjs` 라 감지기의 UI 확장자 정규식에도 안 걸린다.
Phase 2.5.5c 캡처가 일어나지 않았으므로 Phase 3.7 은 완전한 no-op 이다.

## Files Changed

| File | Action | Lines |
|---|---|---|
| `docs/viewer/build-docs.mjs` | UPDATED | +약 480 / -약 30 (989행 → 1,461행) |
| `.claude/plans/insuniverse-docs-70-viewer-m3.gates.mjs` | CREATED | +1,180 |
| `.claude/prds/insuniverse-docs-70-viewer.prd.md` | UPDATED | +약 60 / -약 6 |
| `.claude/plans/codex-findings-backlog.md` | UPDATED | +4 (인수 1 · 신규 3) |
| `.claude/notes/insuniverse-docs-70-viewer-m3.md` | CREATED | +약 150 |

산출물(`docs/viewer/build/`)은 `.gitignore` 59행이 덮으므로 커밋되지 않는다.

## Deviations from Plan

1. **`--config` 인자 신설** — 계획서는 인자 목록을 정하지 않았다. 이 인자가 없으면 `#7` 의 순서
   반사실과 `#13` 의 `배치맵null`·`배치밖status` 픽스처를 **실저장소 config 를 건드리지 않고는**
   만들 수 없고, `docs/_meta/template-config.yaml` 은 golden 이 무는 파일이라 그것을 건드리는
   검사는 UI11 위반이다. `--docs-root` 과 **독립**으로 뒀다 — 어휘는 문서 트리의 성질이 아니라
   저장소 계약이고 `lint-docs.mjs` 도 config 를 명시 인자로 받는다.
2. **`metrics.json` 키 6종 추가** — `placements` · `statuses` · `status_weights` ·
   `status_precedence` · `anchor_freshness` · `inheritance`. 게이트가 HTML 과 마크다운 재열거
   사이의 **세 번째 다리**로 쓴다.
3. **`#7` 에 반사실 추가** — 계획서는 "소스에 상수 순위표가 없다" 를 요구했으나 그것은 grep 으로
   증명되지 않는다. precedence 양끝을 교환한 config 로 다시 빌드해 무게가 따라 뒤집히는지 본다.
4. **`#13` 을 자기 기전 판정으로 강화** — 아래 §Issues.
5. **지면 제목을 배치 어휘 토큰 그대로** (`지면 <code>appendix</code>`) — 배치 라벨의 정본이 없다.
   만들어 넣으면 그것이 생성기 소스의 배치 어휘 리터럴이 된다(`#3`). 대신 각 지면에 그 배치로
   떨어지는 status 라벨 목록을 함께 실었다.

## Issues Encountered

### `#13` 이 "거부됐다" 만 보고 어느 기전이 물었는지를 안 봤다 (층위 3)

음성 프로브에서 유일하게 통과시킨 자리. `freshness항목집합` 픽스처는 생성기의 **집합 대조**를
겨냥한 것인데, 그 대조를 지워도 하류 조인 가드가 대신 거부해 rc 1 이 나왔다. 두 기전이 다 살아
있는 것은 좋은 일이지만 **앞엣것이 죽어도 초록인 검사**였다.

수정: 픽스처마다 자기 기전이 내는 문면(`want`)을 함께 요구한다. 반사실로 뒤집힘을 확인했다 —
수정 전 통과 / 수정 후 `#13 FAIL: 자기 기전이 아닌 것이 거부했다 1건` / 무변이에서 `#13 ok`.

### `#7` 반사실의 초판 종료 조건이 값을 봤다 (층위 6)

*"뒤집은 순서에서도 원래 최대가 최대다"* 는 최대 **값**(n-1)이 어느 config 에서나 같으므로 항상
참이었고 정상 구현에서 `#7` 이 붉었다. 판정을 *"그 값을 **누가** 갖는가"* 로 바꿨다.

### 카드 여는 태그에 속성을 더해 앞 창 하네스 `#8` 을 깼다 (층위 5)

커밋 후 `#15` 재실행에서 드러났다. M2 `#8` 의 정규식이 `<section class="card" id="([^"]+)">` 로
**`>` 가 바로 오는 것**까지 문다(M2 하네스 904행). 같은 계약을 M2 `#7` 은 `>` 없이 물어 통과했고,
커밋 전 완주에서는 `#1` 의 붉음에 가려 안 보였다.

**앞 창 하네스는 고치지 않았다** — 이 창의 §Files to Change 밖이고(고치면 `#1` 이 붉어진다)
완료된 마일스톤의 봉인 산출물이다. 카드 여는 태그의 바이트 모양을 되돌리고 신뢰 등급 축을
카드의 `<h3>` 로 옮겼다. 곁가지로 `#11` 이 `data-status` **속성 보유**로 배지를 세고 있어
`<h3>` 이 같은 속성을 갖자 계수가 2배가 됐다 — 배지는 **클래스로** 세고, 반대로 접힘 판정은
`<span>` 한정에서 **태그 종류 무관**으로 넓혔다(span 만 보면 다른 요소로 접은 구현이 지나간다).

### `#15` 의 귀속이 경로 언급을 인과로 읽었다 (층위 6)

위 결함이 났을 때 `#15` 는 *"형제 델타 귀속 (이 창 [없음] · 형제
[docs/01-foundation/01-glossary.md])"* 로 판정했다. **거짓이다** — 그 경로는 결함의 **대상**이고
원인 파일 `docs/viewer/build-docs.mjs` 는 문면에 한 글자도 없었다. 두 신호를 따로 내도록 고쳤다:
(a) 문면 경로는 **대상**으로만 표기, (b) 이 창의 델타가 앞 창 하네스의 **입력 접두**를 건드렸는가.
(b)가 비어야만 형제 귀속을 주장한다.

### 색인 페이지에서 패싯 클릭이 매번 예외를 던졌다 (브라우저 실측으로만 드러남)

**정적 검사 15종이 전건 초록인 채 살아 있던 결함.** `page()` 가 모든 페이지에 `FACET_JS`(카드
필터)를 싣는데, 색인 페이지에는 `SEARCH_JS` 가 만드는 `nav.facet#facets` 가 있고 스크립트 순서상
`FACET_JS` 가 나중에 돌아 그 nav 를 잡는다. 두 스크립트의 축 어휘가 다르다(`s`·`pl` vs
`placement`·`status`) — 남의 버튼에 자기 핸들러를 걸고 클릭마다
`TypeError: Cannot read properties of undefined (reading 'appendix')` 를 던졌다.
검색 필터 자체는 동작해서(핸들러 등록 순서) **화면만 보면 정상**이고 콘솔을 봐야 드러난다.

고침 두 겹 — `FACET_JS` 가 카드 0건이면 조기 반환 · 선택자가 자기 어휘를 지목.
정적 항체를 `#12` 에 넣었고(가드 실재 + 색인 카드 0건), 음성 프로브로 뒤집힘을 확인했다.

### playwright 오류 — 원인 둘, 우회 하나

1. **프로젝트 `.mcp.json` 이 전역을 덮어쓴다** — 전역은 `--user-data-dir`·`--headless` 를 갖는데
   이 저장소 것은 `--executable-path`·`--output-dir` 뿐이다(실행 로그가 기본 프로필을 쓴 증거).
2. **Whale 은 `--remote-debugging-pipe` 에서 즉사한다** — playwright 의 기본 채널이고 전역에도
   포트 플래그가 없으므로 **1을 고쳐도 안 뜬다.**

`.mcp.json` 은 추적 파일 + 이 창의 §Files to Change 밖이라 고치지 않았다(고치면 `#1` 이 붉어지고
MCP 인자 변경은 세션 재시작을 요구한다). 대신 Whale 을 `--remote-debugging-port` 로 직접 띄우고
Node 24 의 전역 `WebSocket`·`fetch` 로 CDP 를 몰았다 — **신규 의존 0**(INV1). 재현 절차는
notes §브라우저 실측.

### `verify.sh --check` 가 두 번 거짓 통과했다 (환경)

rc 0 인데 `check PASS` 도 `golden 대조` 판정도 없었다 — Windows fork 고갈이 종료코드를 0 으로
마스킹한다. 3회차는 `template-config 게이트` 까지만 돌고 죽어 **golden 대조 자체가 안 돌았다**.
고아 프로세스(조상 체인 끊김) 30건 + 6건을 정리하고 재실행했으며, 판정은 rc 가 아니라
`check PASS` 문자열의 실재로 한다. 상세는 notes §환경 관측.

## Tests Written

| Test File | Tests | Coverage |
|---|---|---|
| `.claude/plans/insuniverse-docs-70-viewer-m3.gates.mjs` | 검사 15종 + `--selfprobe` 16쌍 | 기준선 · 어휘 유도 · 배치 파티션 · 격리 지면 · 원본 대조 · 시각 위계 · 승계 · 지배 갈림 픽스처 · 앵커 조인 · 직교 · 검색 패싯 · 결정성 + fail-closed 4종 · PRD 전이 · 앞 창 귀속 |

음성 프로브 15건은 `notes` §음성 프로브 실행 기록에, 브라우저 실측 18건은 §브라우저 실측에 실행 관측으로 남겼다.

## Next Steps

- [x] `bash docs/_meta/verify/verify.sh --check` — `check PASS` + `  ok  golden 대조` 실재 확인
- [x] 커밋 후 `check-all` → **15/15** (앞 창 하네스 포함)
- [x] 브라우저 실측 (Whale CDP 직결) — 18/18 · 결함 1건 잡아 수정
- [ ] `.mcp.json` 의 playwright 항목을 전역과 맞출지 사용자 판단 (포트 채널 문제는 별개로 남는다)
- [ ] Code review via `/mccp:code-review`
- [ ] Create PR via `/mccp:pr`
