From 4533122f9db04a3715c03d1bd2f80aec149f37d4 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Tue, 28 Jul 2026 13:52:46 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20ca-skeleton=20frontend=20=EB=9F=B0?= =?UTF-8?q?=ED=83=80=EC=9E=84=20adapter=20feature=20=ED=99=95=EC=9E=A5=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 요청된 9개 능력 묶음을 기존 26개 FE-OC 계약과 대조해 순수 델타 6개 능력 도메인(FE-OC-027~032)으로 분해한 설계 스펙. - port 12개 신규, 멀티프로토콜은 기존 output port 재사용(신규 port 0) - capability flag default OFF + FE-GATE-033 이 번들 부재를 증명 - FE-D019(SW default off)를 FE-D034 로 supersede — precaching 은 OFF 유지, push/background sync/Cache Storage 호스트만 opt-in - FE-REG-CAPABILITY 신설(8→9), 나머지 8개 registry 는 additive 확장 - gate 7 · NFR 5 · runbook 2 · FLOW-FE-EVENT-001~005 추가 - 신규 결정 11건은 근거 raw 부재를 project-local default 로 명시 범위는 port·adapter·registry·gate 까지이며 use case·domain model· business rule 은 명시적 out of scope. Co-Authored-By: Claude Opus 5 (1M context) --- ...rontend-runtime-adapter-features-design.md | 904 ++++++++++++++++++ 1 file changed, 904 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-28-ca-skeleton-frontend-runtime-adapter-features-design.md diff --git a/docs/superpowers/specs/2026-07-28-ca-skeleton-frontend-runtime-adapter-features-design.md b/docs/superpowers/specs/2026-07-28-ca-skeleton-frontend-runtime-adapter-features-design.md new file mode 100644 index 0000000..254908d --- /dev/null +++ b/docs/superpowers/specs/2026-07-28-ca-skeleton-frontend-runtime-adapter-features-design.md @@ -0,0 +1,904 @@ +# 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 다. + +| 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 의 기존 모든 행과 동일). + +### 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 의 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, 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 + +`` 블록과 하단 수기 목록 양쪽에 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) | + +### 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 문자열 교체; `accepts_delegations` 에 `DELEG-FE-009` | +| `feature-frontend-observability-logging-trace-contract` | 문자열 파급 | `REGISTRY-001` Summary 문자열 교체 | +| `feature-frontend-contract-registry-governance` | 문자열 파급 | `REGISTRY-001` Summary 문자열 교체 | +| `feature-frontend-contract-compatibility-governance` | 문자열 파급 | `REGISTRY-001` Summary 문자열 교체 | +| `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 체크리스트의 문자열 정합은 **수기로** 확인해야 한다. + +이 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 전체 | 승인 |