# 뷰어 데이터 계약 요청

> 요청자: `insuniverse-docs-70-viewer` · 소유자: `insuniverse-docs-00-protocol`
> 근거: `.claude/meta/14-doc-viewer-meta-analysis.md` §2 · PRD `.claude/prds/insuniverse-docs-70-viewer.prd.md` §데이터 계약 요청
> 처리 시점: **`00-protocol` 마일스톤 5** (`docs/_meta/template-config.yaml` 확정). `term-requests/` 와 같은 구조 — 요청자는 기록만, 병합은 소유자.
>
> **왜 지금인가**: 아래 #2 는 `lint-docs.mjs` 의 config 파서 수정을 동반한다. **M5 이전이면 비용 0, 이후면 lint 수정 + 전 문서 재검증.**
> 뷰어 자체의 실행은 착수 게이트까지 보류되지만 이 요청은 보류할 수 없다.

---

## 1. `repos[].url_template` — 앵커를 URL 로 만드는 규칙

**요청**: `template-config.yaml` 또는 `extraction-manifest.yaml` 의 `repos[]` 에 원격 호스트 기반 URL 템플릿을 추가.

```yaml
repos:
  - name: insuniverse-server
    url_template: "<원격 base>/blob/{sha}/{path}"     # {sha} 는 manifest 의 head, {path} 는 앵커의 저장소 이후 경로
```

**사유**: 앵커는 현재 `insuniverse-server/apps/api/src/.../x.guard.ts#ClassName` 문자열이다. 저장소가 서브모듈이고 SHA 는 manifest 에 동결돼 있는데 **원격 URL 이 어디에도 없어** 앵커를 클릭 가능한 링크로 만들 수 없다. 우산 제약 #4(역추적 가능)가 사람이 실제로 쓰는 형태로 성립하지 않는다.

**부수 결정 필요**: 앵커에 line 번호를 넣는가. 넣으면 링크 정밀도가 오르고 rot 가 빨라진다. 심볼(`#ClassName`)만 유지하면 rot 는 느리나 링크는 파일 단위.

**미반영 시**: 뷰어 2차 마일스톤(앵커 신선도)만 차단. MVP 무영향.

---

## 2. `statuses` 를 스칼라 리스트 → 매핑으로 승격 ★ MVP 차단 항목

**요청**:

```yaml
statuses:
  active-prod:            {label: "운영 반영",     placement: body}
  conditional-enabled:    {label: "조건부 활성",   placement: body}
  prod-hotfix-unreconciled: {label: "미정합",      placement: body}
  conditional-unknown:    {label: "게이트 미확인", placement: appendix}
  staged-only:            {label: "stg 전용",      placement: appendix}
  data-dependent-unknown: {label: "데이터 의존",   placement: appendix}
  dead-code-candidate:    {label: "사장 후보",     placement: deprecated}
  deprecated-confirmed:   {label: "폐기 확정",     placement: deprecated}
```

*(label 문자열은 초안. 확정은 소유자 판단.)*

**사유**: 현재 `config.statuses` 는 이름 8개 나열뿐이고, **"본문 수록 / 부록 격리 / `99-deprecated` 로" 라는 처리 규칙은 `.claude/meta/05-final-spec.md` §3 산문 표에만 있다.** 뷰어가 상태 배지·미판정 격리 필터를 만들려면 그 표가 데이터여야 한다. 이 필터가 우산 PRD 최상위 Risk(폐기 정책 오학습, 영향 매우 높음)의 **렌더 단계 방어 수단**이다.

`severity`(정렬 순서)는 이미 있는 `status_precedence` 에서 파생 가능하므로 요청하지 않는다. 실질 추가는 `label` · `placement` 2필드.

**동반 수정**: `lint-docs.mjs` 가 `statuses` 를 평탄 리스트로 읽는다(`parseFlatYaml` 경유). 매핑 승격 시 config 파서와 status 검증 규칙을 함께 수정해야 한다.

**미반영 시**: 뷰어 MVP 표면 1(상태 배지 + 미판정 격리 필터)을 **드롭한다.** 하드코딩으로 우회하지 않는다 — 문서와 뷰어가 갈라지는 표면을 새로 만드는 것이 더 나쁘다.

---

## 3. 읽는 순서의 기계 형태

**요청**: 새 파일이나 새 키가 아니라 **한 줄 선언**이면 충분하다. `docs/00-index.md`(M7) 또는 `template-config.yaml` 에:

> 파일명의 숫자 접두(`01-`, `02-` …)와 디렉터리 숫자 접두가 읽는 순서의 정본이다.

**사유**: `05-final-spec.md` §4 의 트리는 산문이고 `docs/00-index.md` 는 사람용 마크다운이다. 뷰어 nav 는 정렬 규칙이 **계약**이어야 한다 — 관행이면 언젠가 깨진다. 이미 전 경로가 그렇게 설계돼 있으므로 추가 파일 0, 추가 작업 0.

**미반영 시**: 파일명 정렬로 대체 가능. MVP 무영향이나 계약이 아니면 조용히 깨진다.

---

## 4. `## 연혁` 의 기계 접근 경로

**요청**: 판단만 필요. 셋 중 하나.

- (a) 현행 유지 — 마크다운 자유 표. 뷰어가 표를 파싱하되 **실패를 경고로 노출**한다
- (b) 프론트매터로 승격 — 작성 부담 증가
- (c) 문서 횡단 타임라인 포기 — 문서 내 렌더만

**사유**: `## 연혁` 은 `릴리스 | 날짜 | SHA | 변경 요약` 마크다운 표다. 문서 횡단 타임라인("2026-03 에 어떤 정책들이 같이 바뀌었나")은 우산 PRD 가 명시한 문제 (c)"왜 이렇게 되어 있는가"에 직접 대응하는 유일한 뷰인데, 자유 표 파싱은 형식이 흔들리면 **조용히 실패**한다.

**부수 제안 (ADR 문헌 근거)**: 연혁의 `변경 요약` 칸에 "무엇이 바뀌었나"만 적히면 (c)는 형식적으로 해결되지 않는다. 근거 없는 결정 기록은 시간이 지나면 가치를 잃는다는 것이 ADR 문헌의 핵심 주장이다. 작성 규약에 "무엇 + 왜"를 명시하거나 5열을 추가할지 판단 필요.

**미반영 시**: 뷰어 Out of scope 확정.

---

## 5. `policies[].id` ↔ `policy-candidates.*.jsonl` 행 연결 키

**요청**: 두 방향 중 하나.

- `policies[].candidate_refs: [<jsonl 행 id>]` (문서 쪽)
- 또는 candidate 행에 `documented_in: <doc_id>#<policy id>` (인덱스 쪽)

**사유**: 우산 Success Metric #3(담당자 이해 일치)의 측정 단위는 문서 본문이 아니라 **`policy-candidates.*.jsonl` 행**이다. 그런데 문서의 정책 id 와 candidate 행을 잇는 필드가 정의돼 있지 않아, 리뷰 모드("이 정책은 어느 후보에서 왔나 / 후보 중 문서화 안 된 것은 무엇인가")를 만들 수 없고 지표 자동화가 막힌다.

**지금 확인이 필요한 것 (요청과 별개)**: 인덱스 3종(`10`·`11`·`12`)이 candidate 행에 **안정적인 id 를 부여하고 있는지.** 없으면 나중에 만들 수 없다 — 재실행 시 행 순서가 바뀌면 id 가 사라진다. 이 확인만은 M5 를 기다리지 않는 편이 안전하다.

**미반영 시**: 뷰어 MVP 무영향. 리뷰 모드 포기.

---

## 6. `config.actors` 준수 강제 + 별칭 정규화 ★ 표면 5 차단 항목

*(2026-08-26 재분석 신설. 근거: `.claude/_meta/2026-08-26-70-viewer-reanalysis.md` E4 · P5)*

**요청**: 두 가지가 함께 필요하다.

1. `## 적용 범위` 표의 actor 열이 `config.actors` 어휘를 벗어나면 lint 가 잡을 것
2. 별칭이 있다면 `config` 에 정규화 표를 둘 것

**사유**: 실측으로 표 헤더가 3형태이고 actor 어휘가 **9종**인데 `config.actors` 는 5종이다.

| 문서군 | 헤더 |
|---|---|
| actor 열 없음 6건 | `| 정책 id | server | site | admin |` |
| actor 5종 14건 | `| 정책 id | 관리자 | 영업대리점 | 기업담당자 | 일반회원 | 기업회원 | server | site | admin |` |
| `05-schedules/04-runtime-topology.md` | `| 정책 id | 비회원 | 회원 | 기업담당자 | 대리점 | 관리자 | 영업관리자 | server | site | admin |` |

config 밖 4종: `비회원` · `회원` · `대리점` · `영업관리자`. `일반회원`/`회원` 과 `영업대리점`/`대리점` 은 같은 대상으로 보이나 **그 판정은 도메인 판단이고 뷰어가 할 수 없다**.

어긋남이 게이트에 안 걸리는 이유가 있다 — **`config.actors` 를 참조하는 lint 규칙이 0건이다**(규칙 20종 전수 확인). `anchor-repo` 가 `config.repos` 를 보는 것과 대칭인 검사가 actor 축에는 없다.

**미반영 시**: 뷰어 표면 5(actor×서비스 역인덱스)를 **MVP 에서 내린다.** 실제로 내렸다 — PRD 마일스톤 4 로 강등했다. 뷰어가 자체 별칭 표를 갖는 우회는 하지 않는다: 문서와 뷰어가 갈라지는 표면을 새로 만드는 것이 더 나쁘다.

**부수 판단 필요**: 이 판정의 주체가 `00-protocol` 인가 `20-foundation-access`(actor 정의 소유) 인가.

### 2026-08-28 계수 정정과 미반영 감당 방식 (뷰어 마일스톤 4)

**계수 2건이 정정됐다.** 초판이 적은 `01-foundation/ 3건`·`05-schedules/ 3건`·`규칙 22종`은 그 시점의 모집단
값이었다. 문서군을 이름이 아니라 **헤더 형태**로 다시 세면 actor 열 없는 문서는 **6건**(`01-foundation/` 밖으로
번졌다)이고, `lint-docs.mjs` 의 `rule(` 선언은 **20종**이다. 어휘 계수 9종·config 밖 4종은 안 움직였다.
정정된 값은 요청자 재열거이며 뷰어 마일스톤 4 게이트 `#14` 가 이 절의 숫자를 자기 재열거와 매 실행 대조한다 —
손으로 고친 값은 다음 창에 또 낡기 때문이다.

**뷰어는 이 미반영을 이렇게 감당했다 — 정규화하지 않고 어휘를 파티션으로 드러낸다.** 마일스톤 4 의
역인덱스는 관측 어휘 9종에 각각 **자기 지면**을 주고, config 안 5종과 밖 4종을 두 파티션으로 갈라 계수와
함께 낸다. config 밖 지면은 그 사실과 이 계약(#6) 대기를 문면으로 이고 있다. 어떤 항목도 두 어휘로
접히지 않는 것을 게이트가 단언한다 — 별칭 한 줄이 들어오면 그 자리가 붉어진다. **뷰어가 이 계약을 대신
내리지 않는다는 것이 그 검사의 내용이다.**

덧붙여 실측 하나를 남긴다: actor 축이 덮는 항목은 **492 중 195** 이고, 덮은 175개 정책 중 **129건**은
actor 열이 전건 `—` 라 운반체는 있고 값이 0 이다. 이 계약이 반영되면 정규화가 가능해질 뿐 아니라
그 129건이 의도된 부재인지 미기입인지도 lint 가 물을 수 있게 된다.

---

## 7. `flows[].status_source` 승계의 해석 규칙

*(2026-08-26 재분석 신설. 근거: `.claude/_meta/2026-08-26-70-viewer-reanalysis.md` E5)*

**요청**: 문서로 규칙을 고정한다. 판단 3개.

- 승계원(`./00-ia.md#<id>`)이 **해석 불가**일 때 flow 의 status 는 무엇인가 — 빈 값인가, 오류인가, 최하위 등급인가
- 승계원의 status 가 그 뒤 바뀌면 flow 쪽은 **자동 추종**인가, 명시 재선언이 필요한가
- 2단 이상 승계(`A → B → C`)를 허용하는가

**사유**: `flow-doc@1` 의 `flows[]` **20건 전부**가 `status_source` 를 갖고 자기 `status` 를 다른 문서에서 물려받는다. 뷰어가 상태 배지(표면 1)를 flow 문서에 그리려면 **문서를 건너 status 를 해석**해야 하는데, 초판 PRD 의 표면 1 설계에는 그 단계가 없었다.

승계가 조용히 실패하면 배지가 **빈 채로 렌더된다** — 그것은 "상태 미상"과 구별되지 않고, 우산 최상위 Risk(폐기 정책 오학습)의 렌더 단계 방어를 통째로 무력화한다.

**소유**: `lint-docs.mjs` 의 `flow-*` 규칙군이 이미 `flow-refs` 를 갖고 있으므로 `doc-template-kit` 소관으로 본다.

**미반영 시**: 마일스톤 3(신뢰 등급 가시화)이 `flow-doc@1` 5문서에 대해 성립하지 않는다. 문서 축의 배지만 살고 절차 축은 죽는다.

---

## 8. `splitExitItem` 을 export 목록에 올릴 것

*(2026-08-28 신설. 근거: 뷰어 마일스톤 4 관측 5)*

**요청**: `.claude/templates/doc-kit/lint-docs.mjs` 의 `splitExitItem` 을 파일 끝 `export { … }` 목록에 더한다.

**사유**: `surfaces[].exits` 원소는 `<대상>[ :: <조건>]` 이고 그 구분 규칙의 정본은 이 함수 하나다 — 같은
파일의 `ia-exits` 규칙과 `--probe=relation` 이 둘 다 이 함수를 부른다. 함수 머리 주석이 그 이유를 적는다:
*"두 벌로 두면 구분자 경계가 갈려도 양쪽이 각자 초록이 된다."* 그런데 **export 목록에 이 이름이 없다.**
`parseExitToken` 은 flow 이탈 표의 `결과` 열용이라 이 자리의 함수가 아니다.

뷰어는 `parseDoc`·`tableRowsInRange`·`resolveRefItem`·`statusPlacementMap` 을 export 로 받아 쓰며 파서를 두
벌 만들지 않는다는 규약을 지킨다. 이 함수만 예외로 남아 마일스톤 4 는 **복제하되 조용하지 않게** 처리했다 —
생성기가 구분자를 자기 상수(`EDGE_SEP`)로 갖고, 게이트가 `lint-docs.mjs` 의 `splitExitItem` 선언 소스와
문자열 대조해 갈리면 이름 있는 실패를 낸다.

**미반영 시**: 그 대조가 남기는 **사각지대 하나가 그대로 남는다** — 두 정본이 *동시에* 같은 새 값으로
바뀌면 대조는 통과하고 뷰어만 옛 동작을 한다. 게이트 출력이 이 사각지대를 매 실행 문면으로 낸다.
복제 자체는 마일스톤 3 이 `APPENDIX_SECTION`·`INTACT_BUCKET` 두 상수에 쓴 것과 같은 규약이라 새로운
부채는 아니지만, 이 축은 **동작 규칙**이라 상수보다 갈렸을 때의 피해가 크다.

**부수 판단 불필요**: export 목록에 이름을 더하는 것뿐이고 동작은 안 바뀐다.

---

## 처리 결과 (소유자 기입)

**2026-08-26 재분석이 실측으로 판정을 채웠다.** 요청자 쪽 표가 5건 전부 `pending` 이었는데 #2 는 이미 반영돼 있었다 — `00-protocol` 이 M5 에서 병합하며 이 표를 갱신하지 않았다. 아래 판정은 **소유자 기입이 아니라 요청자 실측**이며, 소유자 확인 시 그대로 승계하거나 정정한다.

| # | 요청 | 판정 | 반영 위치 | 일자 |
|---|---|---|---|---|
| 1 | `repos[].url_template` | **미반영** | `template-config.yaml` `repos:` 는 스칼라 리스트 한 줄 | 2026-08-26 실측 |
| 2 | `statuses` 매핑 승격 | **반영** | `docs/_meta/template-config.yaml` `statuses` — `id`·`label`·`placement` 3필드 맵 8종 | M5 |
| 3 | 읽는 순서 계약 선언 | **미반영** | config 에 `sort`·`order`·`nav` 키 0건 | 2026-08-26 실측 |
| 4 | `## 연혁` 접근 경로 | **미반영 · 요청 형태 정정 필요** | 헤더는 `lint-docs.mjs` `HISTORY_HEADER` 로 이미 동결. 실제 결손은 **항목 id 열** | 2026-08-26 실측 |
| 5 | candidate 연결 키 | **부분** — 조인 키 미반영, **안정 id 는 확인됨** | 3축 전부 내용 파생 id. 8,120행 중 문서 정책 60건과 17건 직접 일치 | 2026-08-26 실측 |
| 6 | `config.actors` 준수 강제 | pending | — | — |
| 7 | `status_source` 승계 규칙 | pending | — | — |
| 8 | `splitExitItem` export | pending | — | — |

### #4 의 요청 형태 정정

초판은 사유를 *"`## 연혁` 이 프론트매터 밖 자유 표라 파싱이 취약"* 으로 적었다. **그 전제가 틀렸다** — `lint-docs.mjs` 의 `HISTORY_HEADER` 상수가 `릴리스 | 날짜 | SHA | 변경 요약` 4열을 동결하고 `history-columns` 규칙이 강제한다. 형식은 취약하지 않다.

**실제 결손은 항목 id 열의 부재다.** 연혁은 **문서 단위이지 정책 단위가 아니다.** 그래서 잃는 것이 문서 횡단 타임라인만이 아니라 **항목 1건의 변경 시점 전부**다. 초판 Success Metric 2 의 (c)"변경 시점"이 현행 데이터로 도달 불가인 이유이며, PRD 는 그 항을 "연결된 항목(`exits`·`refs`)"으로 교체했다.

### #5 의 선행 확인 항목 종결

초판이 *"인덱스 3종이 candidate 행에 안정적 id 를 부여하고 있는지는 **지금 확인해야 한다** — 없으면 나중에 만들 수 없다"* 로 남긴 항목이다. **확인 완료 — 있다.**

| 축 | id 형태 | 안정성 |
|---|---|---|
| `policy-candidates.code.jsonl` | `policy_candidates:<심볼>` | 심볼 파생. 안정 |
| `policy-candidates.git.jsonl` | `<repo>:comment:<path>:<line>-<line>` | 경로+행 파생. **행 번호를 담아 rot 에 취약** |
| `policy-candidates.runtime.jsonl` | `<축>:<키>` | 키 파생. 안정 |

전부 순번이 아니라 내용 파생이므로 재실행으로 사라지지 않는다. **"나중에 만들 수 없다" 우려는 해소됐다** — 조인 키는 언제든 추가할 수 있다. 다만 git 축 id 의 행 번호 결박은 별도 항목으로 남는다.

---

판정 결과는 `70-viewer` PRD §착수 게이트 재분석 절차 3 에서 읽는다. **미반영 판정도 판정이다** — 그 경우 뷰어가 해당 기능을 잘라낸다. 잘린 형태는 PRD §NOT Building 에 항목별로 적혀 있다.

재분석 정본: `.claude/_meta/2026-08-26-70-viewer-reanalysis.md` (색인 등재 · lint green).
