Files
llm-wiki/raw/branch-notes/feature-frontend-error-classification-boundary-contract.md
T

345 lines
35 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-error-classification-boundary-contract
source_type: branch-note
status: raw
id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007
kind: project-work-item
project: ca-skeleton-frontend-operational-contract
work_item: WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-007
inherits: [DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-VALIDATION-001@1]
refines: []
overrides: []
depends_on: [WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-005, WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-006]
contract_packet: 1
branch: feature-frontend-error-classification-boundary-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, error-handling, integration, javascript, api-contract]
created: 2026-07-18
target_merge:
status_label: in-progress
contract_packet_sha256: 24279f92059756981b403bc43da12c4b65488a08cc137f39a2277e0ae83dfdc7
imports: [FE-OC-006@1, FE-OC-007@1, FE-OC-009@1, FE-OC-010@1, FE-OC-011@1, FE-OC-013@1, FE-OC-014@1, FE-OC-015@1, FE-OC-016@1, FE-OC-022@1, FLOW-FE-RESP-001@1, FLOW-FE-RESP-002@1, FLOW-FE-RESP-003@1, FLOW-FE-RESP-004@1, FLOW-FE-RESP-005@1, FLOW-FE-RESP-006@1, FLOW-FE-RESP-007@1]
---
# branch: feature-frontend-error-classification-boundary-contract
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. 머지/종료 후 verified 결과는 `/ingest`로 `wiki/projects/`에 추출한다.
<!-- 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`
- **완료 조건**: normalization matrix와 raw body·stack leakage negative test가 통과한다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-VALIDATION-001@1` | boundary runtime validation은 Zod schema로 수행한다 | validation failure signal을 stable frontend error kind로 정규화한다 | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
| D1 | 모든 failure를 total function으로 단일 kind에 정규화한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D2 | normalized failure는 safe field만 보존한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D3 | FE-REG-ERROR를 error UX의 single-owner registry로 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D4 | project trigger-to-kind matrix를 구현 계약으로 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D5 | user copy는 userMessageKey로 간접화한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D6 | recovery action을 closed vocabulary로 제한한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D7 | defaultRetryable은 분류 힌트로만 사용한다 | `local` | [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | `proposed` |
| D8 | Zod validation failure를 stage별 kind로 매핑한다 | `local` | `raw/official-docs/zod-runtime-schema-validation-official.md#ZOD-VALID-C4`, `raw/official-docs/zod-runtime-schema-validation-official.md#ZOD-VALID-C5` | `proposed` |
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
없음.
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
이 브랜치는 project-wide 계약 `FE-OC-008`("모든 failure 는 stable frontend error kind 로 MUST 정규화하고 raw body·stack 을 UI 에 노출하면 안 됨")을 *구현 착수 가능한 상세 명세*로 내린다. 핵심은 두 불변식이다. (1) **Total normalization** — HTTP response·adapter exception·browser exception 어느 경로든 정확히 하나의 안정 error kind(hub §5.6 의 26-kind enum)로 정규화되고, 어떤 named branch 와도 일치하지 않으면 catch-all `UNKNOWN_FAILURE` 로 폐기되며, 정규화되지 않은 throw 가 presentation 으로 통과하는 경로는 없다(hub §8.2 total-function 문단). (2) **Redaction boundary** — normalized failure 는 §8.1 의 안전 필드 집합만 담고 raw body·token·authorization header·full URL/query·stack·storage value 는 절대 포함하지 않는다. 이 브랜치는 error 계약 registry `FE-REG-ERROR`(`src/contracts/errors.js`)의 single owner 이며(hub §5.1), 그 산출물을 세 계약에 기여한다 — `FE-OC-011`(async terminal-error state 가 registry `action` 을 소비), `FE-OC-015`(operational failure 를 normal state 로 반환해 render boundary 로 throw 하지 않는 분리 신호 제공), `FE-OC-020`(negative fixture 카탈로그). 모든 진술은 코드가 없으므로 `planned` 등급이다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- **Total normalization function** — response/adapter/browser exception → 26-kind 중 정확히 하나, 최종 catch-all `UNKNOWN_FAILURE`, presentation 으로의 un-normalized throw 금지 (hub §8.2, §5.6). 등급 `planned`.
- 이 총함수가 곧 응답 처리 순서의 마지막 단계이며 **그 stage 의 owner 는 본 branch** 다 — hub §2.1.4 Flow Stage Registry `FLOW-FE-RESP-008@1`(application model 또는 실패 신호 → 정규화된 결과 반환; 총함수이므로 미매핑 예외는 `UNKNOWN_FAILURE` 로 귀결하고 throw 를 presentation 으로 통과시키지 않는다). 이 단계의 Invariants 를 바꾸려면 본 branch 가 revision 을 올려야 한다. stage 1~7 은 남의 소유라 `imports` 로만 pin 한다.
- **`FE-REG-ERROR` registry** (`src/contracts/errors.js`) — kind/defaultRetryable/severity/userMessageKey/action/telemetryEvent/redaction 7-field row 를 26 kind 에 대해 소유 (hub §5.6, §5.1 owner map). 등급 `planned`.
- **Trigger → kind 매핑 매트릭스** — hub §8.2 의 트리거(network/timeout/abort/content-type/JSON/envelope/schema/HTTP status class/chunk/boot/release/storage/render/telemetry/query-cache/unknown) → kind 총함수 매핑 구현 명세 (hub §8.2, §8.5). 등급 `planned`.
- **Normalized failure safe-shape + redaction projection** — §8.1 필드 allowlist 만 통과, 나머지 drop (hub §8.1, §7.1 "raw response body 를 log 금지"). 이게 "raw body/stack 미노출" 절반. 등급 `planned`.
- **`action` closed vocabulary 매핑** — 각 kind → `retry`/`reauth`/`navigate`/`reload-once`/`contact-support`/`none` 중 하나 + allowed-when/MUST-NOT 제약 (hub §8.4). 등급 `planned`.
- **Negative fixtures + total-normalization matrix test + raw-body/stack leakage negative test** — §20 Measurable completion 의 두 산출물 (hub §8.5). 등급 `planned`.
### 제외 범위
> 의도적 제외. 각 항목은 소유 브랜치를 명시(§15.5 R3 OUT_OF_BRANCH_SCOPE 방지). 아래 sibling 링크는 hook quirk 회피를 위해 `FE-OC-###` 계약 ID 로만 짝지음.
- **Retry algorithm/loop**(backoff·jitter·`Retry-After`·cap) — [[raw/branch-notes/feature-api-client-response-envelope-contract]] 의 `FE-OC-009` 소유. 본 브랜치는 kind 별 `defaultRetryable` *분류 힌트*만 선언하고 실제 재시도 루프는 실행하지 않는다.
- **Schema/envelope validation 실패 신호 생성**(ZodError) — [[raw/branch-notes/feature-runtime-schema-validation-contract]] 의 `FE-OC-007` 소유. 본 브랜치는 그 실패를 *소비*해 kind 로 매핑만 한다.
- **Telemetry transport/queue/redaction sink** — [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] 의 `FE-OC-014` 소유. registry 는 `telemetryEvent` 참조와 redaction *규칙*만 선언한다.
- **Error boundary component ownership + reload-loop guard 메커니즘** — [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] 의 `FE-OC-015` 소유. 본 브랜치는 operational-vs-defect 분류 *입력*만 공급한다.
- **Async surface state 렌더링** — [[raw/branch-notes/feature-async-ui-state-contract]] 의 `FE-OC-011` 소유. 본 브랜치는 kind + action 만 공급한다.
- **Token lifecycle / 401 recovery callback state machine** — auth·api-client 소유(`FE-OC-010`/`FE-OC-006`). 본 브랜치는 401→`AUTH_REQUIRED`, 403→`FORBIDDEN`, adapter throw→`AUTH_INTEGRATION_FAILURE` *매핑*만.
## 근거 (필수, 최소 1개+)
| Source | 정당화하는 결정 |
|---|---|
| [[raw/project-notes/ca-skeleton-frontend-operational-contract]] | §8 Frontend Failure Taxonomy(§8.1 shape·§8.2 matrix·§8.3 retry order·§8.4 action·§8.5 fixture) + §5.6 error registry + §5.1 `FE-REG-ERROR` owner map — `FE-OC-008` 의 project-decision SSOT. D1·D2·D3·D4·D5·D6·D7 근거. |
| [[raw/official-docs/zod-runtime-schema-validation-official]] | `FE-D007`(boundary runtime validation = Zod). `.parse()` 실패 시 granular `ZodError` throw(`ZOD-VALID-C4``.safeParse()` discriminated union(`ZOD-VALID-C5`) 가 `SCHEMA_MISMATCH`/`ENVELOPE_MISMATCH` 매핑의 *소비 대상* 신호. D8 근거. 단 validator 소유는 sibling(`FE-OC-007`). |
## TODO
각 항목 옆에 증거 등급 표기. 현재 frontend 코드가 없으므로 전부 `planned`.
- [ ] `FE-REG-ERROR` registry `src/contracts/errors.js` — 26-kind × 7-field row 정의 (D3/D4/D5/D6) — 등급: `planned`
- [ ] `adapters/http` total normalization function — dispatch 순서 + catch-all + safe-shape projection (D1/D2/D4/D7/D8) — 등급: `planned`
- [ ] Trigger → kind 매핑 매트릭스 구현 (D4) — 등급: `planned`
- [ ] Redaction / safe-shape projection — 필드 allowlist + drop rule (D2/D5) — 등급: `planned`
- [ ] Total-normalization matrix test + raw-body/stack leakage negative test + §8.5 8종 negative fixture (D1/D2/D4) — 등급: `planned`
## 진행 중 메모
없음 — `/branch-spec` 자동 채움 단계. 코드 미착수.
## 결정 사항
> 아래 8개 결정은 Decision Evidence Map 의 prose mirror. 근거는 대부분 hub 의 project decision(§8/§5.6)이며, D8 만 외부 official-doc(Zod)이 병행 근거.
- 2026-07-19: **모든 failure 를 total function 으로 단일 kind 정규화** / 이유: presentation 이 raw exception·status 로 분기하면 계약이 깨지고 leakage 발생 / 검토한 대안: page 별 ad hoc try/catch(hub §5.1 `FE-REG-ERROR` "raw status/message 로 UI 분기" = ad hoc failure) — 배포 0회 throwaway 에서만 / 근거: hub §8.2 total-function 문단, §5.6.
- 2026-07-19: **normalized failure 는 §8.1 safe 필드 집합만; raw body·token·header·URL·stack·storage value drop** / 이유: FE-OC-008 의 "raw body/stack 미노출" 강제 / 검토한 대안: 전체 error object 전달 후 UI 에서 마스킹 — 유출 위험으로 기각 / 근거: hub §8.1, §7.1.
- 2026-07-19: **`FE-REG-ERROR` 를 error kind → 기본 UX 의 단일 owner registry 로 고정** / 이유: kind/action/userMessageKey/redaction 을 code 전역에서 재정의하면 single-owner 계약 위반 / 검토한 대안: 각 adapter 가 로컬 enum 소유 — governance 붕괴로 기각 / 근거: hub §5.6, §5.1.
- 2026-07-19: **hub §8.2 trigger→kind 매핑(31-row / 고유 kind 26종)을 구현 매트릭스로 채택** / 이유: 트리거별 정규화 결과를 명세로 고정해야 total 성 검증 가능 / 검토한 대안: 상위 status class 만 매핑하고 나머지는 generic — negative fixture 통과 불가로 기각 / 근거: hub §8.2, §8.5.
- 2026-07-19: **user copy 는 `userMessageKey` 간접화, `severity`(telemetry routing)와 분리, raw backend message 금지** / 이유: 다국어·문안 변경·PII 유출 방지 / 검토한 대안: backend `error.message` 직접 표시 — §5.6 금지 / 근거: hub §5.6.
- 2026-07-19: **`action` 은 6개 closed vocabulary 로 제한** / 이유: 무한 spinner·history loop·반복 reload 같은 UX anti-pattern 을 계약으로 차단 / 검토한 대안: 자유 문자열 action — §8.4 제약 강제 불가로 기각 / 근거: hub §8.4, §5.6.
- 2026-07-19: **`defaultRetryable` 은 분류 힌트일 뿐 재시도 결정이 아님** / 이유: 재시도 루프는 api-client 소유(method/idempotency/cap 조합), 분류는 요청을 발행하지 않음 / 검토한 대안: 분류 계층이 retryable=true 를 보고 직접 재시도 — safe/idempotency 조건 무시로 storm 위험, 기각 / 근거: hub §5.6(`defaultRetryable` override 가능), §8.3, §8.2 note.
- 2026-07-19: **schema/envelope invalid 는 runtime-schema-validation 의 ZodError 를 소비해 `SCHEMA_MISMATCH`/`ENVELOPE_MISMATCH` 로 매핑, safe issue-path count + schema ID 만 보존** / 이유: validator 소유는 sibling, 분류는 결과 계약만 소비 / 검토한 대안: 분류 계층에서 zod schema 직접 실행 — 소유 경계 위반, 기각 / 근거: Zod `ZOD-VALID-C4`/`ZOD-VALID-C5`, hub §8.2·§5.6.
## 결정-근거 매핑
> `Supporting Claims` 는 hook quirk 회피를 위해 `FE-D###` 를 hub 경로에만 붙인다(sibling branch 링크 근처에 두지 않는다).
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | 모든 failure(response·adapter·browser exception)를 total function 으로 정확히 하나의 26-kind 로 정규화; 불일치·mapper 실패 시 catch-all `UNKNOWN_FAILURE`; un-normalized throw 의 presentation 통과 금지 (`FE-OC-008`) | client SPA 가 공유 backend 계약을 소비하고 배포·라우트가 존재하는 한 이 default 유지 / ad hoc page-local try/catch 는 route 1개·외부 API 0개·배포 0회 throwaway prototype 일 때만(hub §0.4 escape) | `raw/project-notes/ca-skeleton-frontend-operational-contract.md` §8.2 total-function 문단·§5.6 enum | `project-decision` | 총함수성은 exhaustive matrix test 로만 증명 가능(§8.5) — 미구현 시 mapper 누락 경로가 leak |
| D2 | normalized failure 는 §8.1 safe 필드(kind/code/httpStatus?/retryable/operationId/attemptCount/requestId?/traceId?/userMessageKey/action/causeClass)만 담고 raw body·token·authorization header·full URL/query·stack·storage value 는 drop (`FE-OC-008`) | 모든 kind·모든 경로에서 불변(FE-OC-008 이 무조건 강제) / 예외 없음 — 예외 필요 시 FE-OC-008 자체 변경 절차(hub §3.3) | `...frontend-operational-contract.md` §8.1 shape·§7.1 "raw response body 를 log 금지"·§8.2 telemetry 열 | `project-decision` | leakage 는 negative test(직렬화 후 금지 필드 부재 assert)로만 확인 — 이게 FE-OC-008 minimum evidence |
| D3 | `FE-REG-ERROR`(`src/contracts/errors.js`)를 error kind→기본 UX 의 single-owner registry 로 고정; kind/defaultRetryable/severity/userMessageKey/action/telemetryEvent/redaction 7-field | 8-registry governance(hub `FE-D018`)가 유효한 한 registry-first / code generation SSOT 채택 시 재검토 | `...frontend-operational-contract.md` §5.6·§5.1(`FE-REG-ERROR` owner=this branch, ad hoc=raw status/message 분기); `...frontend-operational-contract.md` `FE-D018` | `project-decision` | registry snapshot·single-owner scan 강제는 `FE-OC-022` sibling 소유 — 본 브랜치는 스키마·row 만 |
| D4 | hub §8.2 trigger→kind 매핑(31-row / 고유 kind 26종)을 구현 매트릭스로 채택; 각 트리거는 정확히 하나의 kind, §8.5 8종 negative fixture 로 검증 | backend 가 structured JSON envelope + 표준 HTTP status 를 제공하는 한 유지 / backend protocol 이 근본적으로 다르면(hub 가정 C 무효) 매트릭스 재도출 | `...frontend-operational-contract.md` §8.2 matrix·§8.5 fixtures | `project-decision` | 일부 row 는 sibling 이 실패 신호를 *생성*해야 성립(schema→FE-OC-007, status/retry→FE-OC-009) — 그 계약 shape 미확정 시 매핑 재조정 |
| D5 | user copy 는 `userMessageKey` 간접화, `severity`(telemetry routing hint)와 분리, raw backend `error.message` 표시 금지 | 다국어/문안 거버넌스가 존재하는 한 항상 keyed / 대안 없음 — raw message 표시는 §5.6 이 금지 | `...frontend-operational-contract.md` §5.6(userMessageKey·severity rule) | `project-decision` | message key → 실제 copy 카탈로그 소유(i18n)는 본 브랜치 밖 — 미정 시 key 계약만 고정 |
| D6 | 각 kind 는 6개 closed action(`retry`/`reauth`/`navigate`/`reload-once`/`contact-support`/`none`) 중 하나로 매핑, §8.4 allowed-when/MUST-NOT 제약 준수 | UX 계약이 유지되는 한 closed set / product 가 새 recovery 모드를 요구하면 §8.4 확장 후 registry 갱신 | `...frontend-operational-contract.md` §8.4 vocabulary·§5.6 action field | `project-decision` | action 의 실제 UI 실행은 async-ui(`FE-OC-011`)·render-recovery(`FE-OC-015`) 소유 — 본 브랜치는 kind→action 계약만 |
| D7 | `defaultRetryable` 은 분류 힌트일 뿐 재시도 결정·루프가 아님; 분류 계층은 어떤 요청도 발행하지 않음, request context override 가능 | 재시도 정책이 api-client(`FE-OC-009`) 소유인 한 힌트-only / 대안(분류가 직접 재시도)은 method+idempotency+cap 조건을 통합 소유하도록 scope 병합 시에만 | `...frontend-operational-contract.md` §5.6(`defaultRetryable` override 가능)·§8.3 retry decision order·§8.2 note("retryable=true 는 필요조건이지 충분조건 아님") | `project-decision (delegated boundary)` | 힌트와 실제 정책이 어긋나면(backend retryable=true 지만 unsafe mutation) storm — 통합 테스트로 경계 검증 필요 |
| D8 | content-type/JSON/envelope/payload invalid 는 runtime-schema-validation 이 낸 ZodError 를 소비해 `CONTENT_TYPE_MISMATCH`/`MALFORMED_JSON`/`ENVELOPE_MISMATCH`/`SCHEMA_MISMATCH` 로 매핑, schema ID + safe issue-path count 만 보존 | `FE-D007`(Zod boundary validation)이 유효한 한 소비-매핑 / bundle budget·generated schema pipeline 이 대체안을 요구하면 재검토(hub `FE-D007` revisit) | `raw/official-docs/zod-runtime-schema-validation-official.md#ZOD-VALID-C4`, `#ZOD-VALID-C5`; `...frontend-operational-contract.md` `FE-D007`·§8.2 해당 row·§5.6 | `official-vendor-doc + project-decision` | ZodError → 어느 kind(envelope vs payload)인지는 sibling 이 어느 단계에서 던졌는지에 의존 — 처리 순서(§7.3 4~6단계) 계약 미확정 시 매핑 모호 |
## 구현 가이드
> `planned` blueprint. 경로는 hub §4.6 Planned directory blueprint + §5.1 owner map 에서 유도되므로 grounded 하지만 repository 생성 시 바뀔 수 있어 전체 `planned`. sibling 링크가 필요한 detail 은 §범위 Out of scope 로 위임했고 여기 남기지 않는다(R3).
### 1. `FE-REG-ERROR` 계약 registry (`src/contracts/errors.js`)
> **Trace**: D3 + D4 + D5 + D6 / `FE-OC-008`·`FE-REG-ERROR`·hub §5.6·§8.2·§8.4. 26-kind enum(hub §5.6) 각각에 대해 7-field row.
>
> - **UNSUPPORTED_IMPL_DECISION**: `code` 필드 포맷(§8.1 은 `code` 존재만 명시, 포맷 미규정) → `<KIND>` 접미 없는 안정 문자열 상수 채택. trade-off: kind 와 1:1 이면 code 잉여지만, backend `error.code`(§7.3)와 대응시키려면 별 축이 필요 — 초기엔 kind 파생 상수로 두고 backend code 매핑표는 추후.
> - **UNSUPPORTED_IMPL_DECISION**: `userMessageKey` 명명 스킴(§5.6 은 "key" 만 요구, 규칙 미규정) → `error.<kind_snake>.message` 제안(planned). trade-off: i18n 카탈로그 소유 밖이므로 key 계약만 고정, 실제 문안은 미정.
| Field | 규칙(hub §5.6) | 이 브랜치 명세 |
|---|---|---|
| `kind` | frontend stable enum | §5.6 26-kind enum 그대로, rename 금지 |
| `defaultRetryable` | request context override 가능 | boolean 기본값; 실제 재시도는 D7 대로 미실행 |
| `severity` | telemetry routing hint, user copy 분리 | enum(예: `low`/`warn`/`error`) — telemetry 소비, D5 대로 copy 와 분리 |
| `userMessageKey` | raw backend message 금지 | key 상수(UNSUPPORTED_IMPL_DECISION 스킴) |
| `action` | 6-value closed set | D6 vocabulary 중 하나 |
| `telemetryEvent` | registry event 매핑 | `FE-REG-TELEMETRY` event 참조(소유는 FE-OC-014, 여기선 참조만) |
| `redaction` | cause/body/header drop rule | D2 safe-shape 와 일치하는 drop rule id |
### 2. Total normalization 함수 (`adapters/http` error mapper)
> **Trace**: D1 + D2 + D4 + D7 + D8 / `FE-OC-008`·hub §8.2·§8.1·§7.3(처리 순서 4~8단계). `adapters/http` 가 "envelope/schema/error mapping" 을 소유(hub §4.2).
>
> - **UNSUPPORTED_IMPL_DECISION**: 정규화 함수 파일/심볼명(hub 는 `adapters/http/` 폴더와 `src/contracts/errors.js` registry 만 grounding, 함수명 미규정) → `adapters/http/normalize-failure.js` 단일 export 제안. trade-off: 이름은 임의지만 "단일 진입 + adapters/http 내부" 두 제약만 지키면 계약 동등.
> - **UNSUPPORTED_IMPL_DECISION**: dispatch 메커니즘(§8.2 는 총함수·catch-all 만 요구, switch vs lookup table 미규정) → 트리거 판별 → kind lookup 순서 dispatch 제안. trade-off: lookup table 은 registry 대조가 쉽고 switch 는 분기 명시적 — 총함수성만 test 로 보장하면 무관.
> - **UNSUPPORTED_IMPL_DECISION**: `causeClass` internal allowlist 실제 값(§8.1 "internal allowlist only" 만, 목록 미열거) → 초기 allowlist(예: `network`/`parse`/`schema`/`auth`/`http-status`/`browser-storage`/`render`/`unknown`) 제안(planned). trade-off: allowlist 밖 값은 `unknown` 으로 접어 leak 방지, 세분화는 telemetry 요구에 따라 확장.
처리 순서(§7.3 4~8단계 하류에서 호출됨, 요청 발행 없음):
```text
input = { transportOutcome | thrownValue, requestContext }
1. aborted(navigation/user/superseded) 이면 REQUEST_ABORTED
2. network-level opaque 실패면 NETWORK_UNREACHABLE / timeout 이면 REQUEST_TIMEOUT
3. content-type/JSON/envelope/payload 실패 신호(sibling 생성)면 D8 매핑
4. HTTP status class 면 §8.2 status row 매핑(auth/authz/not-found/conflict/validation/rate/server/generic)
5. chunk/boot/release/deploy/storage/render/telemetry/query-cache 트리거면 해당 kind
6. 위 어디에도 안 맞거나 mapper 자체 throw 면 UNKNOWN_FAILURE(catch-all)
7. 매핑 결과를 §3 safe-shape 로 projection 후 반환 (raw value 폐기)
```
### 3. Trigger → kind 매핑 매트릭스
> **Trace**: D4 + D8 / hub §8.2 (31-row / 고유 kind 26종 — row 기준으로 세면 같은 kind 로 매핑되는 status row 5개가 누락된다)·§8.5. 아래는 hub §8.2 를 이 브랜치의 in-scope(=여기서 정규화 산출) 관점으로 재기술한 것이며, "생성 소유"가 sibling 인 트리거는 *소비*만 표시(값 재정의 아님).
>
> - **UNSUPPORTED_IMPL_DECISION**: 없음 — 모든 row 는 hub §8.2 가 trigger·kind·retry·fallback·UX·telemetry 를 직접 grounding.
| 트리거 그룹(§8.2) | 산출 kind | 생성 소유 | 본 브랜치 역할 |
|---|---|---|---|
| network opaque / total timeout / abort | `NETWORK_UNREACHABLE`·`REQUEST_TIMEOUT`·`REQUEST_ABORTED` | api-client transport(`FE-OC-006`) | 소비→정규화 |
| content-type/JSON/envelope/payload invalid | `CONTENT_TYPE_MISMATCH`·`MALFORMED_JSON`·`ENVELOPE_MISMATCH`·`SCHEMA_MISMATCH` | schema-validation(`FE-OC-007`) | 소비→정규화(D8) |
| 401/403/404/409/422/other-4xx/429/5xx | `AUTH_REQUIRED`·`FORBIDDEN`·`NOT_FOUND`·`CONFLICT`·`VALIDATION_REJECTED`·`UNKNOWN_CLIENT_FAILURE`·`RATE_LIMITED`·`SERVER_FAILURE` | api-client status(`FE-OC-006`) | 소비→정규화, `defaultRetryable` 힌트만(D7) |
| auth attach/recovery adapter 실패 | `AUTH_INTEGRATION_FAILURE` | auth/api-client(`FE-OC-010`) | 소비→정규화 |
| chunk/boot/release/deploy | `CHUNK_LOAD_FAILURE`·`BOOT_CONFIG_FAILURE`·`RELEASE_MANIFEST_FAILURE`·`DEPLOY_MISMATCH` | bootstrap/release(`FE-OC-015`/`FE-OC-016`) | 소비→정규화 |
| storage unavailable/quota | `STORAGE_UNAVAILABLE`·`STORAGE_QUOTA_EXCEEDED` | storage(`FE-OC-013`) | 소비→정규화 |
| render throw / telemetry fail / query-cache fail / unknown | `RENDER_FAILURE`·`TELEMETRY_FAILURE`·`QUERY_CACHE_FAILURE`·`UNKNOWN_FAILURE` | 각 owner / catch-all | 소비→정규화, 최종 catch-all 소유 |
### 4. Redaction & safe-shape projection
> **Trace**: D2 + D5 / hub §8.1·§7.1·§8.2 telemetry 열. 정규화 함수 마지막 단계(§2 step 7).
>
> - **UNSUPPORTED_IMPL_DECISION**: projection 구현 방식(§8.1 은 필드 집합만, allowlist-copy vs blocklist-delete 미규정) → **allowlist-copy**(안전 필드만 새 객체로 복사) 제안. trade-off: blocklist-delete 는 신규 raw 필드 추가 시 leak 위험 — allowlist 가 fail-closed 이므로 채택.
- **통과 허용(allowlist)**: §8.1 필드 집합 그대로.
- **항상 drop**: raw response body, token, authorization header, full URL/query, stack, storage value(§8.1) + backend raw `error.message`(D5, §5.6).
- **telemetry projection**: §8.2 telemetry 열의 kind별 safe 항목만(예: status group·attempts·elapsed bucket·schema ID·safe issue-path count) — raw URL·body·principal·token 금지. 실제 전송은 `FE-OC-014` 소유(여기선 payload 계약만).
### 5. test 카탈로그 (§20 Measurable completion)
> **Trace**: D1 + D2 + D4 / hub §8.5·§20("total normalization matrix + raw body/stack leakage negative tests"). `FE-OC-020` 기여.
>
> - **UNSUPPORTED_IMPL_DECISION**: test 파일 경로·러너별 배치(hub §4.6 은 `tests/unit|component|...` 폴더만) → `tests/unit/error-classification/*` 배치 제안. trade-off: 경로 임의, "unit 레벨 + registry/mapper 대상" 계약만 유지.
| Fixture(§8.5) | 기대 정규화 결과 |
|---|---|
| JSON operation + `text/html` response | `CONTENT_TYPE_MISMATCH` |
| auth attach callback throw/reject | `AUTH_INTEGRATION_FAILURE` |
| bounded recovery invalid state | `AUTH_INTEGRATION_FAILURE` |
| release manifest network/parse/schema 실패 | `RELEASE_MANIFEST_FAILURE` |
| QueryCachePort adapter throw / invalid result | `QUERY_CACHE_FAILURE` |
| unregistered `418`/기타 unmapped 4xx | `UNKNOWN_CLIENT_FAILURE` |
| thrown non-`Error` / symbol / mapper exception | `UNKNOWN_FAILURE` |
| **총함수 matrix test**(추가) | 26-kind 전체 트리거 exhaustive → 정확히 1 kind |
| **leakage negative test**(추가) | 정규화 결과 직렬화 후 body/token/header/URL/stack/storage value 부재 assert |
## 엣지·실패·의존
- **실패·엣지 경로**:
- *Mapper 자체 throw* → 최종 catch-all 이 raw value 폐기 후 `UNKNOWN_FAILURE` 반환(§8.2 total-function 문단). 정규화 실패로 인한 un-normalized throw 는 계약상 존재 불가.
- *Unmapped 4xx*(예: `418`) → `UNKNOWN_CLIENT_FAILURE`; *unmapped thrown value*(non-Error/symbol) → `UNKNOWN_FAILURE`(§8.5).
- *이미 정규화된 failure 재진입* → 재정규화는 idempotent 여야 함(같은 kind 유지) — Claims To Verify 로 승격.
- *registry 미등록 kind 사용* → registry 가 closed enum 이므로 컴파일/lint 단계 차단이 이상적(강제는 `FE-OC-022` governance sibling).
- *`defaultRetryable=true` 이지만 unsafe mutation* → 분류는 힌트만 노출, 재시도 미실행(D7). 실제 안전성은 api-client 가 method/idempotency/cap 으로 최종 판단.
- **다른 계약 의존**(hook quirk 회피: sibling 링크는 `FE-OC-###` 로만 짝지음):
- [[raw/branch-notes/feature-api-client-response-envelope-contract]] (`FE-OC-006`/`FE-OC-009`) — transport outcome·HTTP status·retry 정책을 *생성/소유*. 그 계약(§7.3 처리 순서, §7.4 timeout/abort) 이 바뀌면 본 브랜치 트리거→kind 매핑 재조정 필요.
- [[raw/branch-notes/feature-runtime-schema-validation-contract]] (`FE-OC-007`) — content-type/JSON/envelope/payload 검증 실패(ZodError)를 *생성*. 어느 단계에서 던지는지가 envelope vs payload kind 를 결정(D8) — 계약 변경 시 매핑 영향.
- [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] (`FE-OC-014`) — `telemetryEvent`·redaction sink 를 *소비*. registry 의 telemetry payload 계약이 그 소유와 정합해야 함.
- [[raw/branch-notes/feature-async-ui-state-contract]] (`FE-OC-011`) 와 [[raw/branch-notes/feature-frontend-render-recovery-boundary-contract]] (`FE-OC-015`) — normalized kind + action 을 *소비*(terminal-error state·operational-vs-defect 분리). 본 브랜치 산출이 이들 입력.
- [[raw/branch-notes/feature-frontend-test-taxonomy-contract]] (`FE-OC-020`) — negative fixture 를 gate 로 *소비*.
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| 정규화가 진짜 total 이다 — 어떤 경로도 un-normalized 로 presentation 도달 안 함 | 코드 미존재, mapper 누락 분기 가능 | exhaustive trigger matrix test + mapper-throws fixture → `UNKNOWN_FAILURE` (§8.5) | `needs-confirmation` |
| normalized failure 에 raw body/stack/token/header/URL/storage value 가 유출되지 않는다 | allowlist projection 미구현 | leakage negative test — 결과 직렬화 후 금지 필드 부재 assert (FE-OC-008 minimum evidence) | `needs-confirmation` |
| 26-kind 각각 정확히 1 registry row + closed action 1개를 갖는다 | registry 미작성 | registry snapshot test + action ∈ 6-set 검증 | `needs-confirmation` |
| 분류 계층은 어떤 요청도 발행하지 않는다(재시도는 api-client 소유) | 힌트/정책 경계가 코드로 미분리 | 분류 함수 단위 test 에서 fetch/network mock 호출 0회 assert | `needs-confirmation` |
| ZodError → envelope vs payload kind 매핑이 처리 순서와 정합 | sibling 처리 단계 계약 미확정 | schema-invalid fixture(envelope-level, payload-level 각각) → `ENVELOPE_MISMATCH`/`SCHEMA_MISMATCH` | `needs-confirmation` |
| 재정규화가 idempotent 하다(이미 정규화된 failure 재진입 시 동일 kind) | 재진입 경로 미설계 | 정규화 결과를 재입력 → 동일 kind assert | `planned` |
## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
> `/coverage`가 채우는 생성물이며 손으로 유지하지 않는다.
| 관심사 | 상태 | owner | 심각도 | 근거 |
|---|---|---|---|---|
| TODO — `/coverage` 실행 전 | missing | (없음) | 미평가 | TODO |
## 마주친 문제
없음 — scaffolding 단계
## 묶음 (이 branch에서 파생된 자료)
<!-- GENERATED: flow:start -->
### 가져온 흐름 단계
| Stage Ref | Order | Owner | Input | Action | Output |
|---|---:|---|---|---|---|
| `FLOW-FE-RESP-001@1` | 1 | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | HTTP 요청 | transport 완료 대기 | raw Response |
| `FLOW-FE-RESP-002@1` | 2 | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | raw Response | content-type 기대값 검사 | 본문 판독 가능 Response |
| `FLOW-FE-RESP-003@1` | 3 | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | 본문 판독 가능 Response | JSON parse | unvalidated JSON |
| `FLOW-FE-RESP-004@1` | 4 | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | unvalidated JSON | envelope 공유 스키마 검증 | discriminated envelope |
| `FLOW-FE-RESP-005@1` | 5 | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | discriminated envelope | success/failure 분기 검증 | 분기 확정 envelope |
| `FLOW-FE-RESP-006@1` | 6 | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | 분기 확정 envelope | payload per-operation 스키마 검증 | 검증된 payload(deep clone) |
| `FLOW-FE-RESP-007@1` | 7 | [[raw/branch-notes/feature-boundary-mapper-viewmodel-contract]] | 검증된 payload | DTO → application model 매핑 | application model |
<!-- GENERATED: flow:end -->
<!-- GENERATED: project-contract-imports:start -->
## 가져온 프로젝트 계약
| Ref | Owner | 요약 | Branch 적용 |
|---|---|---|---|
| `FE-OC-006@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | 모든 HTTP는 shared client를 MUST 통과하고 timeout·abort·response parsing을 page에서 구현하면 안 됨 | import 참조로 적용 |
| `FE-OC-007@1` | [[raw/branch-notes/feature-runtime-schema-validation-contract]] | JSON envelope와 payload는 boundary에서 runtime schema를 MUST 통과 | import 참조로 적용 |
| `FE-OC-009@1` | [[raw/branch-notes/feature-api-client-response-envelope-contract]] | retry는 safe/idempotent request에 한정하고 cap·jitter·`Retry-After`를 MUST 적용 | import 참조로 적용 |
| `FE-OC-010@1` | [[raw/branch-notes/feature-frontend-auth-session-integration-contract]] | skeleton은 session state를 소비하되 token lifecycle을 MUST 소유하지 않음 | import 참조로 적용 |
| `FE-OC-011@1` | [[raw/branch-notes/feature-async-ui-state-contract]] | async surface는 initial-loading, success, empty, terminal-error를 MUST 표현 | import 참조로 적용 |
| `FE-OC-013@1` | [[raw/branch-notes/feature-frontend-storage-registry-contract]] | storage key는 namespace·version·classification을 MUST 가지며 token/secret 저장을 금지 | import 참조로 적용 |
| `FE-OC-014@1` | [[raw/branch-notes/feature-frontend-observability-logging-trace-contract]] | telemetry는 best-effort이며 render·API success를 차단하면 안 되고 PII·token을 전송하면 안 됨 | 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-016@1` | [[raw/branch-notes/feature-frontend-release-cache-rollback-contract]] | HTML, asset, runtime config, release manifest cache policy를 MUST 구분 | import 참조로 적용 |
| `FE-OC-022@1` | [[raw/branch-notes/feature-frontend-contract-registry-governance]] | 8개 registry는 single primary owner와 compatibility impact를 MUST 기록 | import 참조로 적용 |
<!-- GENERATED: project-contract-imports:end -->
### Sub-branches (세부 작업)
없음 — scaffolding 단계
### 오류 기록 (이 branch 작업 중 발생)
없음 — scaffolding 단계
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
없음 — scaffolding 단계
### 강의 (이 작업을 위해 학습한 강의)
없음 — scaffolding 단계
### job-posting tie-ins (이 작업에서 파생된 글감)
없음 — scaffolding 단계
## 관련 일일 노트
없음 — scaffolding 단계
## 완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출):
- `actually-implemented` 항목:
- `locally-verified` 항목:
- `prod-verified` 항목:
- **추출하지 않을 항목** (planned / documented-only / abandoned):