Files
llm-wiki/docs/superpowers/specs/2026-07-28-ca-skeleton-frontend-runtime-adapter-features-design.md
DongHyeonka d19670975b docs(spec): 실행 결과 반영 — 정정 3건과 검증 기록
- §12.1 #42: §21 표와 frontmatter imports 에 FE-OC-027~032 포함.
  둘 다 branch-owned 계약을 hub 가 pin 하는 자리다
- §12.3: registry 개수 서술이 Summary 밖에도 14곳 있었음
- §13: contract_packet_sha256 생성기 부재 — 신규 노트는 필드 미포함,
  기존 7개는 해시가 낡음
- §15 실행 기록 신설
2026-07-28 14:43:00 +09:00

929 lines
81 KiB
Markdown
Raw Permalink 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.
# CA Skeleton Frontend — 런타임 adapter feature 확장 설계
**일자 / Date:** 2026-07-28
**대상 문서 / Target:** `raw/project-notes/ca-skeleton-frontend-operational-contract.md`
**범위 / Scope:** 신규 능력 6개 도메인을 port·adapter·registry·gate 계층까지 계약화. use case·domain model·business rule 은 명시적 제외.
**요청 언어 / User language:** ko
**상태 / Status:** 설계 승인 완료 (2026-07-28), 구현 계획 미작성
---
## 0. 이 문서의 지위
이 문서는 `raw/project-notes/ca-skeleton-frontend-operational-contract.md`(이하 **hub**)에 무엇을 어떻게 추가할지 고정한 설계 스펙이다. hub 본문의 대체물이 아니며, 여기서 확정한 ID·문자열·수치를 hub 와 신규 branch-note 가 그대로 옮겨 담는다.
이 문서 자체는 구현 증거가 아니다. hub §0.2 의 증거 등급 경계가 그대로 적용되어, 여기 적힌 모든 계약·기본값·경로는 `planned` 또는 `documented-only` 다.
---
## 1. 배경과 문제
### 1.1 요청
사용자 요청은 hub 에 다음 능력을 "구현할 수 있게" 추가하되 **런타임(adapter)까지**이고 use case·domain 이 필요한 부분까지는 아니라는 것이다.
1. File, Blob, 파일 선택기, 다운로드, IndexedDB, OPFS, Cache Storage
2. TanStack Query 메모리 캐시, Local/Session Storage, IndexedDB, 탭 간 무효화
3. Presigned URL, Multipart/Resumable Upload, Streaming Download, Image CDN
4. REST·GraphQL·gRPC-Web API, Schema, Mapper, Server State Cache
5. SSE, WebSocket, WebPush, 제한된 Polling
6. gRPC-Web·Connect-Web·Protobuf 또는 REST Gateway
7. Fetch HTTP Client, Runtime Config, Router, Query/Mutation
8. Connection, Reconnect, Resume, Event Validation, Subscription Cleanup
9. Web Worker, Service Worker, Background Sync
### 1.2 기존 계약과의 대조
요청 9묶음을 hub 의 26개 `FE-OC-*` 와 8개 registry 에 대조하면 상당 부분이 이미 계약되어 있다.
| 요청 묶음 | 기존 계약 |
|---|---|
| 7 (Fetch HTTP Client · Runtime Config · Router · Query/Mutation) | `FE-OC-004`·`FE-OC-005`·`FE-OC-006`·`FE-OC-012`**전부 존재** |
| 4 중 REST API · Schema · Mapper · Server State Cache | `FE-OC-006`·`FE-OC-007`·`FE-OC-012` + `feature-boundary-mapper-viewmodel-contract`**존재** |
| 2 중 Local/Session Storage · IndexedDB | `FE-REG-STORAGE.backend` enum 에 세 값 **존재**. OPFS·Cache Storage 는 부재 |
| 9 중 Service Worker | `FE-D019` / `DEC-…-OFFLINE-CACHE-001`**default off** 로 고정 |
따라서 순수 델타는 9개가 아니라 **6개 능력 도메인**이다. 나머지는 기존 계약의 additive 확장으로 처리한다.
### 1.3 델타가 만드는 구체적 실패
이 확장이 없으면 발생하는 실패를 명시한다. 계약은 이 실패를 막기 위해 존재한다(hub §2.2 질문 1).
| 실패 | 설명 |
|---|---|
| Blob URL 누수 | `URL.createObjectURL` 을 컴포넌트가 직접 부르고 `revokeObjectURL` 을 빠뜨려 탭 수명 동안 메모리가 증가한다. |
| 조용한 quota fallback | IndexedDB quota 초과 시 correctness 에 영향을 주는 값(업로드 part 상태)이 memory 로 fallback 되어 새로고침에 사라진다. |
| 탭 간 stale | 탭 A 의 mutation 이 탭 B 의 캐시를 무효화하지 않아 두 탭이 서로 다른 사실을 보여준다. |
| credential 유출 | presigned URL 로 보내는 byte 전송에 session 헤더가 그대로 붙어, 제3자 스토리지 도메인에 인증 정보가 전달된다. |
| GraphQL 부분 실패 오판 | `200 OK` + `errors[]` 응답을 success 로 반환해 빈 화면이 정상처럼 보인다. |
| gRPC status 오판 | HTTP 200 + `grpc-status: 13` 을 성공으로 처리한다. |
| 재연결 폭주 | SSE/WebSocket 끊김에 상한 없는 즉시 재연결을 걸어 backend 를 증폭 공격한다. |
| 구독 누수 | 라우트 이탈 후에도 WebSocket 이 열린 채 남아 연결 수가 단조 증가한다. |
| 미검증 이벤트 | 인바운드 프레임을 스키마 검증 없이 상태에 병합해 `TypeError` 가 렌더 트리 깊은 곳에서 늦게 터진다. |
| 중복 write | Background Sync 가 idempotency key 없는 mutation 을 재생해 중복 생성한다. |
| release 훼손 | Service Worker precache 가 이전 release 자산을 붙들어 `FE-OC-016`/`FE-OC-017` 의 release coherence 를 깬다. |
---
## 2. 확정된 설계 선택 (사용자 승인)
| 선택 | 결정 | 근거 |
|---|---|---|
| 분해 단위 | **6개 능력 도메인** = `FE-OC-027`~`FE-OC-032` + branch 6개 + WI 6개 | 요청 9묶음을 그대로 쓰면 `FE-OC-004/005/006/007/012` 와 소유권이 겹쳐 `DUPLICATE_CONTRACT_OWNER` 가 된다 |
| 활성화 자세 | **port + adapter 구현, capability flag default OFF** | `FE-D019`(SW off)·`FE-D025`(제거 가능 sample fixture) 자세와 일관. 안 쓰는 프로젝트가 번들·보안 표면을 떠안지 않는다 |
| `FE-D019` 충돌 | **SW 역할 분리 후 supersede** — precaching 은 OFF 유지, push·background sync·Cache Storage 호스트는 capability opt-in | WebPush·Background Sync·Cache Storage 가 SW 없이는 동작하지 않는다 |
| registry | **`FE-REG-CAPABILITY` 하나만 신설 → 9개.** 실시간 구독은 `FE-REG-API` 의 protocol 행 | 구독을 별도 장부로 두면 같은 backend 를 REST/SSE 로 부를 때 operation 정의가 이원화된다 |
| 근거 자료 | **project-local default 로 명시** + §19.2 open question 으로 후속 조사 예약 | `FE-D001`·`FE-D010`·`FE-D011` 선례. 없는 출처를 지어내지 않는다 |
| 산출물 | hub 갱신 + branch-note 6개 스캐폴딩 | 각 branch 의 깊은 명세는 다음 단계 |
---
## 3. 범위
### 3.1 In scope
- 6개 도메인의 **port 정의**(`application` 소유), **adapter 구현 계약**, **registry 행**, **정규화 실패 어휘**, **acceptance gate**, **NFR**, **runbook**
- 기존 8개 registry 의 additive 확장
- `FE-D019` supersede 를 포함한 결정 레지스트리 갱신
- 신규 branch-note 6개 스캐폴딩과 Cluster 연결
- 기존 branch-note 7개의 pin·문자열·위임 접수 정합 수정 (§12.3)
### 3.2 Out of scope
**사용자 제약을 문서에 못박는 항목이다.** hub §0.5 Out of scope 에 그대로 추가한다.
- 6개 도메인의 **use case, domain model, business rule** — port 와 adapter 계약까지만 정의하고, 그 port 를 호출하는 use case 는 적용 프로젝트가 작성한다
- 구체 backend API 설계(presigned URL 발급 endpoint 스펙, GraphQL 스키마, protobuf 서비스 정의, push 발송 서버)
- 특정 vendor 선택(Image CDN 제공자, object storage 제공자, push 서비스)
- 파일 형식별 처리(이미지 리사이즈 알고리즘, 비디오 트랜스코딩, 문서 파서)
- 오프라인 우선(offline-first) 데이터 동기화 정책과 충돌 해결(CRDT, last-write-wins 등)
- SSR·RSC·edge rendering (hub §0.5 유지)
### 3.3 Non-goal
- 6개 도메인을 **기본 활성화**하는 것. 기본값은 전부 OFF 이며 `FE-GATE-033` 이 이를 증명한다.
- 기존 26개 `FE-OC-*` 의 의미 변경. `FE-OC-006`·`FE-OC-012`·`FE-OC-013` 은 확장되지만 normative summary 는 유지된다.
---
## 4. 도메인 분해
### 4.1 도메인 → 계약 · branch · Work Item
| 계약 | 도메인 | branch slug | Work Item |
|---|---|---|---|
| `FE-OC-027` | 바이너리/파일 I-O + 로컬 대용량 저장 | `feature-frontend-binary-file-io-store-contract` | `WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-028` |
| `FE-OC-028` | 캐시 계층 + 탭 간 무효화 | `feature-frontend-cache-tier-cross-tab-invalidation-contract` | `WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-029` |
| `FE-OC-029` | 대용량 객체 전송 | `feature-frontend-large-object-transfer-contract` | `WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-030` |
| `FE-OC-030` | 멀티프로토콜 API transport | `feature-frontend-multi-protocol-api-transport-contract` | `WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-031` |
| `FE-OC-031` | 실시간 구독 수명주기 | `feature-frontend-realtime-subscription-lifecycle-contract` | `WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-032` |
| `FE-OC-032` | 백그라운드 실행 | `feature-frontend-background-execution-worker-contract` | `WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-033` |
요청 묶음과의 대응:
| 요청 묶음 | 흡수한 계약 |
|---|---|
| 1 (File·Blob·picker·download·IndexedDB·OPFS·Cache Storage) | `FE-OC-027` |
| 2 (TanStack 메모리·Local/Session·IndexedDB·탭 간 무효화) | `FE-OC-028` (+ 기존 `FE-OC-012`·`FE-OC-013` 확장) |
| 3 (presigned·multipart/resumable·streaming download·Image CDN) | `FE-OC-029` |
| 4 (REST·GraphQL·gRPC-Web·Schema·Mapper·Server State Cache) | `FE-OC-030` (+ 기존 `FE-OC-006`·`FE-OC-007`·`FE-OC-012`) |
| 5 (SSE·WebSocket·WebPush·제한된 Polling) | `FE-OC-031` (+ WebPush 의 SW 호스트는 `FE-OC-032`) |
| 6 (gRPC-Web·Connect-Web·Protobuf·REST Gateway) | `FE-OC-030` |
| 7 (Fetch·Runtime Config·Router·Query/Mutation) | 기존 `FE-OC-004`·`005`·`006`·`012` — 신규 계약 없음 |
| 8 (Connection·Reconnect·Resume·Event Validation·Subscription Cleanup) | `FE-OC-031` |
| 9 (Web Worker·Service Worker·Background Sync) | `FE-OC-032` |
### 4.2 §2.1 Stable Contract Index 신규 행
hub §2.1 표에 그대로 추가한다.
| Contract ID | Single owner | Normative summary | Minimum evidence | Status |
| --- | --- | --- | --- | --- |
| `FE-OC-027` | `feature-frontend-binary-file-io-store-contract` | 파일 선택·다운로드·로컬 바이너리 저장은 등록된 port 를 MUST 경유하고, 원시 `File`/`Blob` handle 과 object URL 수명은 adapter 경계를 MUST NOT 벗어남 | binary I-O fixtures | `planned` |
| `FE-OC-028` | `feature-frontend-cache-tier-cross-tab-invalidation-contract` | 캐시 계층과 탭 간 무효화는 `QueryCachePort` 정책과 registry 를 MUST 경유하고, release·config·API version 이 불일치하는 영속 캐시를 MUST NOT 재사용 | cache tier + cross-tab tests | `planned` |
| `FE-OC-029` | `feature-frontend-large-object-transfer-contract` | 대용량 전송은 presigned 획득과 byte 전송의 credential 경계를 MUST 분리하고, 재개 가능 전송의 part 상태·무결성·취소를 MUST 소유 | transfer fixtures | `planned` |
| `FE-OC-030` | `feature-frontend-multi-protocol-api-transport-contract` | 모든 protocol adapter 는 동일한 application output port 를 구현하고 protocol 별 성공/실패를 정규화된 failure 로 MUST 매핑하며, transport status 만으로 성공을 판정하면 안 됨 | protocol mapping tests | `planned` |
| `FE-OC-031` | `feature-frontend-realtime-subscription-lifecycle-contract` | 실시간 구독은 연결·재연결·재개·이벤트 검증·해제를 MUST 계약하고, 스키마 미검증 이벤트를 application 으로 MUST NOT 전달하며 unmount 후 열린 구독을 MUST NOT 남김 | realtime lifecycle tests | `planned` |
| `FE-OC-032` | `feature-frontend-background-execution-worker-contract` | 백그라운드 실행은 명시 owner·update UX·idempotency 조건을 MUST 갖추고, precaching 으로 release coherence 를 MUST NOT 훼손 | background execution tests | `planned` |
### 4.3 §2.1.1 Contract Registry (typed) 신규 행
| Contract ID | Concern Key | Revision | Type | Owner | Trigger | Required Effect | Enforcement | Status |
|---|---|---|---|---|---|---|---|---|
| `FE-OC-027` | `fe.binary-file-io-store` | 1 | operational-contract | `feature-frontend-binary-file-io-store-contract` | 파일을 고르거나 내려받거나 바이너리를 로컬에 쓸 때 | 파일 선택·다운로드·로컬 바이너리 저장은 등록된 port 를 MUST 경유하고, 원시 `File`/`Blob` handle 과 object URL 수명은 adapter 경계를 MUST NOT 벗어남 | binary I-O fixtures | active |
| `FE-OC-028` | `fe.cache-tier-cross-tab` | 1 | operational-contract | `feature-frontend-cache-tier-cross-tab-invalidation-contract` | 캐시를 영속화하거나 다른 탭에 무효화를 전파할 때 | 캐시 계층과 탭 간 무효화는 `QueryCachePort` 정책과 registry 를 MUST 경유하고, release·config·API version 이 불일치하는 영속 캐시를 MUST NOT 재사용 | cache tier + cross-tab tests | active |
| `FE-OC-029` | `fe.large-object-transfer` | 1 | operational-contract | `feature-frontend-large-object-transfer-contract` | 대용량 객체를 올리거나 스트리밍으로 내려받을 때 | 대용량 전송은 presigned 획득과 byte 전송의 credential 경계를 MUST 분리하고, 재개 가능 전송의 part 상태·무결성·취소를 MUST 소유 | transfer fixtures | active |
| `FE-OC-030` | `fe.multi-protocol-transport` | 1 | operational-contract | `feature-frontend-multi-protocol-api-transport-contract` | REST 이외 protocol 로 operation 을 호출할 때 | 모든 protocol adapter 는 동일한 application output port 를 구현하고 protocol 별 성공/실패를 정규화된 failure 로 MUST 매핑하며, transport status 만으로 성공을 판정하면 안 됨 | protocol mapping tests | active |
| `FE-OC-031` | `fe.realtime-subscription-lifecycle` | 1 | operational-contract | `feature-frontend-realtime-subscription-lifecycle-contract` | 스트림을 구독하거나 해제할 때 | 실시간 구독은 연결·재연결·재개·이벤트 검증·해제를 MUST 계약하고, 스키마 미검증 이벤트를 application 으로 MUST NOT 전달하며 unmount 후 열린 구독을 MUST NOT 남김 | realtime lifecycle tests | active |
| `FE-OC-032` | `fe.background-execution` | 1 | operational-contract | `feature-frontend-background-execution-worker-contract` | worker·service worker·background sync 를 등록하거나 갱신할 때 | 백그라운드 실행은 명시 owner·update UX·idempotency 조건을 MUST 갖추고, precaching 으로 release coherence 를 MUST NOT 훼손 | background execution tests | active |
gate 행 7개는 §7.1 에 있다.
---
## 5. Port 설계
### 5.1 신규 port 12개
`FE-D010`(port 는 `application` 소유, adapter 가 구현)과 `FE-D011`(bootstrap 단일 composition root)을 그대로 따른다. hub §4.4 Port ownership matrix 에 추가한다.
| Port | Definition owner | Planned implementation | Consumer | Input / output | Failure vocabulary |
|---|---|---|---|---|---|
| `CapabilityPort` | `application` | `adapters/capability` | bootstrap + 모든 capability 소비 use case | capability ID → 활성 여부 + 사유 | `CAPABILITY_DISABLED`, `CAPABILITY_UNSUPPORTED` |
| `FileDialogPort` | `application` | `adapters/file` | 파일 입출력 use case | 선택 제약(accept·multiple·max) → 파일 descriptor 목록 / 저장 요청 → 저장 결과 | `FILE_PICKER_DISMISSED`, `FILE_REJECTED` |
| `BlobStorePort` | `application` | `adapters/blob-store` | 로컬 바이너리 보관 use case | 등록 key + 바이너리 descriptor → 저장/조회/삭제 결과 | `BLOB_STORE_UNAVAILABLE`, `BLOB_STORE_QUOTA_EXCEEDED` |
| `CachePersistencePort` | `application` | `adapters/cache-persistence` | `QueryCachePort` 구현 보조 | 캐시 스냅샷 + version tuple → 영속/복원 결과 | `CACHE_PERSISTENCE_FAILURE` |
| `CrossTabSyncPort` | `application` | `adapters/cross-tab` | 캐시 무효화 orchestration | 무효화 key 메시지 → 발행/수신 구독 | `CROSS_TAB_CHANNEL_UNAVAILABLE` |
| `UploadTransferPort` | `application` | `adapters/transfer/upload` | 업로드 use case | 전송 계획(source descriptor + presign 결과) → 진행 스트림 + 완료 결과 | `PRESIGN_EXPIRED`, `UPLOAD_PART_FAILED`, `TRANSFER_INTEGRITY_MISMATCH` |
| `StreamingDownloadPort` | `application` | `adapters/transfer/download` | 다운로드 use case | operation + range/resume 위치 → 진행 스트림 + 완료 결과 | `STREAM_INTERRUPTED` |
| `RealtimeSubscriptionPort` | `application` | `adapters/realtime/{sse,websocket,polling}` | 스트림 소비 use case | 구독 descriptor + resume cursor → 검증된 이벤트 스트림 + 해제 handle | `REALTIME_CONNECT_FAILED`, `REALTIME_DISCONNECTED`, `REALTIME_RESUME_GAP`, `EVENT_SCHEMA_MISMATCH` |
| `PushSubscriptionPort` | `application` | `adapters/realtime/push` | 알림 등록 use case | 권한 요청 + 공개키 → 구독 descriptor | `PUSH_PERMISSION_DENIED`, `PUSH_SUBSCRIPTION_EXPIRED` |
| `WorkerTaskPort` | `application` | `adapters/worker` | CPU 오프로드 use case | task 이름 + 직렬화 가능 입력 + timeout → 결과 또는 종료 | `WORKER_UNAVAILABLE`, `WORKER_TASK_TIMEOUT` |
| `ServiceWorkerHostPort` | `application` | `adapters/service-worker` | bootstrap + release 감시 | 등록 요청 → 등록/갱신 상태 스트림 | `SW_REGISTRATION_FAILED` |
| `BackgroundSyncPort` | `application` | `adapters/service-worker/sync` | 지연 mutation use case | keyed mutation descriptor → 큐 등록 결과 | `BACKGROUND_SYNC_UNSUPPORTED`, `BACKGROUND_SYNC_REPLAY_REJECTED` |
### 5.2 의도적으로 port 를 만들지 않는 두 가지
이 두 결정은 설계의 핵심이며 hub §4.4 하단에 설명 문단으로 넣는다.
**멀티프로토콜(`FE-OC-030`)은 신규 port 가 0개다.** GraphQL·gRPC-Web·Connect-Web 은 기존 `ResourceQueryPort`/`ResourceCommandPort` 의 다른 **구현체**다. 프로토콜이 application 에 새 인터페이스로 새면 `FE-D010`(dependency inversion)이 무너지고, backend 가 REST 에서 gRPC 로 옮겨갈 때 use case 를 다시 써야 한다. 프로토콜 선택은 `FE-REG-API.protocol` 필드 = **데이터**이지 타입이 아니다.
**Image CDN 은 adapter 가 아니라 `application` 의 순수 정책이다.** URL 파생에는 I-O 가 없다. `MediaUrlPolicy``application/policies/` 에 두고 CDN base·허용 transform 은 `FE-REG-ENV` 행에서 읽는다. port 로 만들면 테스트에 불필요한 test double 만 늘어난다.
### 5.3 §4.2 Component responsibility 신규 행
| Component | Owns | Consumes | MUST NOT own | Evidence status |
|---|---|---|---|---|
| `adapters/capability` | capability flag 해석, 브라우저 feature detection, 비활성 사유 | application port, runtime config, browser globals | 활성화 여부의 제품 판단 | `planned` |
| `adapters/file` | 파일 선택·저장 dialog, object URL 생성/해제 | application port, File System Access / input element | 파일 내용 해석, 도메인 검증 | `planned` |
| `adapters/blob-store` | IndexedDB·OPFS·Cache Storage 백엔드, quota 매핑, eviction | application port, browser storage API | 저장 대상의 의미, 도메인 정책 | `planned` |
| `adapters/cache-persistence` | 캐시 직렬화, version partition, 복원 거부 | application port, `adapters/blob-store` 계약이 아닌 자체 백엔드 | 캐시 정책 결정(= `QueryCachePort` 소유) | `planned` |
| `adapters/cross-tab` | BroadcastChannel·`storage` event 전송, 메시지 봉투 | application port, browser globals | 무효화 대상 결정 | `planned` |
| `adapters/transfer` | part 분할·병렬·재시도·무결성·진행 보고, credential-less 전송 | application port, fetch, Streams | presign 발급, 업로드 대상 도메인 규칙 | `planned` |
| `adapters/protocol` | GraphQL·gRPC-Web·Connect-Web codec 과 status 정규화 | application output port, fetch | operation 정의, use-case policy | `planned` |
| `adapters/realtime` | 연결 수명주기, 재연결 backoff, resume cursor, 프레임 디코드, 구독 해제 | application port, EventSource·WebSocket·fetch | 이벤트의 도메인 의미, 상태 병합 정책 | `planned` |
| `adapters/worker` | worker 생성·통신·timeout·terminate | application port, Worker API | 도메인 계산 규칙 | `planned` |
| `adapters/service-worker` | SW 등록·갱신 상태, background sync 큐 등록 | application port, ServiceWorker API | precache 정책 결정(`FE-D034` 소유), mutation 의미 | `planned` |
### 5.4 §4.3 Dependency matrix 보강
기존 `adapters/*` 행의 "다른 adapter 의 concrete implementation import 금지"를 유지하되 두 예외를 명시한다.
- `adapters/transfer``UploadTransferPort` 입력으로 **바이너리 descriptor** 를 받는다. `adapters/file`·`adapters/blob-store` 의 concrete 모듈을 import 하지 않는다. descriptor 조립은 `bootstrap` 또는 application orchestration 이 한다.
- `adapters/realtime/push``ServiceWorkerHostPort` **인터페이스**를 통해 SW 등록 상태를 읽는다. `adapters/service-worker` 의 concrete 모듈을 import 하지 않는다. 이 관계는 `DELEG-FE-007` 로 등록한다.
worker/SW 엔트리 파일 규칙을 추가한다.
- `src/workers/*.worker.js``public/sw.js` 는 **별도 실행 컨텍스트**이므로 `presentation`·`application`·`domain` 을 import 하지 않는다. 공유가 필요하면 `domain` 의 순수 모듈만 복제 없이 참조하고, 그 사실을 `FE-REG-CAPABILITY` 행에 기록한다.
- worker 엔트리는 `adapters/worker` 가 소유한다. `bootstrap` 이 직접 `new Worker()` 를 부르지 않는다.
### 5.5 §4.5 Boot order 변경
기존 10단계 사이에 capability 해석을 넣는다. 삽입 위치는 registry snapshot load 직후, adapter 생성 직전이다.
```text
1. build identity 읽기
2. runtime config fetch
3. config envelope·schema·compatibility 검증
4. release manifest 정합성 확인
5. registry snapshot load
6. capability 해석 — flag × 브라우저 feature detection → 활성 capability 집합 확정 ← 신규
7. auth integration adapter 주입
8. HTTP/storage/telemetry/query-cache adapter 생성
9. 활성 capability 의 adapter 생성 (비활성 capability 의 adapter 는 생성하지 않음) ← 신규
10. application facade 생성
11. router 생성
12. React root mount
```
규범:
- 6단계는 `MUST` 실패하지 않는다. flag ON + 브라우저 미지원이면 해당 capability 를 비활성으로 확정하고 `capability.activation.rejected` telemetry 를 남긴 뒤 boot 를 계속한다. capability 부재가 boot 를 막으면 skeleton 이 특정 브라우저에 묶인다.
- 9단계는 비활성 capability 의 adapter 모듈을 **동적 import 하지 않는다**. 정적 import 하면 `FE-NFR-020`(비활성 시 initial JS 증가 0)이 깨진다.
### 5.6 §4.6 Directory blueprint 확장
```text
src/
adapters/
capability/
file/
blob-store/
cache-persistence/
cross-tab/
transfer/
upload/
download/
protocol/
graphql/
grpc-web/
connect-web/
realtime/
sse/
websocket/
polling/
push/
worker/
service-worker/
sync/
application/
policies/ # MediaUrlPolicy 등 순수 정책
contracts/
capabilities.js # FE-REG-CAPABILITY
workers/ # *.worker.js 엔트리
public/
sw.js # capability 활성 시에만 배포 artifact 에 포함
```
---
## 6. Registry 설계
### 6.1 `FE-REG-CAPABILITY` — 9번째 registry (신설)
`FE-REG-CAPABILITY` 의 owner 는 `feature-frontend-env-runtime-config-contract` 다. capability flag 는 runtime config 키이므로 env registry owner 가 자연 owner 이고, 새 branch 를 추가로 만들지 않는다.
Planned path: `src/contracts/capabilities.js`
Ad hoc use failure: registry 없이 `import.meta.env` 나 config 객체를 직접 읽어 기능을 분기
hub 에서의 위치는 **§5.11 로 뒤에 붙인다**. §5.5 뒤에 끼워 넣으면 §5.6~§5.10 이 밀려 문서 전체와 branch-note 의 절 참조가 어긋난다. 절 번호 순서보다 기존 참조 안정성이 우선이다.
| Field | Required | Rule |
|---|---|---|
| `capabilityId` | yes | stable `UPPER_SNAKE_CASE`; rename 은 breaking |
| `envFlagKey` | yes | `FE-REG-ENV`**포인터**. 키의 타입·기본값·검증은 env registry 가 계속 소유하고 여기 재진술하지 않음 |
| `owner` | yes | 소유 branch slug |
| `requiredGate` | yes | 활성화 시 반드시 PASS 해야 하는 `FE-GATE-*` |
| `browserRequirement` | yes | feature detection 식별자. 미지원 시 활성화되지 않음 |
| `disabledFallback` | yes | 비활성 시 동작 — `feature-hidden`, `degraded-alternative`, `error-surface` 중 하나 |
| `runbookRef` | conditional | 운영 실패 절차가 있는 capability 는 `FE-RB-*` 참조 필수 |
Initial planned rows:
| capabilityId | envFlagKey | owner | requiredGate | browserRequirement | disabledFallback | runbookRef |
|---|---|---|---|---|---|---|
| `CAP_FE_BINARY_IO` | `CAPABILITY_BINARY_IO_ENABLED` | `feature-frontend-binary-file-io-store-contract` | `FE-GATE-027` | `indexedDB` (OPFS·Cache Storage 는 하위 detection) | `feature-hidden` | — |
| `CAP_FE_CACHE_PERSISTENCE` | `CAPABILITY_CACHE_PERSISTENCE_ENABLED` | `feature-frontend-cache-tier-cross-tab-invalidation-contract` | `FE-GATE-028` | `indexedDB` | `degraded-alternative` (메모리 캐시만) | — |
| `CAP_FE_LARGE_TRANSFER` | `CAPABILITY_LARGE_TRANSFER_ENABLED` | `feature-frontend-large-object-transfer-contract` | `FE-GATE-029` | `ReadableStream`, `AbortController` | `feature-hidden` | — |
| `CAP_FE_ALT_PROTOCOL` | `CAPABILITY_ALT_PROTOCOL_ENABLED` | `feature-frontend-multi-protocol-api-transport-contract` | `FE-GATE-030` | `fetch` streaming (gRPC-Web 시) | `degraded-alternative` (REST gateway) | — |
| `CAP_FE_REALTIME` | `CAPABILITY_REALTIME_ENABLED` | `feature-frontend-realtime-subscription-lifecycle-contract` | `FE-GATE-031` | `EventSource` 또는 `WebSocket` | `degraded-alternative` (bounded polling) | `FE-RB-006` |
| `CAP_FE_BACKGROUND_EXEC` | `CAPABILITY_BACKGROUND_EXEC_ENABLED` | `feature-frontend-background-execution-worker-contract` | `FE-GATE-032` | `Worker`; SW 하위 기능은 `serviceWorker` | `feature-hidden` | `FE-RB-007` |
### 6.2 기존 8개 registry 확장 (전부 additive)
#### `FE-REG-API` — §5.3 schema 확장
| Field | Required | Rule |
|---|---|---|
| `protocol` | yes | `rest`(기본) \| `graphql` \| `grpc-web` \| `connect-web` \| `sse` \| `websocket` \| `poll`. `rest` 외 값은 대응 capability 활성 필요 |
| `transferMode` | yes | `unary`(기본) \| `stream` \| `upload` \| `download` |
| `operationRef` | conditional | `graphql` 은 persisted-document ID, `grpc-web`/`connect-web``package.Service/Method`. `rest``none` |
| `eventSchema` | conditional | `transferMode: stream` 이면 이벤트 union schema reference 필수 |
| `resumeStrategy` | conditional | `stream`/`download``none` \| `last-event-id` \| `cursor` \| `range` 중 하나 |
기존 필드는 그대로 유지된다. `timeoutMs``transferMode: stream` 행에서 **연결 수립 timeout** 을 뜻하고 스트림 총 수명에는 적용하지 않는다 — 이 해석 차이를 §5.3 본문에 명시한다.
Initial planned rows(sample fixture 용, `feature-sample-feature-slice-contract-fixture` owner):
| operationId | method | path | protocol | transferMode | idempotency | 비고 |
|---|---|---|---|---|---|---|
| `STREAM_SAMPLE_EVENTS` | `GET` | `/api/sample/events` | `sse` | `stream` | `safe` | `resumeStrategy: last-event-id` |
| `PRESIGN_SAMPLE_UPLOAD` | `POST` | `/api/sample/uploads/presign` | `rest` | `unary` | `keyed` | presign 획득만; byte 전송 아님 |
| `DOWNLOAD_SAMPLE_OBJECT` | `GET` | `/api/sample/objects/:id/content` | `rest` | `download` | `safe` | `resumeStrategy: range` |
#### `FE-REG-STORAGE` — §5.5 schema 확장
- `backend` enum 에 `opfs`, `cacheStorage` 추가
- `+payloadClass``structured`(기본) \| `binary`. `binary``sensitive-forbidden` classification 과 조합할 수 없다
- `+evictionOrder` — quota 압박 시 제거 순서(정수). 낮을수록 먼저 제거. `FE-D027` 의 "correctness 값은 임의 fallback 금지"와 연동해 correctness 값은 `null`(제거 불가)로 표기
Initial planned rows 추가:
| logicalName | backend | classification | payloadClass | TTL / fallback | evictionOrder |
|---|---|---|---|---|---|
| `UPLOAD_PART_STATE` | `indexedDB` | `opaque-cache` | `structured` | 전송 완료 또는 24h / **fallback 없음** | `null` |
| `TRANSFER_OBJECT_BUFFER` | `opfs` | `opaque-cache` | `binary` | 전송 완료 시 삭제 / no-persist | `1` |
| `QUERY_CACHE_SNAPSHOT` | `indexedDB` | `opaque-cache` | `structured` | release·config·API version 파티션 / memory-only | `2` |
| `SW_RESPONSE_CACHE` | `cacheStorage` | `opaque-cache` | `binary` | release 단위 파티션 / feature-disable | `3` |
#### `FE-REG-ERROR` — §5.6 `kind` enum 26종 추가
```text
CAPABILITY_DISABLED
CAPABILITY_UNSUPPORTED
FILE_PICKER_DISMISSED
FILE_REJECTED
BLOB_STORE_UNAVAILABLE
BLOB_STORE_QUOTA_EXCEEDED
CACHE_PERSISTENCE_FAILURE
CROSS_TAB_CHANNEL_UNAVAILABLE
PRESIGN_EXPIRED
UPLOAD_PART_FAILED
TRANSFER_INTEGRITY_MISMATCH
STREAM_INTERRUPTED
PROTOCOL_STATUS_MISMATCH
CODEC_DECODE_FAILURE
PARTIAL_RESULT_FAILURE
REALTIME_CONNECT_FAILED
REALTIME_DISCONNECTED
REALTIME_RESUME_GAP
EVENT_SCHEMA_MISMATCH
PUSH_PERMISSION_DENIED
PUSH_SUBSCRIPTION_EXPIRED
WORKER_UNAVAILABLE
WORKER_TASK_TIMEOUT
SW_REGISTRATION_FAILED
BACKGROUND_SYNC_UNSUPPORTED
BACKGROUND_SYNC_REPLAY_REJECTED
```
의도적으로 제외한 것:
- `TRANSFER_ABORTED` — 기존 `REQUEST_ABORTED` 를 재사용한다.
- `SUBSCRIPTION_LEAKED` — 사용자에게 보이는 실패가 아니라 gate 가 잡는 **결함**이다. `FE-GATE-031` fixture 이름으로만 쓴다.
- `SW_UPDATE_PENDING` — 실패가 아니라 §9.7 SW 갱신 surface state 다.
`FILE_PICKER_DISMISSED` 는 사용자 취소이므로 `severity: info`, `action: none` 이고 error surface 를 띄우지 않는다. 이를 §8.2 failure matrix 행에 명시한다.
#### `FE-REG-QUERY` — §5.7 규칙 확장
| Rule | Normative behavior |
|---|---|
| `persistenceTier` | `memory`(기본) \| `session` \| `local` \| `indexedDB`. `memory` 외 값은 `CAP_FE_CACHE_PERSISTENCE` 활성 필요 |
| `crossTabScope` | `none`(기본) \| `same-origin`. `same-origin` 은 무효화 **key 만** 전파하고 값은 전파하지 않음(`FE-D028`) |
| version partition | 영속 tier 는 `releaseId`·`configSchemaVersion`·`apiContractVersion` 을 파티션 키에 포함. 불일치 시 복원하지 않고 폐기 |
#### `FE-REG-ENV` — §5.4 신규 키
| Key | Phase | Classification | Required | Default | Failure |
|---|---|---|---|---|---|
| `CAPABILITY_BINARY_IO_ENABLED` | runtime | public | no | `false` | invalid value boot fail |
| `CAPABILITY_CACHE_PERSISTENCE_ENABLED` | runtime | public | no | `false` | invalid value boot fail |
| `CAPABILITY_LARGE_TRANSFER_ENABLED` | runtime | public | no | `false` | invalid value boot fail |
| `CAPABILITY_ALT_PROTOCOL_ENABLED` | runtime | public | no | `false` | invalid value boot fail |
| `CAPABILITY_REALTIME_ENABLED` | runtime | public | no | `false` | invalid value boot fail |
| `CAPABILITY_BACKGROUND_EXEC_ENABLED` | runtime | public | no | `false` | invalid value boot fail |
| `ALT_PROTOCOL_BASE_URL` | runtime | public-sensitive | conditional | none | capability 활성 시 boot fail |
| `REALTIME_ENDPOINT_URL` | runtime | public-sensitive | conditional | none | capability 활성 시 boot fail |
| `MEDIA_CDN_BASE_URL` | runtime | public-sensitive | conditional | none | 미설정 시 원본 URL 사용 |
| `MEDIA_CDN_ALLOWED_TRANSFORMS` | runtime | public | conditional | `[]` | 허용 외 transform 요청은 무시 |
| `PUSH_PUBLIC_KEY` | runtime | public | conditional | none | push 활성 시 boot fail |
| `TRANSFER_PART_SIZE_BYTES` | runtime | public | no | `8388608` (8 MiB) | invalid value boot fail |
| `TRANSFER_MAX_PARALLEL_PARTS` | runtime | public | no | `3` | invalid value boot fail |
| `REALTIME_RECONNECT_CAP_MS` | runtime | public | no | `30000` | invalid value boot fail |
| `WORKER_TASK_TIMEOUT_MS` | runtime | public | no | `30000` | invalid value boot fail |
`PUSH_PUBLIC_KEY` 는 VAPID **공개**키이므로 `public` 이다. 대응 개인키는 backend 소유이며 frontend registry 에 등록할 수 없다.
#### `FE-REG-TELEMETRY` — §5.8 신규 이벤트
| Event | Trigger | Required attributes |
|---|---|---|
| `capability.activation.rejected` | flag ON 인데 브라우저 미지원으로 비활성 확정 | `capability_id`, `reason`, `build_id` |
| `transfer.part.failed` | upload part 재시도 소진 | `operation_id`, `part_index_bucket`, `error_kind` |
| `transfer.completed` | 전송 종료(성공/실패 공통) | `operation_id`, `outcome`, `size_bucket`, `duration_bucket` |
| `realtime.connection.state_changed` | 연결 상태 전이 | `operation_id`, `from_state`, `to_state`, `attempt_count_bucket` |
| `realtime.event.rejected` | 인바운드 프레임 스키마 거부 | `operation_id`, `error_kind` |
| `sw.update.applied` | SW 신 버전 활성화 | `build_id`, `previous_build_id` |
| `background.sync.replayed` | background sync 재생 결과 | `operation_id`, `outcome` |
`forbiddenAttributes` 는 기존 규칙 그대로다. 특히 파일명·object key·presigned URL·구독 endpoint 는 전송 금지이며 이를 §5.8 본문에 추가 명시한다.
#### `FE-REG-RELEASE` — §5.9 신규 토큰
| Token | Source | Compatibility role |
|---|---|---|
| `serviceWorkerVersion` | build output | SW 스크립트와 release 의 coherence. rollback 시 SW 도 되돌아갔는지 판정 |
### 6.3 registry 개수 변경 파급
`FE-D018``DEC-…-REGISTRY-001` 의 Summary 가 "8개 registry" 를 열거하므로 다음을 함께 고친다.
- §5.1 Registry owner map 에 `FE-REG-CAPABILITY` 행 추가 (8행 → 9행)
- `FE-OC-022` 의 normative summary "8개 registry" → "9개 registry" (§2.1 과 §2.1.1 양쪽)
- §18.2 `FE-RDY-009` blocking question "8 registry" → "9 registry"
- `DEC-…-REGISTRY-001` Summary 갱신 — 상세는 §7.2 참조
---
## 7. 결정 레지스트리
### 7.1 신규 결정 11건
hub 는 §3.2(`FE-D*`, legacy)와 §6.1(`DEC-…`, 현행 SSOT)을 쌍으로 유지한다. 신규 결정도 같은 패턴으로 양쪽에 넣는다.
§3.2 신규 행:
| Decision ID | Decision | Status | Owner | Affected FE-OC | Evidence / rationale | Revisit trigger | Supersedes |
|---|---|---|---|---|---|---|---|
| `FE-D026` | 신규 runtime capability 6종은 `FE-REG-CAPABILITY` flag 로 default OFF이며 활성화는 owner·gate·runbook 을 동반한다 | `conditional-default` | `feature-frontend-env-runtime-config-contract` | `FE-OC-004`, `FE-OC-022`, `FE-OC-027`~`FE-OC-032` | project-local default, 외부 source claim 아님 | 특정 capability 가 제품 필수가 되어 상시 활성이 요구됨 | — |
| `FE-D027` | 로컬 바이너리 backend 는 IndexedDB 를 default 로 하고 OPFS 는 대용량 순차 write 에 opt-in, Cache Storage 는 service worker 호스팅 response cache 전용이다 | `conditional-default` | `feature-frontend-binary-file-io-store-contract` | `FE-OC-013`, `FE-OC-027` | project-local default, 외부 source claim 아님 | OPFS 브라우저 지원 또는 quota 정책이 바뀜 | — |
| `FE-D028` | 탭 간 무효화는 BroadcastChannel 우선에 `storage` event fallback 을 쓰고 leader election 없이 무효화 key 만 전파한다 | `conditional-default` | `feature-frontend-cache-tier-cross-tab-invalidation-contract` | `FE-OC-012`, `FE-OC-028` | project-local default, 값 전파 시 PII·stale 표면 확대 | 다중 탭 실시간 협업이 제품 요구가 됨 | — |
| `FE-D029` | presigned URL 획득은 shared client 를 경유하고 실제 byte 전송은 session credential 을 첨부하지 않는 transfer adapter 가 수행한다 | `accepted-documented-only` | `feature-frontend-large-object-transfer-contract` | `FE-OC-006`, `FE-OC-019`, `FE-OC-029` | credential 유출 방지 invariant, project decision | 스토리지가 same-origin proxy 만 제공 | — |
| `FE-D030` | 재개 가능 전송은 part size·병렬도·part 재시도 상한을 registry 로 고정하고 part 상태를 `BlobStorePort` 에 보존한다 | `conditional-default` | `feature-frontend-large-object-transfer-contract` | `FE-OC-029` | project-local default, 외부 source claim 아님 | 스토리지 제공자가 다른 multipart 제약을 요구 | — |
| `FE-D031` | transport default 는 REST 이고 GraphQL·gRPC-Web·Connect-Web 은 `FE-REG-API` 의 protocol 필드로 opt-in 하며 미지원 환경은 REST gateway 로 fallback 한다 | `conditional-default` | `feature-frontend-multi-protocol-api-transport-contract` | `FE-OC-006`, `FE-OC-007`, `FE-OC-030` | project-local default, 외부 source claim 아님 | backend 가 단일 비-REST protocol 만 제공 | — |
| `FE-D032` | 실시간 transport 는 SSE 를 우선하고 양방향이 필요하면 WebSocket, 둘 다 불가할 때만 최소 간격·backoff·visibility gating 을 갖춘 bounded polling 을 쓴다 | `conditional-default` | `feature-frontend-realtime-subscription-lifecycle-contract` | `FE-OC-031` | project-local default, polling 은 마지막 수단 | backend 가 SSE 를 제공하지 않거나 양방향이 기본 요구가 됨 | — |
| `FE-D033` | 실시간 연결은 full jitter backoff 와 30초 cap 을 쓰고 재시도 상한 후 terminal 상태로 전이하며 resume 은 `Last-Event-ID` 또는 cursor 로 수행하고 unmount 시 구독을 해제한다 | `conditional-default` | `feature-frontend-realtime-subscription-lifecycle-contract` | `FE-OC-031`, `FE-OC-011` | 재연결 폭주·구독 누수 억제, project decision | backend 가 서버 주도 재연결 정책을 계약으로 제공 | — |
| `FE-D034` | service worker 는 역할을 분리해 release asset precaching 은 default off 로 유지하고 push·background sync·Cache Storage 호스트 역할만 capability opt-in 으로 허용하며 update UX 계약을 요구한다 | `conditional-default` | `feature-frontend-background-execution-worker-contract` | `FE-OC-016`, `FE-OC-017`, `FE-OC-023`, `FE-OC-032` | precaching 은 release coherence 훼손 원인이나 push·sync 는 SW 없이는 불가 | offline-first 가 제품 요구가 되고 update UX 가 설계됨 | `FE-D019` |
| `FE-D035` | background sync 재생은 `idempotency: keyed` operation 만 허용한다 | `accepted-documented-only` | `feature-frontend-background-execution-worker-contract` | `FE-OC-009`, `FE-OC-032` | `FE-D016` 의 중복 write 방지 invariant 를 지연 재생에 확장 | mutation 이 naturally idempotent 임이 schema 로 증명됨 | — |
| `FE-D036` | Web Worker 작업은 structured-clone 또는 Transferable 로만 통신하고 timeout 과 terminate 를 계약하며 worker 안에서 application port 를 재구현하지 않는다 | `accepted-documented-only` | `feature-frontend-background-execution-worker-contract` | `FE-OC-002`, `FE-OC-032` | worker 안 로직 중복이 layer 경계를 우회하는 것을 차단, project decision | SharedArrayBuffer 기반 병렬 처리가 요구됨 | — |
`Evidence / rationale` 열의 "project-local default, 외부 source claim 아님" 표기는 `FE-D001` 이 쓰는 문구와 같다. 근거 raw 문서 부재를 감추지 않고 명시하는 장치다.
### 7.2 §6.1 Project Decision Registry 신규 행
Summary 셀은 소비 branch 의 상속 표와 **문자열이 정확히 일치**해야 한다(hub §6.1 하단 규칙). 아래 문자열이 SSOT 다.
**조사(particle) 앞 공백 없음.** 기존 §6.1 행이 `…release token을 8개 registry로 관리한다` 형태이므로 신규 행도 같은 표기를 쓴다. 이 표를 branch-note 상속 표에 옮길 때 공백을 하나라도 넣으면 문자열 불일치가 된다.
| Decision ID | Revision | Domain | Decision Summary | Status | Evidence |
|---|---:|---|---|---|---|
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CAPABILITY-001` | 1 | `capability` | 신규 runtime capability 6종은 FE-REG-CAPABILITY flag로 default OFF이며 활성화는 owner·gate·runbook을 동반한다 | `conditional-default` | §3.2 `FE-D026` |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-BINARY-STORE-001` | 1 | `binary-store` | 로컬 바이너리 backend는 IndexedDB를 default로 하고 OPFS는 대용량 순차 write에 opt-in, Cache Storage는 service worker 호스팅 response cache 전용이다 | `conditional-default` | §3.2 `FE-D027` |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-CROSS-TAB-001` | 1 | `cross-tab` | 탭 간 무효화는 BroadcastChannel 우선에 storage event fallback을 쓰고 leader election 없이 무효화 key만 전파한다 | `conditional-default` | §3.2 `FE-D028` |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-TRANSFER-CREDENTIAL-001` | 1 | `transfer-credential` | presigned URL 획득은 shared client를 경유하고 실제 byte 전송은 session credential을 첨부하지 않는 transfer adapter가 수행한다 | `accepted-documented-only` | §3.2 `FE-D029` |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-RESUMABLE-TRANSFER-001` | 1 | `resumable-transfer` | 재개 가능 전송은 part size·병렬도·part 재시도 상한을 registry로 고정하고 part 상태를 BlobStorePort에 보존한다 | `conditional-default` | §3.2 `FE-D030` |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-PROTOCOL-001` | 1 | `protocol` | transport default는 REST이고 GraphQL·gRPC-Web·Connect-Web은 FE-REG-API의 protocol 필드로 opt-in하며 미지원 환경은 REST gateway로 fallback한다 | `conditional-default` | §3.2 `FE-D031` |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REALTIME-TRANSPORT-001` | 1 | `realtime-transport` | 실시간 transport는 SSE를 우선하고 양방향이 필요하면 WebSocket, 둘 다 불가할 때만 최소 간격·backoff·visibility gating을 갖춘 bounded polling을 쓴다 | `conditional-default` | §3.2 `FE-D032` |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-REALTIME-LIFECYCLE-001` | 1 | `realtime-lifecycle` | 실시간 연결은 full jitter backoff와 30초 cap을 쓰고 재시도 상한 후 terminal 상태로 전이하며 resume은 Last-Event-ID 또는 cursor로 수행하고 unmount 시 구독을 해제한다 | `conditional-default` | §3.2 `FE-D033` |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-SERVICE-WORKER-ROLE-001` | 1 | `service-worker-role` | service worker는 역할을 분리해 release asset precaching은 default off로 유지하고 push·background sync·Cache Storage 호스트 역할만 capability opt-in으로 허용하며 update UX 계약을 요구한다 | `conditional-default` | §3.2 `FE-D034` |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-BACKGROUND-SYNC-001` | 1 | `background-sync` | background sync 재생은 idempotency keyed operation만 허용한다 | `accepted-documented-only` | §3.2 `FE-D035` |
| `DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-WORKER-TASK-001` | 1 | `worker-task` | Web Worker 작업은 structured-clone 또는 Transferable로만 통신하고 timeout과 terminate를 계약하며 worker 안에서 application port를 재구현하지 않는다 | `accepted-documented-only` | §3.2 `FE-D036` |
Owner 열은 모두 `[[raw/project-notes/ca-skeleton-frontend-operational-contract]]` 다(§6.1 의 기존 모든 행과 동일).
§3.2 의 `Decision` 셀은 문자열 일치 제약을 받지 않으므로 §7.1 의 서술형 문장을 그대로 써도 된다. 제약을 받는 것은 **§6.1 Summary 셀과 branch-note 상속 표** 두 곳뿐이다.
### 7.3 기존 결정 개정 2건
§3.3 변경 프로토콜을 따르고 §6.1 하단 개정 기록에 남긴다.
#### (a) `FE-D019` → superseded, `DEC-…-OFFLINE-CACHE-001` → revision 2
- **분류**: 새 결정(`FE-D034`)이 기존 의미를 대체 → §3.3 5단계 supersede chain
- **compatibility_impact**: `behavior-change` — "SW 전면 off" 가 "precaching off + 역할별 opt-in" 으로 바뀐다
- §3.2 `FE-D019``Status``superseded` 로 바꾸고 행은 **삭제하지 않는다**
- `DEC-…-OFFLINE-CACHE-001` 을 revision 2 로 올리고 Summary 를 다음으로 교체 (교체 전 문자열은 `service worker와 offline asset cache는 default off다`):
> `service worker의 release asset precaching은 default off이고 push·background sync·Cache Storage 호스트 역할만 capability opt-in으로 허용한다`
- **파급 pin**: `feature-frontend-release-cache-rollback-contract` 의 frontmatter `inherits:` 에서 `DEC-…-OFFLINE-CACHE-001@1``@2`, 그리고 그 branch 의 상속 표 Summary 문자열도 위 문자열로 교체
- §8.0 `WI-…-024``Applies Decisions` 에서도 `@1``@2`
- **behavior-change 이므로 §3.3 4단계**: migration·rollback·test evidence 없이 merge 금지. 여기서는 `FE-GATE-032`(SW update UX + rollback 시 SW 되돌림)와 `FE-RB-007` 이 그 evidence 요구를 담당한다
#### (b) `DEC-…-REGISTRY-001` — revision 1 유지, Summary 갱신
- **분류**: 9번째 registry 추가 → 기존 8개의 동작은 바뀌지 않음
- **compatibility_impact**: `additive`. §6.1 의 `DEC-…-SUPPLY-CHAIN-001` 선례대로 **revision 을 유지(1)** 한다
- Summary 를 다음으로 교체 (교체 전 문자열은 `route, API operation, env, storage, error, query, telemetry, release token을 8개 registry로 관리한다`):
> `route, API operation, env, storage, error, query, telemetry, release token, capability를 9개 registry로 관리한다`
- `FE-D018` 의 Decision 셀도 같은 취지로 갱신
- **파급 문자열**: 이 결정을 상속 표에 복사해 둔 4개 branch — `feature-frontend-storage-registry-contract`, `feature-frontend-observability-logging-trace-contract`, `feature-frontend-contract-registry-governance`, `feature-frontend-contract-compatibility-governance` — 의 Summary 문자열을 함께 교체해야 `CONFLICTS_WITH_PROJECT_DECISION` 이 발생하지 않는다. `inherits:``@1` pin 은 revision 이 그대로이므로 수정 불필요
### 7.4 §2.1.2 Delegation Registry 신규 행
| Delegation ID | Concern Key | Revision | Delegator | Delegate | Scope | Status |
|---|---|---|---|---|---|---|
| `DELEG-FE-007` | `fe.deleg.sw-host-for-push` | 1 | `feature-frontend-realtime-subscription-lifecycle-contract` | `feature-frontend-background-execution-worker-contract` | WebPush 가 요구하는 service worker 등록·수명주기 호스팅 | proposed |
| `DELEG-FE-008` | `fe.deleg.binary-handle-ownership` | 1 | `feature-frontend-large-object-transfer-contract` | `feature-frontend-binary-file-io-store-contract` | 전송 대상 `File`/`Blob` handle 과 object URL 수명 소유 | proposed |
| `DELEG-FE-009` | `fe.deleg.persistent-cache-key` | 1 | `feature-frontend-cache-tier-cross-tab-invalidation-contract` | `feature-frontend-storage-registry-contract` | 영속 캐시의 physical key·namespace·classification·quota fallback 소유 | proposed |
| `DELEG-FE-010` | `fe.deleg.decoded-payload-validation` | 1 | `feature-frontend-multi-protocol-api-transport-contract` | `feature-runtime-schema-validation-contract` | codec 디코드 이후 payload 의 runtime schema 검증 | proposed |
| `DELEG-FE-011` | `fe.deleg.sw-release-coherence` | 1 | `feature-frontend-background-execution-worker-contract` | `feature-frontend-release-cache-rollback-contract` | service worker 버전과 release·rollback coherence 판정 | proposed |
`Status``proposed` 로 시작하고 delegate branch 가 frontmatter `accepts_delegations` 로 접수하면 `accepted` 가 된다. 이번 스캐폴딩에서 **5건 모두 `accepted` 까지 완료**한다.
- `DELEG-FE-007`(delegate = background-execution)·`DELEG-FE-008`(delegate = binary-file-io) — delegate 가 이번에 새로 만드는 branch 이므로 생성 시점에 `accepts_delegations` 를 채워 바로 접수한다.
- `DELEG-FE-009`(storage-registry)·`DELEG-FE-010`(runtime-schema-validation)·`DELEG-FE-011`(release-cache-rollback) — delegate 가 기존 branch 이므로 해당 파일의 `accepts_delegations` 를 §12.3 에서 함께 수정한다.
접수하지 않고 `proposed` 로 남기면 "A 가 넘겼는데 B 는 받은 적 없는" 공백이 되고, 검사기가 없는 현재 환경에서는 그 공백이 자동으로 드러나지 않는다.
### 7.5 §2.1.4 Flow Stage Registry — inbound event 흐름 신설
기존 `FLOW-FE-RESP-001`~`008` 은 요청/응답 전용이라 스트림 프레임이 들어갈 자리가 없다. 같은 형식으로 인바운드 이벤트 흐름을 추가한다.
| Stage ID | Order | Owner | Input | Action | Output | Invariants | Revision |
|---|---:|---|---|---|---|---|---|
| `FLOW-FE-EVENT-001` | 1 | `feature-frontend-realtime-subscription-lifecycle-contract` | 열린 연결 | 프레임 수신 대기 | raw frame | 연결 오류는 이 단계가 소유하고 이후 단계로 예외를 넘기지 않는다 | 1 |
| `FLOW-FE-EVENT-002` | 2 | `feature-frontend-realtime-subscription-lifecycle-contract` | raw frame | transport decode (SSE 필드 / WebSocket frame / poll 응답 본문) | unvalidated event JSON | decode 실패는 프레임을 버리고 실패로 전환하며 연결을 즉시 끊지 않는다 | 1 |
| `FLOW-FE-EVENT-003` | 3 | `feature-runtime-schema-validation-contract` | unvalidated event JSON | event envelope 공유 스키마 검증 | discriminated event envelope | 경계 검증은 non-throwing 이며 throw 를 상위로 누출하지 않는다 | 1 |
| `FLOW-FE-EVENT-004` | 4 | `feature-runtime-schema-validation-contract` | discriminated event envelope | `eventSchema` per-event 검증 | 검증된 event payload | 미검증 프레임은 `EVENT_SCHEMA_MISMATCH` 로 드롭하고 application 에 도달시키지 않는다 | 1 |
| `FLOW-FE-EVENT-005` | 5 | `feature-frontend-error-classification-boundary-contract` | 검증된 event payload 또는 실패 신호 | 정규화된 이벤트 또는 failure 반환 | application event 또는 normalized failure | 총함수 — 미매핑 예외는 `UNKNOWN_FAILURE` 로 귀결한다 | 1 |
구독 해제(cleanup)는 이 흐름이 아니라 `RealtimeSubscriptionPort` 계약이 소유한다. 흐름은 프레임 하나의 여정만 기술한다.
---
## 8. Acceptance Gate
### 8.1 조건부 차단 개념 — `NOT_APPLICABLE`
capability 가 OFF 인 프로젝트에서 해당 gate 를 PASS 라고 하면 거짓말이고, FAIL 이라고 하면 영원히 배포할 수 없다. `NOT_APPLICABLE` 상태를 도입하되 남용을 막는 불변식을 건다.
hub §18.1 Formula 를 다음으로 교체한다.
```text
PASS_STATES = {PASS, PASS_SCOPED}
NOT_APPLICABLE = capability-scoped gate 가, 대응 capability flag 가 OFF 이고
FE-GATE-033 이 그 adapter 의 번들 부재를 증명했을 때만 취할 수 있는 상태
READY iff every blocking row Current is in PASS_STATES or is a valid NOT_APPLICABLE
otherwise NOT_READY
```
규범:
- `NOT_APPLICABLE``FE-GATE-027`~`FE-GATE-032` **만** 취할 수 있다. `FE-GATE-033` 자신은 취할 수 없다.
- `FE-GATE-033` 이 PASS 가 아니면 모든 `NOT_APPLICABLE` 은 무효이며 `FAIL_UNVERIFIED` 로 강등된다.
- "안 쓴다"는 주장은 선언이 아니라 **번들에 그 adapter 가 없다는 증거**로만 성립한다.
### 8.2 §2.1.1 gate 행 (typed)
| Contract ID | Concern Key | Revision | Type | Owner | Trigger | Required Effect | Enforcement | Status |
|---|---|---|---|---|---|---|---|---|
| `FE-GATE-027` | `fe.gate.binary-file-io` | 1 | gate | `feature-frontend-binary-file-io-store-contract` | 파일 I-O 또는 로컬 바이너리 저장 코드를 변경할 때 | picker·다운로드·quota·object URL 해제 fixture 가 실패하면 merge 를 MUST 차단 | binary I-O report | active |
| `FE-GATE-028` | `fe.gate.cache-tier-cross-tab` | 1 | gate | `feature-frontend-cache-tier-cross-tab-invalidation-contract` | 캐시 영속화나 탭 간 전파를 변경할 때 | version 파티션·탭 간 무효화 fixture 가 실패하면 merge 를 MUST 차단 | cache tier report | active |
| `FE-GATE-029` | `fe.gate.large-object-transfer` | 1 | gate | `feature-frontend-large-object-transfer-contract` | 대용량 전송 경로를 변경할 때 | presign 만료·part 재시도·무결성·credential 경계 fixture 가 실패하면 merge·release 를 MUST 차단 | transfer report | active |
| `FE-GATE-030` | `fe.gate.multi-protocol-transport` | 1 | gate | `feature-frontend-multi-protocol-api-transport-contract` | protocol adapter 나 codec 을 변경할 때 | protocol 별 성공/실패 정규화 fixture 가 실패하면 merge 를 MUST 차단 | protocol mapping report | active |
| `FE-GATE-031` | `fe.gate.realtime-lifecycle` | 1 | gate | `feature-frontend-realtime-subscription-lifecycle-contract` | 실시간 연결·구독 코드를 변경할 때 | backoff·resume·구독 해제·이벤트 검증 fixture 가 실패하면 merge·release 를 MUST 차단 | realtime lifecycle report | active |
| `FE-GATE-032` | `fe.gate.background-execution` | 1 | gate | `feature-frontend-background-execution-worker-contract` | worker·service worker·background sync 를 변경할 때 | SW update UX·sync idempotency·worker timeout fixture 가 실패하면 merge·release 를 MUST 차단 | background execution report | active |
| `FE-GATE-033` | `fe.gate.capability-default-off` | 1 | gate | `feature-frontend-env-runtime-config-contract` | capability flag 나 adapter 등록을 변경할 때 | 기본 config build 에 비활성 capability 의 adapter 가 포함되면 merge·release 를 MUST 차단 | capability bundle report | active |
### 8.3 §15.1 Gate ownership 행
| Gate ID | Gate | Blocking scope | Covered FE-OC | Covered FE-NFR | Required fixtures | Pass condition | Evidence artifact | Current |
|---|---|---|---|---|---|---|---|---|
| `FE-GATE-027` | binary I-O & local store | merge | `FE-OC-013`, `FE-OC-027` | — | picker 취소·거부, quota 초과 fallback, OPFS 순차 write, Cache Storage 버전 파티션, object URL 해제 | 모든 fixture 가 기대 kind 로 처리되고 object URL 누수 0 | binary I-O report | `FAIL_UNVERIFIED` |
| `FE-GATE-028` | cache tier & cross-tab | merge | `FE-OC-012`, `FE-OC-028` | — | 영속 캐시 version 파티션, 탭 A mutation → 탭 B 무효화, BroadcastChannel 부재 fallback | 불일치 version 캐시는 복원되지 않고 탭 간 무효화가 도달 | cache tier report | `FAIL_UNVERIFIED` |
| `FE-GATE-029` | large object transfer | merge + release | `FE-OC-019`, `FE-OC-029` | `FE-NFR-018` | presign 만료, part 재시도 상한, stream 중단 후 재개, 무결성 불일치, credential 첨부 negative | 모든 fixture 통과 + 전송 요청에 session credential 0건 | transfer report | `FAIL_UNVERIFIED` |
| `FE-GATE-030` | multi-protocol transport | merge | `FE-OC-006`, `FE-OC-007`, `FE-OC-030` | — | GraphQL `200 + errors[]`, gRPC status ↔ HTTP status, codec decode 실패, gateway fallback | 모든 protocol 실패가 기대 kind 로 정규화 | protocol mapping report | `FAIL_UNVERIFIED` |
| `FE-GATE-031` | realtime lifecycle | merge + release | `FE-OC-011`, `FE-OC-031` | `FE-NFR-016`, `FE-NFR-017` | 결정론 fake clock backoff, resume gap 감지, unmount 후 열린 연결, 이벤트 스키마 거부, `FE-RB-006` drill | backoff 가 cap 을 넘지 않고 unmount 후 열린 연결 0, 미검증 이벤트 0건 도달 | realtime lifecycle report | `FAIL_UNVERIFIED` |
| `FE-GATE-032` | background execution | merge + release | `FE-OC-016`, `FE-OC-017`, `FE-OC-032` | `FE-NFR-019` | SW 등록·갱신 UX, rollback 시 SW 되돌림, background sync keyed-only, worker timeout·terminate, `FE-RB-007` drill | 모든 fixture 통과 + keyed 아닌 mutation replay 0건 | background execution report | `FAIL_UNVERIFIED` |
| `FE-GATE-033` | capability default-off | merge + release | `FE-OC-004`, `FE-OC-022`, `FE-OC-027`~`FE-OC-032` | `FE-NFR-020` | 기본 config 로 production build, 각 capability ON 조합 build | 비활성 capability 의 adapter 모듈이 어떤 chunk 에도 없고 initial JS 증가 0 | capability bundle report | `FAIL_UNVERIFIED` |
§15.1 서두의 "현재 stable gate registry는 26개 row" 를 **33개 row** 로 고친다.
### 8.4 §15.2 Negative fixture 신규 행
| Gate | Negative fixture example |
|---|---|
| binary I-O | quota 초과인데 `UPLOAD_PART_STATE` 가 memory 로 fallback |
| cache tier | release/config version 불일치 캐시를 복원해 사용 |
| large object transfer | transfer 요청에 session credential 헤더가 첨부 |
| multi-protocol | `200 OK` + `errors[]` 를 success 로 반환 |
| realtime lifecycle | unmount 후에도 구독이 살아 있음 / 미검증 프레임이 application 도달 |
| background execution | `idempotency: none` mutation 이 background sync 로 replay |
| capability default-off | flag OFF 인데 adapter 가 initial chunk 에 포함 |
### 8.5 §15.3 Promotion rule 갱신
```text
MERGE_READY = FE-GATE-001, FE-GATE-002, FE-GATE-003, FE-GATE-004, FE-GATE-005, FE-GATE-006,
FE-GATE-007, FE-GATE-008, FE-GATE-009, FE-GATE-010, FE-GATE-011, FE-GATE-013,
FE-GATE-020, FE-GATE-033 PASS
AND FE-GATE-027, FE-GATE-028, FE-GATE-030 각각 PASS 또는 유효한 NOT_APPLICABLE
RELEASE_READY = MERGE_READY AND FE-GATE-012, FE-GATE-014, FE-GATE-015, FE-GATE-019, FE-GATE-026 PASS
AND FE-GATE-029, FE-GATE-031, FE-GATE-032 각각 PASS 또는 유효한 NOT_APPLICABLE
PROD_PROMOTION_READY = RELEASE_READY AND FE-GATE-016, FE-GATE-021, FE-GATE-022, FE-GATE-023,
FE-GATE-024, FE-GATE-025 PASS
FIELD_SLO_READY = PROD_PROMOTION_READY AND FE-GATE-018 PASS
DOCUMENTATION_READY = FE-GATE-017 PASS_SCOPED AND evidence ledger updated
PROJECT_READY = all applicable blocking gates PASS
```
`FE-GATE-033``NOT_APPLICABLE` 을 취할 수 없으므로 항상 `MERGE_READY` 에 무조건 포함된다.
---
## 9. NFR · Async surface · Runbook
### 9.1 §14.2 신규 NFR 5개
| NFR ID | Metric | Context | Initial target | Current evidence |
|---|---|---|---|---|
| `FE-NFR-016` | realtime reconnect backoff cap | 결정론 fake clock | 재연결 간격 ≤ 30s, 재시도 상한 후 terminal 전이 | none |
| `FE-NFR-017` | subscription leak | unmount fixture | unmount 후 열린 구독/연결 0 | none |
| `FE-NFR-018` | upload part 재시도 | 결정론 fake clock | part 당 재시도 ≤ 2 (`FE-D015` 상속), 전체 전송은 취소 가능 | none |
| `FE-NFR-019` | worker task timeout | worker fixture | 기본 30s 초과 시 terminate 되고 결과를 기다리지 않음 | none |
| `FE-NFR-020` | capability OFF 시 번들 증가 | `FE-NFR-C04` | 6개 capability 전부 OFF 일 때 initial JS gzip 증가 0 KiB | none |
`FE-NFR-020``FE-NFR-001`(initial JS ≤ 200 KiB) 예산을 신규 기능이 잠식하지 않음을 수치로 방어하는 항목이다.
### 9.2 §9.5 실시간 surface state (신설)
hub §9.1 의 4-state 모델은 요청/응답 전용이라 스트림에 그대로 맞지 않는다. 별도 표로 추가하되 §9.1 과 충돌하지 않게 **매핑**을 명시한다.
| State | 의미 | §9.1 대응 | UI requirement |
|---|---|---|---|
| `connecting` | 최초 연결 시도 중, 수신 이벤트 0 | `initial-loading` | 안정 skeleton, focus 탈취 금지 |
| `live` | 연결 유지, 이벤트 수신 중 | `success` | 최신 상태 표시 |
| `reconnecting` | 끊김 후 backoff 재시도 중, 마지막 데이터 유지 | `refreshing` | 기존 내용 유지 + 은은한 표시 |
| `resumed-with-gap` | 재연결했으나 resume cursor 로 메운 구간에 공백 존재 | `stale-degraded` | 공백 사실 표시 + 수동 새로고침 |
| `disconnected` | 재시도 상한 소진, terminal | `terminal-error` | 안전한 메시지 + registry action |
`reconnecting``terminal-error` 로 표시하면 사용자가 불필요하게 새로고침하고, `disconnected``refreshing` 으로 표시하면 영원히 오지 않는 데이터를 기다린다. 이 구분이 이 표의 존재 이유다.
### 9.3 §9.6 전송 진행 surface state (신설)
| State | 의미 | §9.1 대응 | UI requirement |
|---|---|---|---|
| `transfer-preparing` | presign 획득·part 분할 중 | `initial-loading` | 취소 가능 표시 |
| `transfer-active` | byte 전송 중 | `mutation-pending` | 진행률 + 취소, 중복 시작 차단 |
| `transfer-paused` | 사용자 중단 또는 네트워크 중단, 재개 가능 | `stale-degraded` | 재개 action |
| `transfer-failed` | 재시도 소진 또는 무결성 불일치 | `terminal-error` | registry action, part 상태 폐기 여부 명시 |
| `transfer-completed` | 완료 및 검증됨 | `success` | 결과 표시 |
진행률은 telemetry 에 원본 크기·파일명을 남기지 않고 `size_bucket`·`duration_bucket` 만 남긴다(§6.2 telemetry 확장과 일치).
### 9.4 §9.7 Service Worker 갱신 surface state (신설)
| State | 의미 | UI requirement |
|---|---|---|
| `sw-none` | 등록 없음(기본) | 표시 없음 |
| `sw-active` | 현재 release 의 SW 활성 | 표시 없음 |
| `sw-update-pending` | 새 SW 가 설치됐고 활성화 대기 | 사용자 주도 적용 action. **자동 `skipWaiting` 금지** — 열린 탭이 release 를 갈아타면 `FE-OC-016` coherence 가 깨진다 |
| `sw-update-failed` | 등록·갱신 실패 | `SW_REGISTRATION_FAILED`, 기능 저하만, 제품 흐름 차단 금지 |
### 9.5 §16 신규 runbook 2개
기존 5개 runbook 형식(trigger / diagnosis evidence / mitigation invariant / recovery assertion / escalation)을 그대로 따른다.
- **`FE-RB-006` 실시간 연결 장애** — trigger: `realtime.connection.state_changed``reconnecting` 비율 급증 또는 `REALTIME_RESUME_GAP` 발생. mitigation invariant: 재연결 상한을 낮추거나 capability 를 OFF 로 내려 bounded polling 으로 강등하되 backend 에 무제한 재시도를 보내지 않는다. recovery assertion: 재연결 간격이 cap 이하이고 terminal 전이가 관측되며 구독 수가 안정된다.
- **`FE-RB-007` 백그라운드 실행 장애** — trigger: `sw.update.applied` 부재로 `sw-update-pending` 이 정체되거나 `background.sync.replayed` 에 중복 outcome. mitigation invariant: SW 등록을 해제해 이전 release 로 되돌릴 때 `serviceWorkerVersion` 을 함께 되돌리고, keyed 아닌 mutation 은 재생하지 않는다. recovery assertion: rollback 후 SW 버전이 대상 release 와 일치하고 중복 write 가 0 이다.
두 runbook 의 drill assertion 은 새 gate 를 만들지 않고 `FE-GATE-031`·`FE-GATE-032` 안에 포함한다. `FE-OC-025` 의 normative summary 를 "boot, chunk mismatch, API degradation, telemetry failure, rollback, realtime 연결, background 실행 runbook 을 MUST 유지" 로 갱신한다.
---
## 10. 그 밖의 hub 파급
### 10.1 §0.5 범위
In scope 에 추가:
- 파일·바이너리 I-O 와 로컬 대용량 저장의 port·adapter 계약
- 캐시 계층과 탭 간 무효화 계약
- 대용량 객체 전송(presigned·재개 가능 업로드·스트리밍 다운로드)과 미디어 URL 정책
- REST 이외 protocol adapter 의 정규화 계약
- 실시간 구독 수명주기 계약
- 백그라운드 실행(Worker·Service Worker·Background Sync) 계약
Out of scope 에 추가:
- 위 6개 도메인의 use case, domain model, business rule — 이 skeleton 은 port 와 adapter 계약까지만 제공한다
- 구체 backend API 설계, vendor 선택, 파일 형식별 처리, offline-first 동기화 충돌 해결
### 10.2 §8.2 Failure matrix
신규 26개 kind 에 대해 retryable 여부·기본 action·telemetry event·redaction 을 기존 표 형식으로 채운다. 특기 사항:
- `FILE_PICKER_DISMISSED` — retryable 아님, `action: none`, error surface 없음, telemetry 없음(사용자 취소는 실패가 아니다)
- `CAPABILITY_DISABLED` / `CAPABILITY_UNSUPPORTED` — retryable 아님, `action` 은 registry 의 `disabledFallback` 에 따름
- `PRESIGN_EXPIRED` — retryable(presign 재획득 후 재개), `action: retry`
- `REALTIME_DISCONNECTED` — 재시도 상한 소진 후 도달하는 terminal 이므로 자동 retry 금지, `action: retry`(사용자 주도)
- `BACKGROUND_SYNC_REPLAY_REJECTED` — retryable 아님, `action: contact-support`, 중복 write 방지가 우선
### 10.3 §17.1 Evidence ledger
| Evidence ID | Artifact / observation | Grade | Supports | Does not prove |
|---|---|---|---|---|
| `FE-EV-014` | 본 설계 문서 | `documented-only` | 6개 도메인의 계약 범위·ID·기본값·게이트 구조 | 코드 존재, adapter 동작, 게이트 실행 |
| `FE-EV-015` | 신규 branch-note 6개 | `documented-only` | branch 소유권과 완료 조건의 선언 | 구현 또는 테스트 |
### 10.4 §18.2 Readiness scorecard
- `FE-RDY-009` blocking question "8 registry" → "9 registry"
- 신규 행 `FE-RDY-017` — blocking question "비활성 capability 가 번들에서 실제로 부재한가", required evidence "capability bundle report", Current `FAIL`, reason "build evidence `UNVERIFIED`"
### 10.5 §19 Risk / Open question
신규 risk:
| Risk ID | Risk | Owner | Trigger | Mitigation |
|---|---|---|---|---|
| `FE-RISK-013` | capability flag 가 늘어나 조합 폭발로 테스트 매트릭스가 비현실적이 됨 | capability owner | 3개 이상 동시 활성 | 조합 대신 개별 ON + 전부 OFF 두 축만 gate 로 검증 |
| `FE-RISK-014` | OPFS·Cache Storage 브라우저 지원 편차로 동일 코드가 환경별로 다르게 동작 | binary I-O owner | 지원 매트릭스 확정 | feature detection + `disabledFallback` 강제 |
| `FE-RISK-015` | presigned URL 이 로그·telemetry·referrer 로 유출 | transfer owner | 전송 구현 | forbiddenAttributes + `Referrer-Policy` + gate negative fixture |
| `FE-RISK-016` | 재연결 backoff 가 여러 구독에서 동시 만료해 thundering herd 발생 | realtime owner | 다중 구독 사용 | 구독별 독립 full jitter, 전역 동시 재연결 상한 |
| `FE-RISK-017` | Service Worker 가 rollback 후에도 이전 스크립트를 유지해 release 판정이 어긋남 | background exec owner | rollback drill | `serviceWorkerVersion` 토큰 + `FE-RB-007` |
| `FE-RISK-018` | protobuf·GraphQL 스키마 파이프라인이 없어 codec 과 runtime schema 가 갈라짐 | protocol owner | 첫 비-REST 통합 | 단일 생성 소스 결정 전까지 REST 유지 |
신규 open question:
| Question ID | Question | Owner | Decision trigger |
|---|---|---|---|
| `FE-Q-011` | 6개 도메인의 근거 raw 문서(MDN·WHATWG·gRPC-Web·Connect·Web Push 등)를 언제 수집할 것인가 | project owner | 첫 capability 활성화 |
| `FE-Q-012` | object storage 제공자와 presigned/multipart 제약(part 최소 크기·만료)은 무엇인가 | transfer owner | 첫 업로드 구현 |
| `FE-Q-013` | protobuf/GraphQL 스키마 생성 파이프라인의 SSOT 는 어디인가 | protocol owner | 첫 비-REST 통합 |
| `FE-Q-014` | Web Push 발송 서버와 VAPID 키 소유자는 누구인가 | background exec owner | push 활성화 |
`FE-Q-009`("service worker/offline이 필요한가")는 `FE-D034` 로 해소되므로 Resolution condition 열에 "`FE-D034` 로 해소(2026-07-28)" 를 기록하고 상태를 닫는다.
### 10.6 §22 External Answer Boundary
§22.2("설계라고 명시해야 답할 수 있는 것")에 추가: 6개 capability 의 port 이름·기본값·게이트 구조. §22.3("현재 답하면 안 되는 것")에 추가: 이 adapter 들이 동작한다는 진술, 성능·안정성 수치, 특정 vendor 와의 통합 검증.
---
## 11. Branch 스캐폴딩
### 11.1 §8.0 Work Item Registry 신규 행
이 절과 §11.2 에서 `WI-…-0NN``WI-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-0NN` 의, `DEC-…-DOMAIN-001``DEC-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-DOMAIN-001` 의 축약이다. hub 에 옮겨 적을 때는 **전체 ID 를 그대로** 쓴다. 전체 문자열 SSOT 는 §7.2 다.
| Work Item ID | branch slug | 완료 조건 (측정가능) | Applies Decisions | Dependencies |
|---|---|---|---|---|
| `WI-…-028` | `feature-frontend-binary-file-io-store-contract` | picker·다운로드·object URL 해제·quota·OPFS·Cache Storage fixture 가 통과하고 binary I-O report 가 생성된다 | `DEC-…-CAPABILITY-001@1`, `DEC-…-BINARY-STORE-001@1`, `DEC-…-REGISTRY-001@1` | `WI-…-011`(storage registry), `WI-…-002`(layering) |
| `WI-…-029` | `feature-frontend-cache-tier-cross-tab-invalidation-contract` | version 파티션·탭 간 무효화·채널 부재 fallback fixture 가 통과한다 | `DEC-…-CAPABILITY-001@1`, `DEC-…-CROSS-TAB-001@1`, `DEC-…-SERVER-STATE-001@1` | `WI-…-010`(server state), `WI-…-011` |
| `WI-…-030` | `feature-frontend-large-object-transfer-contract` | presign 만료·part 재시도·무결성·credential 경계 fixture 가 통과한다 | `DEC-…-CAPABILITY-001@1`, `DEC-…-TRANSFER-CREDENTIAL-001@1`, `DEC-…-RESUMABLE-TRANSFER-001@1` | `WI-…-005`(API client), `WI-…-028` |
| `WI-…-031` | `feature-frontend-multi-protocol-api-transport-contract` | protocol 별 성공/실패 정규화와 gateway fallback fixture 가 통과한다 | `DEC-…-CAPABILITY-001@1`, `DEC-…-PROTOCOL-001@1`, `DEC-…-VALIDATION-001@1` | `WI-…-005`, `WI-…-006`(runtime schema), `WI-…-007`(error classification) |
| `WI-…-032` | `feature-frontend-realtime-subscription-lifecycle-contract` | backoff·resume·구독 해제·이벤트 검증 fixture 와 `FE-RB-006` drill 이 통과한다 | `DEC-…-CAPABILITY-001@1`, `DEC-…-REALTIME-TRANSPORT-001@1`, `DEC-…-REALTIME-LIFECYCLE-001@1` | `WI-…-006`, `WI-…-007`, `WI-…-013`(async UI) |
| `WI-…-033` | `feature-frontend-background-execution-worker-contract` | SW update UX·rollback SW 되돌림·sync idempotency·worker timeout fixture 와 `FE-RB-007` drill 이 통과한다 | `DEC-…-CAPABILITY-001@1`, `DEC-…-SERVICE-WORKER-ROLE-001@1`, `DEC-…-BACKGROUND-SYNC-001@1`, `DEC-…-WORKER-TASK-001@1` | `WI-…-024`(release/cache/rollback), `WI-…-005` |
`FE-GATE-033` 의 owner 는 `feature-frontend-env-runtime-config-contract` 이므로 기존 `WI-…-004` 의 완료 조건에 "capability registry 와 default-off 번들 검증" 을 추가한다. 새 WI 를 만들지 않는다.
### 11.2 §20 Branch Decomposition 신규 행
| Branch slug | Primary contract IDs | Contributes to | Priority | Dependency |
|---|---|---|---|---|
| `feature-frontend-binary-file-io-store-contract` | `FE-OC-027` | `FE-OC-013`, `FE-OC-022`, `FE-OC-029` | P4 | storage registry, layering |
| `feature-frontend-cache-tier-cross-tab-invalidation-contract` | `FE-OC-028` | `FE-OC-012`, `FE-OC-013`, `FE-OC-023` | P4 | server state, storage registry |
| `feature-frontend-large-object-transfer-contract` | `FE-OC-029` | `FE-OC-006`, `FE-OC-019`, `FE-OC-027` | P4 | API client, binary I-O |
| `feature-frontend-multi-protocol-api-transport-contract` | `FE-OC-030` | `FE-OC-006`, `FE-OC-007`, `FE-OC-008` | P4 | API client, runtime schema, error classification |
| `feature-frontend-realtime-subscription-lifecycle-contract` | `FE-OC-031` | `FE-OC-007`, `FE-OC-008`, `FE-OC-011`, `FE-OC-025` | P4 | runtime schema, error classification, async UI |
| `feature-frontend-background-execution-worker-contract` | `FE-OC-032` | `FE-OC-009`, `FE-OC-016`, `FE-OC-017`, `FE-OC-025` | P4 | release/cache/rollback, API client |
`P4` 는 신규 우선순위 계층이다. 기존 P1~P3(핵심 skeleton)이 완료된 뒤 착수하는 opt-in capability 임을 뜻하며, §20 서두에 이 의미를 정의한다. §20 서두의 "27개 branch-note" 를 **33개** 로 고친다.
### 11.3 branch-note 파일 생성
`templates/branch-note-template.md` 로 6개 파일을 `raw/branch-notes/` 에 생성한다. **`/branch` 슬래시 명령은 이 repo 에 더 이상 존재하지 않으므로**(§13 참조) 템플릿에서 직접 만든다.
각 파일에서 채우는 것:
- frontmatter — `title`, `source_type: branch-note`, `status: raw`, `id: BR-CA-SKELETON-FRONTEND-OPERATIONAL-CONTRACT-0NN`, `kind: project-work-item`, `project`, `work_item`, `inherits`(§11.1 의 pin), `imports`(관련 `FE-OC-*@1`), `delegates`/`accepts_delegations`(§7.4), `contract_packet: 1`, `related_projects`, `tags`, `created: 2026-07-28`, `status_label: in-progress`
- `## 부모``[[raw/project-notes/ca-skeleton-frontend-operational-contract]]` (project 직접 자식이므로 `parent_branch:` 는 비움)
- `## 브랜치 계약 패킷` — 생성 시 project revision, 완료 조건(§11.1 문자열 그대로)
- `### 상속한 프로젝트 결정` — pinned pointer + Summary 문자열(§7.2 SSOT 와 정확히 일치) + branch 적용점 1줄
- `## 목표` / `## 범위` — 포함 범위는 port·adapter·registry·gate, **제외 범위에 "use case·domain model·business rule" 명시**
- `## 근거` — 이 설계 문서 링크 + "project-local default, 외부 source claim 아님" 명시
- `## 검증해야 할 주장` — 대응 gate 의 fixture 를 claim 으로 전개
**채우지 않는 것**: `## 구현 가이드` 는 헤딩과 R1/R2/R3 규칙 안내만 남기고 비워 둔다. 이 섹션은 근거 있는 결정에서 도출돼야 하며(CLAUDE.md §15.5), 근거 raw 수집 전에 채우면 전부 `UNSUPPORTED_IMPL_DECISION` 이 된다. 다음 단계에서 채운다.
### 11.4 §21.2 Cluster
`<!-- GENERATED: branches:start -->` 블록과 하단 수기 목록 양쪽에 6개 항목을 알파벳 순서로 삽입한다. 생성기가 없으므로 수기로 정렬을 맞춘다.
---
## 12. 편집 체크리스트
### 12.1 hub — `raw/project-notes/ca-skeleton-frontend-operational-contract.md`
| # | 섹션 | 변경 |
|---|---|---|
| 1 | frontmatter | `imports` 에 신규 `FE-OC-027@1`~`FE-OC-032@1`, `FE-GATE-027@1`~`FE-GATE-033@1` 중 hub 가 소비하는 것 추가; `last_reviewed: 2026-07-28` |
| 2 | §0.5 | In scope 6항 + Out of scope 2항 (§10.1) |
| 3 | §2.1 | `FE-OC-027`~`032` 행 6개 (§4.2); `FE-OC-022`·`FE-OC-025` summary 갱신 |
| 4 | §2.1.1 | 계약 행 6개 (§4.3) + gate 행 7개 (§8.2) |
| 5 | §2.1.2 | `DELEG-FE-007`~`011` (§7.4) |
| 6 | §2.1.4 | `FLOW-FE-EVENT-001`~`005` (§7.5) |
| 7 | §3.2 | `FE-D026`~`036` 신규 11행 (§7.1); `FE-D019``superseded`; `FE-D018` Decision 셀 갱신 |
| 8 | §6.1 | `DEC-…` 신규 11행 (§7.2); `OFFLINE-CACHE-001` revision 2; `REGISTRY-001` Summary 갱신; 개정 기록 2건 추가 (§7.3) |
| 9 | §4.2 | adapter 신규 10행 (§5.3) |
| 10 | §4.3 | `adapters/*` 예외 2건 + worker/SW 엔트리 규칙 (§5.4) |
| 11 | §4.4 | port 신규 12행 (§5.1) + "port 를 만들지 않는 두 가지" 문단 (§5.2) |
| 12 | §4.5 | boot order 12단계로 교체 (§5.5) |
| 13 | §4.6 | directory blueprint 확장 (§5.6) |
| 14 | §5.1 | `FE-REG-CAPABILITY` 행 추가 (8→9) |
| 15 | §5.3 | `FE-REG-API` 필드 5개 + sample 행 3개 + stream timeout 해석 문단 |
| 16 | §5.4 | `FE-REG-ENV` 신규 키 15개 |
| 17 | §5.5 | `FE-REG-STORAGE` 필드 2개 + backend enum 2값 + 행 4개 |
| 18 | §5.6 | `kind` enum 26종 추가 |
| 19 | §5.7 | `FE-REG-QUERY` 규칙 3행 |
| 20 | §5.8 | telemetry 이벤트 7개 + forbiddenAttributes 명시 |
| 21 | §5.9 | `serviceWorkerVersion` 토큰 |
| 22 | §5.11 (신설) | `FE-REG-CAPABILITY` minimum schema (§6.1) |
| 23 | §8.2 | failure matrix 신규 kind 행 (§10.2) |
| 24 | §9.5~§9.7 | realtime / transfer / SW surface state (§9.2~§9.4) |
| 25 | §14.2 | `FE-NFR-016`~`020` (§9.1) |
| 26 | §15.1 | gate 7행 + 서두 "26개"→"33개" (§8.3) |
| 27 | §15.2 | negative fixture 7행 (§8.4) |
| 28 | §15.3 | promotion rule 교체 (§8.5) |
| 29 | §16 | `FE-RB-006`·`FE-RB-007` (§9.5) |
| 30 | §17.1 | `FE-EV-014`·`FE-EV-015` |
| 31 | §18.1 | `NOT_APPLICABLE` 불변식 (§8.1) |
| 32 | §18.2 | `FE-RDY-009` 문구 + `FE-RDY-017` |
| 33 | §19.1 | `FE-RISK-013`~`018` |
| 34 | §19.2 | `FE-Q-011`~`014` + `FE-Q-009` 해소 기록 |
| 35 | §8.0 | WI 6행 + `WI-…-004` 완료 조건 보강 (§11.1) |
| 36 | §20 | branch 6행 + 서두 "27개"→"33개" + P4 정의 (§11.2) |
| 37 | §21.2 | Cluster 6항 (§11.4) |
| 38 | §22.2·§22.3 | answer boundary (§10.6) |
| 39 | §23.1 | 체크리스트 "8개 registry owner" → "9개", "runbook 5종" → "runbook 7종" |
| 40 | §25 | Next Steps 에 `P4 — Opt-in runtime capability` 절 신설, 6개 branch 항목 |
| 41 | §26 | Verification Status Summary 에 `opt-in runtime capability` 행 추가 (`planned`, "설계만 존재; adapter·gate evidence `UNVERIFIED`") |
| 42 | §21 `가져온 프로젝트 계약` + frontmatter `imports` | `FE-OC-027@1`~`FE-OC-032@1` 6행/6항 추가 — **2026-07-28 실행 중 정정**. 초판은 "hub 가 정의하는 계약이므로 제외" 라 적었으나, 이 표와 배열은 `FE-OC-002`~`025` 처럼 **branch 가 owner 인** 계약을 hub 가 pin 하는 곳이고 신규 6개도 같은 관계다. 제외하면 `FE-OC-026`(hub owner)과 구분이 사라진다. gate 는 기존에도 `FE-GATE-018`·`026` 2개만 pin 되어 있어 그 성긴 패턴을 유지한다 |
§24.2 drift check 는 손대지 않는다. 정규식 `FE-D[0-9]{3}``FE-REG-[A-Z]+(-[A-Z]+)*``FE-D026`~`FE-D036``FE-REG-CAPABILITY` 를 이미 포괄하고, definition set 추출 패턴 `^\| \`FE-` 도 신규 표 행 형식과 일치한다.
### 12.2 신규 파일 6개
`raw/branch-notes/` 에 §11.3 대로 생성.
### 12.3 기존 branch-note 7개 수정
| 파일 | 변경 사유 | 변경 |
|---|---|---|
| `feature-frontend-release-cache-rollback-contract` | 결정 revision + 위임 접수 | `inherits:` `OFFLINE-CACHE-001@1`→`@2`; 상속 표 Summary 교체; `accepts_delegations` 에 `DELEG-FE-011` |
| `feature-frontend-storage-registry-contract` | 문자열 파급 + 위임 접수 | `REGISTRY-001` Summary 문자열 교체; 본문의 "8개 registry 중 하나" 1곳; `accepts_delegations` 에 `DELEG-FE-009@1` + 수신 위임 표 |
| `feature-frontend-observability-logging-trace-contract` | 문자열 파급 | `REGISTRY-001` Summary 문자열 교체 |
| `feature-frontend-contract-registry-governance` | 문자열 파급 (본문 다수) | `REGISTRY-001` Summary 문자열 교체 + **본문 서술 11곳의 "8개/8 registry" → 9** (완료 조건·D1·목표·구현가이드·TODO·검증표). 이 branch 는 registry governance owner 라 개수를 여러 절에서 서술한다 — Summary 만 고치면 문서가 자기모순이 된다 |
| `feature-frontend-contract-compatibility-governance` | 문자열 파급 | `REGISTRY-001` Summary 문자열 교체 + 본문 서술 2곳 |
| `feature-runtime-schema-validation-contract` | 위임 접수 | `accepts_delegations` 에 `DELEG-FE-010` |
| `feature-frontend-env-runtime-config-contract` | 신규 owner 취득 | `FE-REG-CAPABILITY` registry owner 와 `FE-GATE-033` gate owner 로서 소유 표 갱신; `WI-…-004` 완료 조건 보강 반영 |
---
## 13. 알려진 drift (이번 범위 밖)
직전 커밋 `d71669e`("하네스 제거")가 `harness/` 와 `.claude/{commands,agents,hooks}` 를 삭제했다. 결과:
- `/branch`·`/branch-spec`·`/depth`·`/coverage`·`/lint`·`/sync` 슬래시 명령이 **존재하지 않는다**. 이 설계의 branch 스캐폴딩은 템플릿에서 직접 수행한다.
- hub §2.1.1 의 `harness/source/typed-contracts.json` 과 §2.1.3 의 `harness/source/artifact-schemas/ca-skeleton-frontend/*.schema.json` 은 **끊긴 경로**다.
- 결정론 검사기(`wiki_consistency_check.py`, `wiki_structure_lint.py`)가 없으므로 `CONFLICTS_WITH_PROJECT_DECISION`·`STALE_IMPORTED_CONTRACT`·`UNACCEPTED_DELEGATION` 은 자동 검출되지 않는다. §12 체크리스트의 문자열 정합은 **수기로** 확인해야 한다.
- 기존 branch-note 129개 전부가 frontmatter 에 `contract_packet_sha256` 을 갖는다. 이 해시를 계산하던 `branch_from_project.py` 가 삭제됐으므로 **신규 6개 노트에는 이 필드를 넣지 않았다.** 값을 지어내면 검증기가 복원됐을 때 조용히 통과하는 거짓 해시가 남는다. 기존 7개 노트를 수정하면서 그쪽 해시도 낡았다 — 생성기가 복원되면 33개 노트를 일괄 재계산해야 한다.
이 drift 는 이번 작업이 만든 것이 아니며 고치지 않는다. 다만 두 가지로 대응한다.
1. **신규 `ART-FE-*` 행을 만들지 않는다.** §2.1.3 의 등록 기준은 "두 개 이상 branch 가 같은 파일의 필드를 각자 정하는 artifact" 이고, 신규 7개 gate 의 report 는 전부 단일 branch 소유이므로 애초에 등록 대상이 아니다. 끊긴 schema 경로를 늘리지 않는 부수 효과가 있다.
2. hub §19.2 에 `FE-Q-011` 과 별개로 harness 복원 여부를 묻는 항목은 추가하지 않는다 — 이는 frontend 계약이 아니라 wiki 저장소 운영 이슈이므로 이 문서의 관심사가 아니다.
---
## 14. 승인 이력
| 일자 | 항목 | 결정 |
|---|---|---|
| 2026-07-28 | 분해 단위 | 6개 능력 도메인 |
| 2026-07-28 | 활성화 자세 | port + adapter 구현, capability flag default OFF |
| 2026-07-28 | `FE-D019` 충돌 | SW 역할 분리 후 supersede |
| 2026-07-28 | 산출물 범위 | hub 갱신 + branch-note 6개 스캐폴딩 |
| 2026-07-28 | registry | `FE-REG-CAPABILITY` 하나만 신설 → 9개 |
| 2026-07-28 | 근거 자료 | project-local default 로 명시 + open question 예약 |
| 2026-07-28 | 설계 B 전체 | 승인 |
## 15. 실행 기록
2026-07-28 에 `docs/superpowers/plans/2026-07-28-ca-skeleton-frontend-runtime-adapter-features.md` 의 12개 Task 로 전부 실행했다. 실행 중 이 문서를 세 곳 정정했다.
| 항목 | 초판 | 정정 | 사유 |
|---|---|---|---|
| §12.1 #42 | §21 `가져온 프로젝트 계약`·frontmatter `imports` 를 건드리지 않음 | `FE-OC-027@1`~`032@1` 추가 | 두 곳 모두 **branch-owned** 계약을 hub 가 pin 하는 자리다. 제외하면 hub-owned(`FE-OC-001`·`026`)와 구분이 사라진다 |
| §12.3 | `REGISTRY-001` Summary 문자열만 교체 | `contract-registry-governance` 본문 11곳, `compatibility-governance` 2곳, `storage-registry` 1곳의 개수 서술도 갱신 | Summary 만 고치면 같은 파일 안에서 "9개 registry 로 관리한다"와 "8개 registry 를 single-owner 로 관리한다"가 공존한다 |
| §13 | `contract_packet_sha256` 언급 없음 | 신규 6개 노트에 필드 미포함 + 기존 7개 해시가 낡았음을 명시 | 생성기 부재 상태에서 값을 지어내면 거짓 해시가 남는다 |
검증 결과(계획 §Task 12): 정의 없는 참조 0건, `FE-OC`·`FE-GATE` 정의 행 중복 이상 0건, 개수 정합 10항 전부 일치(FE-OC 32 / FE-GATE 33 / FE-D 36 / DEC 36 / FE-REG 9 / FE-NFR 20 / FE-RB 7 / WI 33 / 소속 노트 33 / Cluster 33), 미접수 위임 0건, 낡은 결정 문자열 0건, 깨진 wikilink 0건, 신규 문서의 과장 등급 주장 0건.