Files
llm-wiki/raw/branch-notes/feature-frontend-observability-logging-trace-contract.md
T
DongHyeonka d6dfda64ab docs(branch): 기존 7개 노트를 개정 결정·신규 위임에 정합
- REGISTRY-001 Summary 문자열 4곳 (additive, revision 은 1 유지).
  registry-governance 는 본문 서술 12곳의 '8개'도 함께 9개로 갱신
- OFFLINE-CACHE-001 pin @1->@2 와 Summary·적용점 (behavior-change)
- DELEG-FE-007~011 delegate 5곳 접수 완료 — 미접수 위임 0건
- runtime-schema-validation 이 FLOW-FE-EVENT-003/004 소유를 명시
- env-runtime-config 가 FE-REG-CAPABILITY registry 와 FE-GATE-033 gate
  owner 를 취득하고 claim 2건 추가
- 신규 6개 노트의 delegation pin 을 기존 @1 형식으로 정규화
2026-07-28 14:40:38 +09:00

379 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: branch / feature-frontend-observability-logging-trace-contract
source_type: branch-note
status: raw
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-012
kind: project-work-item
project: ca-skeleton-frontend-operational-contract
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-012
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TELEMETRY-001@1, DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REGISTRY-001@1]
refines: []
overrides: []
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-004]
contract_packet: 1
branch: feature-frontend-observability-logging-trace-contract
parent_branch:
related_projects: [ca-skeleton-frontend, ca-skeleton]
governing_docs: [raw/project-notes/ca-skeleton-frontend-operational-contract.md]
tags: [branch, ca-skeleton, frontend, observability, error-handling, javascript]
created: 2026-07-18
target_merge:
status_label: in-progress
contract_packet_sha256: e79f4a8ea9b3ea9f54126cdb47a1228194322b9fa883ca499d546f9ffa607e63
imports: [FE-OC-008@1, FE-OC-015@1, FE-OC-021@1, FE-OC-025@1]
---
# branch: feature-frontend-observability-logging-trace-contract
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. `/branch-spec` 로 hub 계약을 구현-준비 spec 으로 내렸다. 프론트엔드 코드가 아직 없으므로 **모든 구현 주장은 `planned`** 이며 코드 evidence 는 repository 생성 후 채운다.
<!-- section-id: branch-parent -->
## 부모 (필수)
- [[raw/project-notes/ca-skeleton-frontend-operational-contract]]
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: telemetry registry·redaction·bounded queue·sink failure test가 통과한다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TELEMETRY-001@1` | telemetry는 best-effort queue와 redaction을 사용하며 sink failure가 UI를 실패시키지 않는다 | TelemetryPort·queue·redaction·degradation 계약에 적용한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REGISTRY-001@1` | route, API operation, env, storage, error, query, telemetry, release token, capability를 9개 registry로 관리한다 | hub §5.8이 정의한 FE-REG-TELEMETRY 스키마·초기 event를 코드 registry로 구현한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
| D1 | telemetry를 best-effort non-blocking 경로로 격리한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D2 | low-cardinality allowlist와 forbidden attribute redaction을 강제한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D3 | bounded queue와 비재귀 drop reporting을 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D4 | FE-REG-TELEMETRY를 event schema의 single SSOT로 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D5 | backend 지원 여부에 따라 trace correlation을 전파하거나 local ID로 강등한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D6 | delivery transport를 adapter-owned degradation 경로로 둔다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D7 | terminal failure telemetry를 bounded safe event로 제한한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D8 | TELEMETRY_ENABLED를 composition-root kill-switch로 소비한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
없음.
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
이 브랜치는 project-wide contract `FE-OC-014` (telemetry 는 best-effort 이며 render·API success 를 차단하면 안 되고 PII·token 을 전송하면 안 됨) 와 그 owner decision [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 (telemetry = best-effort queue + redaction, sink failure 는 UI 를 실패시키지 않음) 를 **구현자가 되묻지 않아도 코드를 쓸 수 있는 수준의 spec** 으로 내린다. 핵심 불변식은 **운영 격리 (operational isolation)** — telemetry 실패가 사용자 경험(render/API critical path)과 완전히 분리된다는 것이다. 동시에 이 브랜치는 `FE-REG-TELEMETRY` registry (§5.8 event/attribute/redaction) 의 single owner 로서 hub §5.8 이 정의한 최소 스키마와 초기 event 집합을 코드 registry 로 구현하고 emit 지점을 확정하며, `FE-OC-008` (실패→telemetry rule), `FE-OC-021` (low-cardinality 성능 attribute), `FE-OC-025` (`FE-RB-004` telemetry sink failure runbook) 에 telemetry 기여 edge 를 제공한다. 등급: 전 항목 `planned` (repository 부재).
- 이슈: 없음 (repository 미생성)
- PR: 없음
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- **Best-effort 전달 불변식** — telemetry send 가 render·API critical path 를 절대 block 하지 않음, sink/queue/adapter-init 실패가 UI 를 실패시키지 않음 (`FE-OC-014`, [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.2).
- **`FE-REG-TELEMETRY` registry 파일 소유** — registry 스키마와 초기 5 event 의 *정의* 는 hub §5.8 소유이고, 본 브랜치는 그 registry 를 코드로 구현하고 emit 지점을 확정한다(§5.8, §5.1). 자유 문자열 event 금지.
- **Redaction** — low-cardinality allowlist context 만 전송, forbidden attribute(token·email·raw URL/query/body·storage value·stack) 전송 금지 (§11.1, §5.8).
- **Bounded queue + overflow 정책 + 비재귀** — bounded queue, overflow drop 정책 registry 명시, telemetry 실패를 동일 sink 로 재귀 보고하지 않음 (§11.2, §16.4).
- **Delivery degradation** — local/dev console-safe sink, prod endpoint 부재/invalid 시 telemetry 만 degrade 하고 app 계속 실행 (§11.2, §16.4).
- **`TELEMETRY_ENABLED` kill-switch 소비** — runtime flag(default `false`)가 off 일 때 telemetry 전체를 무력화하는 **소비 측 의미**와 그 단일 적용 지점 확정, `FE-RB-004` mitigation "telemetry runtime flag disable" 의 실행 가능성 보장 (§5.4, §16.4). key 선언·schema 검증 자체는 `FE-OC-004` 소유.
- **Trace correlation (telemetry 관점)** — W3C `traceparent` 가 backend contract 상 허용될 때만 전파, 미지원 시 local operation ID 로 degrade, retry 는 같은 logical operation correlation 유지하되 attempt 구분 (§11.3, §8.1).
- **기여 edge** — `FE-OC-008` 실패→telemetry rule column, `FE-OC-021` duration/attempt bucket 제공, `FE-OC-025` `FE-RB-004` recovery assertion.
### 제외 범위
> 의도적으로 제외 — 다른 owner 브랜치/외부 계약이 소유. 본 브랜치는 telemetry 관점의 consume/기여만 한다.
- **Error kind 정규화 taxonomy 자체** — `FE-OC-008` owner (frontend-error-classification-boundary branch). 본 브랜치는 `error_kind` 를 소비만 하고 정의하지 않음.
- **Render error boundary 소유·복구** — `FE-OC-015` owner (frontend-render-recovery-boundary branch). 본 브랜치는 boundary-catch 신호를 consume 해 `ui.render.failed` 를 emit 만 함.
- **Web Vitals 측정·NFR 리포트** — `FE-OC-021` owner ([[raw/branch-notes/feature-web-vitals-performance-budget-contract]]). 본 브랜치는 low-cardinality attribute bucket 만 공급.
- **`FE-RB-004` runbook 1차 소유** — `FE-OC-025` owner (frontend-operational-runbook branch). 본 브랜치는 technical escalation 이며 diagnosis evidence field 만 공급.
- **Telemetry endpoint 의 CSP `connect-src`·bundle 내 secret 금지 강제** — `FE-OC-019` owner (frontend-browser-security-boundary branch).
- **Runtime config 로딩·검증** — `FE-OC-004` owner (frontend-env-runtime-config branch). 본 브랜치는 endpoint 값을 consume 만 함(의존, §엣지·실패·의존).
- **Token lifecycle** — 외부 Keycloak / [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] (`FE-OC-010`). telemetry 는 token 을 절대 전송하지 않음.
## 근거 (필수, 최소 1개+)
| Source | 정당화하는 결정 |
|---|---|
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | 본 브랜치의 거의 모든 결정의 SSOT — FE-D021(§3.2), FE-OC-014(§2.1), telemetry 계약(§11), `FE-REG-TELEMETRY`(§5.8/§5.1), 실패 matrix telemetry column(§8.2), `FE-RB-004`(§16.4), `TELEMETRY_ENABLED` runtime key(§5.4) + boot sequence(§6.3)/config validation(§6.4)/composition root(§4.5). D1~D8 전부 이 hub 의 project decision 을 인용한다. |
| [[raw/official-docs/react-ui-library-official]] | `REACT-UI-C1` — presentation 이 React component 로 구성됨(버튼~페이지). `ui.render.failed` event 의 emit point 가 React component boundary catch 라는 D4 event catalog 항목을 간접 뒷받침. boundary 자체의 소유는 render-recovery branch(`FE-OC-015`)에 위임. |
> 참고: §11.3 trace correlation 이 언급하는 W3C `traceparent` (Trace Context) 는 실제 표준이나 `raw/official-docs/` 에 아직 아카이브되지 않았다. 따라서 사실로 인용하지 않고 hub §11.3 의 project decision(허용될 때만 전파)만 근거로 쓴다. 표준 자체를 근거로 삼아야 할 결정이 생기면 `wiki-source-summarizer` 로 먼저 아카이브한다.
## TODO
각 항목 옆 증거 등급. repository 부재 → 전부 `planned` / `needs-confirmation`.
- [ ] `FE-REG-TELEMETRY` registry (`src/contracts/telemetry.js`): hub §5.8 의 7-field 스키마와 초기 5 event 를 코드로 구현 + emit 지점 배선 — 등급: `planned`
- [ ] Redaction 강제 (allowlist projection) + forbidden-attribute scan test — 등급: `planned`
- [ ] Bounded queue + overflow drop 정책 + queue drain/memory test — 등급: `planned`
- [ ] Sink failure / degradation test (endpoint invalid → telemetry 만 degrade, app 계속) — 등급: `planned`
- [ ] Trace correlation 전파 + retry attempt 구분 test — 등급: `planned`
- [ ] `TelemetryPort` (application) + telemetry adapter + composition-root wiring — 등급: `planned`
- [ ] `TELEMETRY_ENABLED=false` → no-op port 주입 + zero-network/zero-queue 회귀 test (`FE-RB-004` mitigation 재현) — 등급: `planned`
- [ ] 구현 repository 및 검증 evidence 식별 — 등급: `needs-confirmation`
## 진행 중 메모
- `/branch-spec` 로 hub §2/§3/§5/§8/§11/§16 을 내려 D1~D7 을 확정. 모든 grounding 은 hub project decision(FE-D021 중심) — 외부 official-doc 은 react-ui(간접)만 관여. web research 0건(hub 가 충분).
- 2026-07-20 loop-back fill: coverage 감사에서 `TELEMETRY_ENABLED` kill-switch **소비 측** 메커니즘이 미결정(MISSING_CONCERN)으로 드러나 D8 + 구현 가이드 7 을 추가했다. hub §5.4 는 key 를 선언하고 §16.4 는 그 disable 을 mitigation lever 로 *요구* 하지만 소비 형태는 미명시 — 사용자 소유 브랜치가 없어 본 브랜치가 소비 owner 다(`FE-OC-004` 는 key 선언·schema 검증만 소유). 같은 pass 에서 `telemetry.delivery.dropped` 의 전달 채널(비재귀 구체화)과 `route_id`/`operation_id` producer 의존을 명시했다.
- 운영 격리(operational isolation)가 이 브랜치의 축: telemetry 는 관찰 목적이며 절대 UX 를 볼모로 잡지 않는다. 그래서 delivery guarantee 를 주장하지 않고 best-effort 로 못 박는다.
## 결정 사항
> 아래 Decision Evidence Map 의 prose mirror. 각 결정의 근거는 hub project decision.
- 2026-07-18: **Telemetry = best-effort, non-blocking** — render/API critical path 를 차단하지 않고 sink failure 가 UI 를 실패시키지 않는다. 대안(delivery-guaranteed audit channel)은 regulated audit event 가 필요할 때만 별도 계약으로 분리. 근거: [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.2.
- 2026-07-18: **Redaction 우선** — low-cardinality allowlist context 만 전송, token/PII/raw payload 는 forbidden. 근거: hub §11.1, §5.8.
- 2026-07-18: **Bounded queue + 비재귀** — overflow drop 정책을 registry 에 명시, telemetry 실패를 동일 sink 로 재귀 전송하지 않음. 근거: hub §11.2, §16.4.
- 2026-07-18: **`FE-REG-TELEMETRY` single SSOT** — 자유 문자열 event 금지, 초기 5 event 고정. 근거: hub §5.8, §5.1, FE-D018.
- 2026-07-18: **Trace correlation 은 조건부 전파** — backend contract 가 허용할 때만 traceparent 전파, 아니면 local operation ID 로 degrade. 근거: hub §11.3, §8.1.
- 2026-07-18: **Delivery transport 는 adapter-owned·degradable** — dev console sink, prod endpoint invalid 시 telemetry 만 degrade. 근거: hub §11.2, §4.2, §16.4.
- 2026-07-18: **실패→telemetry 매핑은 bounded·safe** (`FE-OC-008` 기여) — terminal 실패당 safe field 만으로 최대 1 event, abort 는 error event 미발생, `TELEMETRY_FAILURE` 재귀 금지. 근거: hub §8.2, §8.1.
- 2026-07-20: **`TELEMETRY_ENABLED` 는 composition-root 단일 지점의 kill-switch** — flag 가 `false`(hub 기본값)면 real adapter 를 **아예 구성하지 않고** no-op `TelemetryPort` 를 주입한다. queue·redaction·sink·counter 가 전혀 생성되지 않으므로 disable 은 "전송 억제"가 아니라 "경로 부재"다. flag 는 boot-time runtime config 이므로 in-session flip 은 없고, 다음 boot 에 반영된다. 근거: hub §5.4(`TELEMETRY_ENABLED` runtime·required·default `false`), §16.4 Mitigation("telemetry runtime flag disable"), §6.3 boot sequence, §4.5 composition root.
## 결정-근거 매핑
> 모든 Supporting Claim 은 hub project decision. `[[...operational-contract]]` (project link) 옆의 `FE-D###`·`§n` 은 consistency hook 상 project 링크로 안전하게 검증된다.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | Telemetry 는 best-effort — render·API critical path 를 block 하지 않고 sink failure 가 UI 를 실패시키지 않는다 (`FE-OC-014`) | product telemetry 는 best-effort default 유지. regulated audit event 처럼 delivery guarantee 가 필요하면 best-effort 와 분리된 **별도 audit channel 계약** 신설 (FE-D021 revisit trigger) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.2 | `project-decision` | repository 부재 — telemetry throw 가 render/API success 를 깨지 않음을 증명하는 non-blocking test 필요 |
| D2 | Redaction — low-cardinality allowlist context 만 전송, forbidden attribute(token·email·raw URL/query/body·storage value·stack) 전송 금지 | allowlist 가 invariant(accepted-documented-only). 신규 attribute 는 registry 추가 전 low-cardinality + non-PII 검토 통과 시에만 허용; 실패하면 forbidden 분류 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.1 §5.8 | `project-decision` | redaction 은 caller 가 아니라 transport boundary(adapter)에서 강제해야 함 — forbidden-attribute scan test 로 leakage 0 증명 필요 |
| D3 | Bounded queue + overflow drop 정책 registry 명시 + telemetry 실패 비재귀 보고 | queue 는 항상 bounded. drop 방향(oldest vs newest)은 event class 별 registry 선언값 — 미선언 시 기본 oldest-drop (§구현 가이드 4 의 `UNSUPPORTED_IMPL_DECISION`) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.2 §16.4 | `project-decision` | queue 상한 크기 미확정 — memory/queue drain test 로 bound 내 drain 증명 필요 |
| D4 | `FE-REG-TELEMETRY` 가 event/attribute/redaction 의 single SSOT; 자유 문자열 event 금지; 초기 5 event(`app.boot.failed`·`api.request.failed`·`ui.render.failed`·`release.mismatch.detected`·`telemetry.delivery.dropped`) 고정 | registry-owned 유지. code generation SSOT 채택이 FE-D018 revisit trigger. `ui.render.failed` trigger 는 React boundary catch (`REACT-UI-C1` 이 presentation=React 구성을 뒷받침) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D018 §5.8 §5.1; [[raw/official-docs/react-ui-library-official]] REACT-UI-C1 | `project-decision` | registry schema validation test(자유 문자열 event reject) 필요; event 별 required attribute 가 실제 발생 지점에서 수집 가능한지 미검증 |
| D5 | Trace correlation — W3C `traceparent` 는 backend contract 허용 시에만 전파, requestId/traceId 는 safe internal reference 로 보관, raw trace header user 미노출, 미지원 backend 는 local operation ID 로 degrade, retry 는 같은 logical operation correlation 유지하되 attempt 구분 | backend contract 가 traceparent 지원 → 전파; 미지원 → local operation ID 로 degrade | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.3 §8.1 | `conditional-default` | 전파는 backend contract 의존(외부); W3C Trace Context 표준 미아카이브 → 표준 세부는 사실 인용 불가; retry 간 correlation(같은 op, distinct attempt) test 필요 |
| D6 | Delivery transport 는 adapter-owned·degradable — local/dev console-safe sink, prod endpoint 부재/invalid 면 telemetry 만 degrade 하고 app 계속, page-hide `sendBeacon` 은 adapter decision 이며 delivery guarantee 아님 | local/dev → console sink; prod → endpoint sink; page-hide `sendBeacon` 은 optional(no guarantee) | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.2 §4.2 §16.4 | `project-decision` | endpoint invalid boot path 가 telemetry 만 degrade(app 계속)함을 증명하는 sink-failure test 필요 |
| D7 | 실패→telemetry 매핑은 bounded·safe (`FE-OC-008` 기여) — §8.2 각 terminal normalized failure 는 safe field(status group·attempt bucket·route ID)만으로 최대 1 event, abort 는 error event 미발생, `TELEMETRY_FAILURE` 는 재귀 금지 | normalized error taxonomy 는 error-classification branch(`FE-OC-008`) 소유 — 본 브랜치는 그 kind 를 consume 해 telemetry rule column 만 구현. taxonomy 가 바뀌면 매핑 갱신 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §8.2 §8.1 | `project-decision` | `error_kind` registry 소유는 error-classification boundary branch(`FE-OC-008`) — 그 registry 미확정 시 매핑 draft 상태 |
| D8 | `TELEMETRY_ENABLED` kill-switch 는 composition root 단일 지점에서 소비 — `false`(hub default)면 real adapter 미구성 + no-op `TelemetryPort` 주입(queue·redaction·sink·counter 모두 미생성), `true` 면 D6 delivery ladder 진입. flag 는 boot-time 값이므로 in-session flip 없음(다음 boot 반영), 따라서 flip 시 stranded queue 문제가 정의상 발생하지 않음. `FE-RB-004` mitigation "telemetry runtime flag disable" 은 이 경로로 실행된다 | flag `false` → no-op(관측 0, 부작용 0); `true` → 정상 경로. call-site 조건 분기(`if (telemetry)`)나 port null 주입은 채택하지 않음 — hub §4.2 상 presentation/use-case 는 `TelemetryPort` 만 참조하므로 disable 이 call site 로 새면 안 됨 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §5.4 §16.4 §6.3 §4.5 §11.2 | `project-decision` | no-op vs 미구성의 *구현 형태* 는 hub 미명시(구현 가이드 7 의 `UNSUPPORTED_IMPL_DECISION`); flag off 상태에서도 product e2e 가 동일해야 함을 증명하는 both-state test 필요 |
## 구현 가이드
> `planned` blueprint (프론트엔드 코드 부재). 경로는 hub §4.6 Planned directory blueprint + §5.1 registry owner map 에서 도출된 `planned` anchor 이며 repository 생성 시 변경될 수 있다. 3-rule (R1 Trace 필수 / R2 UNSUPPORTED_IMPL_DECISION / R3 no OUT_OF_BRANCH_SCOPE) 준수.
### 1. TelemetryPort + adapter + composition-root wiring
> **Trace**: D1 + D6 · `FE-OC-014` · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §4.2 §4.6
>
> - **UNSUPPORTED_IMPL_DECISION**: port method 표면(`record(event)` 단일 vs `record`+`flush`+`shutdown`) 과 파일명은 hub 미명시 → 최소 표면(`record` only)을 초기 default 로 제안. trade-off: 최소 표면은 오용 여지가 적으나 page-hide flush 를 adapter 내부로 숨겨야 함.
| 요소 | Planned 경로 | 책임 | MUST NOT |
|---|---|---|---|
| `TelemetryPort` (application-owned interface) | `src/application/ports/` | use-case/presentation 이 부르는 telemetry 계약 정의 | 구현·browser transport·UX 결정 |
| telemetry adapter | `src/adapters/telemetry/` | queue·redaction·sink 구현, port 구현 | navigation/UX 결정 (hub §4.2) |
| composition root | `src/bootstrap/composition-root.js` | runtime config(`TELEMETRY_ENABLED` + endpoint)로 **real adapter 또는 no-op port** 를 생성·주입 (kill-switch 단일 지점 — 7 참조) | business rule, call-site 조건 분기 |
- presentation/use-case 는 `TelemetryPort` 만 참조하고 transport 를 직접 부르지 않는다 (hub §4.2 presentation MUST NOT own telemetry transport).
- adapter 는 endpoint 값을 runtime config 에서 주입받는다 (config 로딩은 env-runtime-config branch 소유 — §엣지·실패·의존).
### 2. `FE-REG-TELEMETRY` registry
> **Trace**: D4 · `FE-REG-TELEMETRY` · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D018 §5.8 §5.1
>
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 스키마 필드와 초기 event 집합은 hub §5.8 을 그대로 채택(신규 제안 아님).
Planned 경로: `src/contracts/telemetry.js` (single owner: 본 브랜치, hub §5.1).
**registry 최소 스키마(7-field)의 owner 는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.8 이다** — "§5.8 그대로" 라고 스스로 밝혔듯 복제였으므로 걷어낸다. 요약 한 줄: event 는 `eventName`·`trigger`·`requiredAttributes`·`optionalAttributes`·`forbiddenAttributes`·`sampling`·`delivery` 를 모두 갖고, required attribute 는 low-cardinality 만 허용한다.
초기 5 event 의 **정의(trigger + required attributes)는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §5.8 소유**다. 본 절은 그 event 를 *어디서 emit 하는가* 만 정한다 — 정의를 옮겨 적으면 hub 가 attribute 를 바꿀 때 이 표가 조용히 낡는다(실제로 `attempt_count``attempt_count_bucket` rename 을 놓쳤었다).
| Event | 본 브랜치의 emit 지점 |
|---|---|
| `app.boot.failed` | boot config/release validation 실패 경로 |
| `api.request.failed` | API client 의 terminal normalized failure 반환 직전 |
| `ui.render.failed` | render recovery boundary 의 catch 핸들러 |
| `release.mismatch.detected` | release check 가 mismatch 를 확정한 지점 |
| `telemetry.delivery.dropped` | 본 브랜치 sink adapter 의 queue drop 경로 |
- 자유 문자열 event 전송 금지 (hub §5.1 ad hoc use failure). registry 미등록 event 는 build/test 에서 reject.
### 3. Redaction 강제
> **Trace**: D2 · `FE-OC-014` · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.1 §5.8
>
> - **해소됨(2026-07-21) — 근거 있는 결정**: redaction 메커니즘은 hub 가 정한다. [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §11.1 이 "이 목록은 **exhaustive default-deny allowlist**다 … 목록 밖 attribute 는 transport boundary 에서 제거된다" 로 메커니즘(default-deny allowlist projection)과 강제 지점(transport boundary)을 모두 명시했다. 본 브랜치가 고른 trade-off 가 아니므로 `UNSUPPORTED_IMPL_DECISION` 라벨을 뗀다.
- **허용/금지 attribute 어휘의 owner 는 [[raw/project-notes/ca-skeleton-frontend-operational-contract]] §11.1** 이며 exhaustive default-deny allowlist 다. 목록을 여기에 옮겨 적지 않는다 — 옮겨 적은 사본이 hub 보다 짧으면 본 절 §2 가 선언한 event(`release.mismatch.detected``active_release_id`·`mismatch_kind`, `telemetry.delivery.dropped``reason`·`queue_size_bucket`)가 transport boundary 에서 전부 제거되어 계약이 자기모순에 빠진다.
- 본 브랜치가 소유하는 것은 *강제 방법* 이다: redaction 은 adapter 의 transport boundary 에서 수행하고 caller 를 신뢰하지 않는다. forbidden-attribute scan test 가 emit payload 를 검사해 위반 시 실패(§검증).
### 4. Bounded queue + overflow + degradation ladder + 비재귀
> **Trace**: D3 + D6 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.2 §16.4
>
> - **UNSUPPORTED_IMPL_DECISION**: (a) queue 상한 크기, (b) 기본 drop 방향(oldest vs newest), (c) page-hide `sendBeacon` 사용 여부는 hub 미명시 → 초기 default 로 **oldest-drop + 유한 상한(초기 제안값, memory test 로 확정)** 제안, `sendBeacon` 은 adapter 내부 optional. trade-off: oldest-drop 은 최신 event 를 보존하나 boot 초기 event 를 잃을 수 있음.
| 단계 | 동작 | 근거 |
|---|---|---|
| 정상 | bounded queue 적재 → sink flush | §11.2 |
| overflow | drop 정책(registry 선언; 기본 oldest-drop) 적용 + `telemetry.delivery.dropped` self-metric 1건 | §11.2, §5.8 |
| sink non-2xx/network 실패 | product flow 계속, console-safe fallback(safe field 한정), 동일 sink 재귀 보고 금지 | §16.4 Containment |
| adapter init 실패 | telemetry 만 degrade, app 계속 | §11.2 |
| prod endpoint 부재/invalid | telemetry 만 degrade, app 계속 | §11.2 |
- telemetry failure 를 telemetry 로 재귀 전송하지 않는다 (hub §11.2). `telemetry.delivery.dropped` 는 self-metric 이며 sink 실패의 원인 event 를 다시 sink 로 보내지 않는다.
**`telemetry.delivery.dropped` 자체의 전달 채널** (비재귀 불변식의 구체화):
> **UNSUPPORTED_IMPL_DECISION**: hub §5.8 은 `telemetry.delivery.dropped` 의 event *shape* 만 정의하고 그 event 자신이 *어느 채널로* 나가는지는 명시하지 않는다. hub §16.4 Diagnosis evidence 가 요구하는 산출물이 event stream 이 아니라 **"dropped event count"·"queue size bucket"** 이라는 점에 근거해, 아래 counter-우선 채널을 초기 default 로 제안한다. trade-off: counter 는 drop 폭주 시에도 자기 증폭이 없고 §16.4 evidence 형태와 1:1 이지만, 개별 drop 의 시점 분포(timeline)를 잃는다.
- self-metric 은 **동일 bounded queue 에 재적재(re-enqueue)하지 않는다** — full/dead queue 로 되돌리는 것은 정의상 순환이며 overflow 를 가속한다.
- 대신 adapter 내부의 **in-process 단조 counter**(key = `reason` × `queue_size_bucket`, hub §5.8 required attribute 와 동형)로 집계하고, hub §16.4 Containment 의 console-safe fallback(safe field 한정)으로 즉시 관측 가능하게 한다.
- 이 counter 는 `FE-RB-004` diagnosis evidence 의 `dropped event count` 로 그대로 공급된다(§6 기여 edge).
- sink 가 회복되어 **정상 flush 가 성공한 이후**에 한해, 누적 counter 를 aggregated event 1건으로 승격 전송하는 것은 adapter 의 optional 결정이다 — 실패 중인 sink 로는 시도하지 않으며 delivery guarantee 로 표현하지 않는다 (hub §11.2).
- counter 자체는 sink 실패로 소실되지 않아야 하므로 queue 와 독립된 lifetime 을 가진다(document lifetime 한정, 영속화 없음 — 영속화는 storage registry owner 영역).
### 5. Trace correlation
> **Trace**: D5 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §11.3 §8.1
>
> - **UNSUPPORTED_IMPL_DECISION**: local operation ID 포맷(예: `operationId#attempt`)은 hub 미명시 → 사용자 선택. trade-off: 짧은 포맷은 로그 가독성↑ 이나 충돌 회피를 위해 request-scoped uniqueness 보장 로직 필요.
- W3C `traceparent` 는 외부 auth/backend contract 가 허용할 때만 전파 (hub §11.3).
- backend 응답의 `requestId`/`traceId` (envelope `meta`, §7.3)는 safe support reference 로 내부 state 보관 가능, user 에 raw 노출 금지.
- normalized failure shape(§8.1)의 `requestId`/`traceId` 는 optional — 존재 시 telemetry attribute 로 승격하지 않고 내부 correlation 에만 사용.
- retry(new request)는 같은 logical operation correlation 유지하되 `attempt` 로 구분 (hub §11.3, §7.2 `attempt`).
- trace propagation 미지원 backend 는 local operation ID 로 degrade.
### 6. 기여 edge (contribution, ownership 은 위임)
> **Trace**: D7 · `FE-OC-008` / `FE-OC-021` / `FE-OC-025` 기여 · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §8.2 §16.4
>
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 아래는 hub 계약 인용이며 각 owner 브랜치에 위임(R3). 본 절은 telemetry 기여 edge 만 명세.
| 기여 대상 | 본 브랜치가 제공하는 telemetry edge | Owner (위임) |
|---|---|---|
| `FE-OC-008` 실패 taxonomy | §8.2 Telemetry rule column 구현 — terminal 실패당 safe field 만으로 최대 1 event, abort 는 error event 미발생, raw URL/body 금지 | error-classification boundary branch |
| `FE-OC-021` NFR | `duration_bucket`·`attempt_count_bucket` 등 low-cardinality attribute 공급(측정·리포트는 미소유) | web-vitals-performance-budget branch |
| `FE-OC-025` runbook | `FE-RB-004` diagnosis evidence field(endpoint classification·queue size bucket·dropped count·build/release ID·redaction test result) + recovery assertion 공급, **및 Mitigation "telemetry runtime flag disable" 의 실행 경로(D8, 구현 가이드 7) 보장** | frontend-operational-runbook branch |
### 7. `TELEMETRY_ENABLED` kill-switch 소비
> **Trace**: D8 (+ D1 non-blocking / D6 degradation ladder) · `FE-OC-014` · [[raw/project-notes/ca-skeleton-frontend-operational-contract]] FE-D021 §5.4 §16.4 §6.3 §4.5 §11.2
>
> - **UNSUPPORTED_IMPL_DECISION**: hub §5.4 는 `TELEMETRY_ENABLED` 를 required runtime key(default `false`)로 *선언*하고 §16.4 는 그 disable 을 *mitigation lever* 로 *요구*하지만, 소비 형태(real adapter 미구성 + **no-op port 주입** vs port 자체를 optional/null 로 두고 call site 에서 분기)는 명시하지 않는다 → **no-op port 주입** 을 초기 default 로 제안. trade-off: no-op 은 disable 경로를 composition root 한 곳에 가두고 call site 를 flag-무지 상태로 유지하나(hub §4.2 의 "presentation 은 `TelemetryPort` 만 참조" 와 정합), no-op 객체가 항상 존재하므로 "telemetry 가 꺼져 있다"는 사실이 호출자에게 보이지 않는다(관측은 boot-time config snapshot 으로만 확인 가능).
**flag 상태별 구성 (composition root 분기 지점 1곳)**
| `TELEMETRY_ENABLED` | composition root 동작 | 생성되는 것 | 생성되지 않는 것 | 근거 |
|---|---|---|---|---|
| `false` (hub default, §5.4) | no-op `TelemetryPort` 주입 | port 표면(`record`)만 | queue · redaction projection · sink/transport · dropped counter · `TELEMETRY_ENDPOINT` 해석 | §5.4, §16.4 Mitigation |
| `true` | real telemetry adapter 구성 후 주입 | 구현 가이드 2~5 전체 경로 | — | §11.2, §4.5 |
- **no-op 의 계약**: `record(event)` 는 인자를 읽지 않고 즉시 반환하며 throw 하지 않는다(D1 non-blocking 불변식을 flag 양쪽 상태에서 동일하게 유지). 어떤 event 도 buffer 하지 않으므로 나중에 flag 가 켜져도 소급 전송되는 event 는 없다.
- **disable 은 "전송 억제"가 아니라 "경로 부재"**: queue 도 counter 도 생성되지 않으므로 §11.1 redaction 위반 표면과 §11.2 overflow 표면이 동시에 0 이 된다. `TELEMETRY_ENDPOINT` 는 hub §5.4 상 `Required: conditional` — 그 조건이 곧 `TELEMETRY_ENABLED=true` 라는 것이 본 브랜치의 소비 측 해석이며, schema 상 conditional 강제는 `FE-OC-004` 소유(§엣지·실패·의존).
- **runtime flip 가능성**: runtime config 는 hub §6.3 boot sequence 에서 `GET /config.json` (no-store) 로 **boot 시 1회** 로드된 뒤 §4.5 composition root 가 의존성을 구성한다. hub 에 config hot-reload 계약이 없으므로 **in-session flip 은 존재하지 않는다** — flag 변경은 provider 측에서 반영한 뒤 **다음 document load(boot)** 부터 적용된다.
- **flip 시 이미 queue 에 쌓인 event**: 위 결과로 정의상 문제가 발생하지 않는다. `true``false` 는 이전 session 의 queue 를 flush 하지 않고 document 와 함께 폐기하며(§11.2 best-effort — delivery guarantee 없음이므로 손실이 계약 위반이 아님), `false``true` 는 시작 시점부터의 event 만 다룬다(no-op 이 아무것도 보관하지 않았으므로 backfill 대상 없음).
- **`FE-RB-004` mitigation 충족 경로**: §16.4 Mitigation 의 "telemetry runtime flag disable" 은 ① provider 의 runtime config 에서 `TELEMETRY_ENABLED=false` 설정 → ② 이후 boot 부터 no-op 주입 → ③ sink 호출·queue 적재·drop counter 증가가 **발생 원천에서** 중단 → ④ §16.4 Containment("product flow 계속")와 Recovery assertion("product e2e unaffected")이 flag 양쪽 상태에서 동일하게 성립, 의 순서로 실행된다. 이 lever 는 sink restore 없이도 즉시 사용 가능한 격리 수단이다.
- **invalid value**: hub §5.4 failure column 은 `TELEMETRY_ENABLED` invalid 를 **boot fail** 로 규정하고 §6.4 는 "boolean parsing without truthy string ambiguity" 를 요구한다. 따라서 composition root 는 **검증된 boolean** 만 받으며 `"false"` 같은 문자열을 스스로 해석하지 않는다(파싱·거부는 `FE-OC-004`). endpoint 부재/invalid 의 **telemetry degrade**(§5.4)와 달리 flag invalid 는 degrade 가 아니라 boot fail 이라는 비대칭에 유의.
## 엣지·실패·의존
> R4 캡처. sibling 브랜치 링크는 소유 계약 `FE-OC-###` 로만 참조(consistency hook 안전).
- **실패·엣지 경로**:
- sink non-2xx/network 실패 → `TELEMETRY_FAILURE`(hub §8.2), product error 없음, console-safe/drop, 재귀 금지.
- queue overflow → 정책(기본 oldest-drop) 적용 + `telemetry.delivery.dropped` self-metric, unbounded 적재 금지 (§16.4).
- telemetry adapter init 실패 / prod endpoint 부재·invalid → telemetry 만 degrade, app 계속 (§11.2).
- redaction miss(forbidden attribute 유출) → forbidden-attribute scan test 가 build 를 실패시켜야 함 (§검증).
- page hide → `sendBeacon` best-effort, delivery guarantee 로 표현 금지 (§11.2).
- backend 가 `traceparent` 미지원 → local operation ID 로 degrade (§11.3).
- `TELEMETRY_ENABLED=false` (hub §5.4 기본값) → real adapter 미구성, no-op port 주입, network·queue·counter 전부 부재. app 은 정상 동작하며 `FE-RB-004` mitigation lever 로 사용 (구현 가이드 7).
- `TELEMETRY_ENABLED` invalid → **boot fail** (§5.4, degrade 아님). 파싱·거부는 `FE-OC-004` 소유이며 telemetry adapter 는 검증된 boolean 만 수신.
- flag `true``false` 전환 → 이전 session queue 는 flush 되지 않고 폐기 (§11.2 best-effort, delivery guarantee 없음). in-session flip 은 §6.3 boot-time config 로딩상 존재하지 않으며 다음 boot 부터 반영.
- navigation/user abort(`REQUEST_ABORTED`, §8.2) → error telemetry event 미발생(interaction-only).
- **다른 계약 의존**:
- [[raw/branch-notes/feature-frontend-env-runtime-config-contract]] (`FE-OC-004`) — `TELEMETRY_ENABLED`(boolean, required, default `false`) 와 `TELEMETRY_ENDPOINT`(conditional) 를 runtime config 로 consume. 그 registry 는 key 선언·분류·schema 검증만 소유하고 **전송·redaction·kill-switch 소비 메커니즘은 본 브랜치 소유**(D8, 구현 가이드 7). config 계약이 바뀌면 flag 해석·endpoint 해석·boot degrade 경로 영향. (hub §20 Dependency 가 본 브랜치의 유일 명시 dependency 로 이 브랜치를 지목.)
- [[raw/branch-notes/feature-routing-navigation-guard-contract]] (`FE-OC-005`) — `route_id`**producer**. `FE-REG-ROUTE` 가 low-cardinality route ID 를 발급하며, telemetry 는 `ui.render.failed`·`api.request.failed` 의 required attribute 로 그 값을 그대로 소비한다(직접 생성·정규화 금지). route ID 어휘가 바뀌면 event attribute cardinality 가 영향받음.
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`FE-OC-006`) — `operation_id`(+`route_id`·`attempt`)의 **producer**. hub §7.2 request context 가 logical request 마다 `operationId`/`routeId`/`attempt` 를 보유하므로, telemetry emit point 는 이 request context 에서 값을 읽고 `attempt``attempt_count_bucket` 만 파생한다. request context 필드가 바뀌면 emit point 수집 경로 영향.
- [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] (`FE-OC-008`) — `error_kind` 를 consume 해 `api.request.failed` 등 event 의 required attribute 채움. taxonomy 변경 시 매핑 갱신.
- [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] (`FE-OC-015`) — React boundary-catch 신호를 consume 해 `ui.render.failed` emit. boundary 소유 계약 변경 시 emit point 영향.
- [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] (`FE-OC-016`) — `build_id`·`active_release_id`·release token 을 consume 해 `app.boot.failed`·`release.mismatch.detected` attribute 채움.
- [[raw/branch-notes/feature-frontend-browser-security-boundary-contract]] (`FE-OC-019`) — telemetry endpoint 의 CSP `connect-src`·bundle 내 secret 금지를 강제(그 브랜치가 본 브랜치를 contributor 로 지목).
- [[raw/branch-notes/feature-frontend-operational-runbook-contract]] (`FE-OC-025`) — `FE-RB-004` recovery assertion(product e2e unaffected·delivery self-check·queue drains·forbidden-attribute scan pass) 소비.
- [[raw/branch-notes/feature-frontend-contract-registry-governance]] (`FE-OC-022`) — `FE-REG-TELEMETRY` 를 8-registry governance 의 single-owner/compatibility check 로 감사.
## 검증해야 할 주장
> hub 계약은 근거지만 내 프로젝트 코드의 동작을 자동 보장하지 않는다. repository 생성 후 검증. 모두 `needs-confirmation`.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| telemetry throw/sink 실패가 render·API critical path 를 깨지 않는다 (D1) | 코드 부재; non-blocking 은 wiring 방식에 의존 | throwing sink 주입 후 render + API success 유지 assert 하는 **sink failure test** (§20 Measurable completion) | `needs-confirmation` |
| forbidden attribute 가 client 를 절대 떠나지 않는다 (D2) | redaction 이 transport boundary 에서 강제되는지 코드로 확인 필요 | emit payload 를 검사하는 **forbidden-attribute scan / redaction test**; 위반 시 build 실패 | `needs-confirmation` |
| bounded queue 가 planned bound 내 drain 하고 정책대로 drop 한다 (D3) | queue 상한·drop 방향이 UNSUPPORTED_IMPL_DECISION | **memory/queue drain test** (`FE-RB-004` recovery assertion) | `needs-confirmation` |
| 자유 문자열/미등록 event 가 reject 된다 (D4) | registry enforcement 미구현 | `FE-REG-TELEMETRY` **schema validation test** | `needs-confirmation` |
| retry 간 같은 logical operation correlation 유지 + attempt 구분 (D5) | traceparent 전파는 backend contract 의존 | local operation ID + attempt 구분 **correlation test** (MSW 로 backend 유/무 traceparent 시나리오) | `needs-confirmation` |
| prod endpoint 부재/invalid 시 telemetry 만 degrade 하고 app 계속 (D6) | boot 경로에서 degrade 격리 미검증 | invalid endpoint **boot/sink-failure matrix test** | `needs-confirmation` |
| `TELEMETRY_ENABLED=false` 에서 network 요청·queue·counter 가 전혀 생성되지 않고 product e2e 가 flag `true` 와 동일하다 (D8) | no-op 주입이 composition root 한 곳에만 있는지, call site 로 새지 않는지 코드로 확인 필요 | flag off/on **both-state test** — off 상태에서 telemetry 관련 network 호출 0건 assert + `FE-RB-004` recovery assertion("product e2e unaffected") 양쪽 상태 재실행 | `needs-confirmation` |
| `telemetry.delivery.dropped` self-metric 이 실패한 queue/sink 로 재진입하지 않는다 (D3 + 구현 가이드 4) | counter 채널이 queue 와 독립 lifetime 인지 미검증 | overflow 유발 후 **비재귀 test** — queue 재적재 0건 assert + dropped counter 가 `FE-RB-004` diagnosis evidence 로 노출되는지 확인 | `needs-confirmation` |
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
| 관심사 | 상태 | owner | 심각도 | 근거 |
|---|---|---|---|---|
## 마주친 문제
- 없음 — scaffolding/spec 단계 (구현 전).
## 묶음 (이 branch에서 파생된 자료)
<!-- GENERATED: project-contract-imports:start -->
## 가져온 프로젝트 계약
| Ref | Owner | 요약 | Branch 적용 |
|---|---|---|---|
| `FE-OC-008@1` | [[raw/branch-notes/feature-frontend-error-classification-boundary-contract]] | 모든 failure는 stable frontend error kind로 MUST 정규화하고 raw body·stack을 UI에 노출하면 안 됨 | import 참조로 적용 |
| `FE-OC-015@1` | [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] | expected operational error와 render defect를 MUST 분리하고 reload loop를 금지 | import 참조로 적용 |
| `FE-OC-021@1` | [[raw/branch-notes/feature-web-vitals-performance-budget-contract]] | NFR은 device/network/cache/build context와 함께 MUST 측정 | import 참조로 적용 |
| `FE-OC-025@1` | [[raw/branch-notes/feature-frontend-operational-runbook-contract]] | boot, chunk mismatch, API degradation, telemetry failure, rollback runbook을 MUST 유지 | import 참조로 적용 |
<!-- GENERATED: project-contract-imports:end -->
### Sub-branches (세부 작업)
- 없음 — scaffolding 단계
### 오류 기록 (이 branch 작업 중 발생)
- 없음 — scaffolding 단계
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- 없음 — scaffolding 단계
### 강의 (이 작업을 위해 학습한 강의)
- 없음 — scaffolding 단계
### job-posting tie-ins (이 작업에서 파생된 글감)
- 없음 — scaffolding 단계
## 관련 일일 노트
- 없음 — scaffolding 단계
## 완료 후 정리
- PR 링크: TODO
- 리뷰 메모: TODO
- 머지 결과 / 배포 환경: TODO
- **wiki 추출 대상**: 없음 — 전 항목 `planned` (repository 부재, 추출 조건 미충족)
- **추출하지 않을 항목**: D1~D8 전부 — `planned` 등급이므로 verified 승급 및 wiki/projects 추출 전까지 제외