345 lines
35 KiB
Markdown
345 lines
35 KiB
Markdown
---
|
||
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):
|