diff --git a/raw/project-notes/ca-skeleton-frontend-operational-contract.md b/raw/project-notes/ca-skeleton-frontend-operational-contract.md index 39594cc..42faa19 100644 --- a/raw/project-notes/ca-skeleton-frontend-operational-contract.md +++ b/raw/project-notes/ca-skeleton-frontend-operational-contract.md @@ -1379,6 +1379,32 @@ Raw response body, token, authorization header, full URL/query, stack, storage v | React render throws | `RENDER_FAILURE` | no auto retry | nearest boundary shell | retry route / reload action | component boundary + build ID | | telemetry endpoint/network fails | `TELEMETRY_FAILURE` | bounded internal queue only | console-safe/drop | no product error | self-metric, no recursion | | `QueryCachePort` read/write/invalidate throws or returns an invalid result | `QUERY_CACHE_FAILURE` | no automatic request retry | operation-declared uncached mode만 허용, 아니면 terminal | retry/support action; stale 표시를 위조하지 않음 | phase + query namespace, raw key/data 금지 | +| capability flag OFF 상태에서 해당 기능 진입 | `CAPABILITY_DISABLED` | no | `FE-REG-CAPABILITY`의 `disabledFallback` | fallback이 `feature-hidden`이면 진입점 자체를 노출하지 않음 | capability ID only | +| flag ON이지만 브라우저 feature detection 실패 | `CAPABILITY_UNSUPPORTED` | no | `FE-REG-CAPABILITY`의 `disabledFallback` | 대체 경로 안내, 브라우저 이름 단정 금지 | capability ID + reason enum | +| 사용자가 파일 선택 dialog를 닫음 | `FILE_PICKER_DISMISSED` | no | 이전 상태 유지 | **error surface 없음** — 취소는 실패가 아님 | error event 금지 | +| 선택 파일이 accept/size 제약 위반 | `FILE_REJECTED` | no | 선택 목록에서 제외 | 위반 제약을 필드 단위로 안내 | 제약 종류만, 파일명 금지 | +| IndexedDB/OPFS/Cache Storage 접근 불가 또는 security error | `BLOB_STORE_UNAVAILABLE` | no | registry `quotaFallback`; `없음`이면 terminal | 기능 저하 고지, 무음 처리 금지 | backend type + reason enum | +| 로컬 바이너리 quota 초과 | `BLOB_STORE_QUOTA_EXCEEDED` | no | `evictionOrder` 순 제거 후 재시도, `null` 행은 제거 금지 | 저장 실패 고지 + 정리 action | quota bucket, 값 금지 | +| 캐시 직렬화·영속·복원 실패 | `CACHE_PERSISTENCE_FAILURE` | no | 메모리 캐시만 사용 | 무음, 필요 시 stale 표시 | phase + tier only | +| BroadcastChannel과 `storage` event가 모두 불가 | `CROSS_TAB_CHANNEL_UNAVAILABLE` | no | 탭 내 무효화만 수행 | 무음, 다중 탭 stale 가능성 고지 가능 | reason enum only | +| presigned URL 만료/거부 | `PRESIGN_EXPIRED` | presign 재획득 후 1회 | 재획득 성공 시 같은 위치에서 재개 | 자동 재개, 재획득도 실패하면 retry action | operation ID only, URL 금지 | +| upload part 재시도 상한 소진 | `UPLOAD_PART_FAILED` | no (part 내부 재시도는 최대 2회) | part 상태 보존 후 일시정지 | 재개 action, 진행률 유지 | part index bucket + attempts | +| 체크섬 또는 크기 불일치 | `TRANSFER_INTEGRITY_MISMATCH` | no | 해당 part 폐기 후 재전송 1회, 재실패면 terminal | 무결성 실패 고지 + 처음부터 다시 action | size bucket only | +| 다운로드 스트림 중단 | `STREAM_INTERRUPTED` | `resumeStrategy: range`면 1회 | 받은 범위 보존 후 재개 | 재개 action | bytes bucket + resume 가능 여부 | +| transport status와 protocol status 불일치 (HTTP 200 + `grpc-status` 비0 등) | `PROTOCOL_STATUS_MISMATCH` | protocol status가 재시도 가능일 때만 safe/keyed | prior safe cache 또는 error | 서비스 응답 비호환 메시지 | protocol + status code group | +| protobuf/GraphQL 디코드 실패 | `CODEC_DECODE_FAILURE` | no | prior safe cache 또는 error | contract failure 메시지 | codec + operation ID, 본문 금지 | +| GraphQL `200 OK` + `errors[]` | `PARTIAL_RESULT_FAILURE` | no | 부분 데이터를 성공으로 취급하지 않음 | 실패한 필드 범위 안내 | error path count only, message 금지 | +| 최초 실시간 연결 수립 실패 | `REALTIME_CONNECT_FAILED` | full jitter backoff, 30s cap, 상한까지 | 마지막 스냅샷 유지 | `connecting` 유지 후 상한 도달 시 전이 | attempt bucket + transport | +| 재시도 상한 소진 후 terminal | `REALTIME_DISCONNECTED` | **no** — 자동 재시도는 이미 끝났음 | 마지막 스냅샷을 stale로 표시 | 사용자 주도 retry action | terminal 1회, attempts | +| resume cursor로 메울 수 없는 공백 감지 | `REALTIME_RESUME_GAP` | no | 권위 데이터 refetch 권고 | 공백 사실 표시 + 새로고침 action | gap 감지 방식 only | +| 인바운드 프레임이 `eventSchema` 위반 | `EVENT_SCHEMA_MISMATCH` | no | 해당 프레임만 드롭, 연결 유지 | 무음 (반복 시 저하 고지) | operation ID + issue path count | +| 알림 권한 거부 | `PUSH_PERMISSION_DENIED` | no | push 없이 계속 | 재요청 반복 금지, 설정 안내 1회 | 결과 enum only | +| push 구독 만료 | `PUSH_SUBSCRIPTION_EXPIRED` | 재구독 1회 | 재구독 실패 시 push 비활성 | 무음, 필요 시 재활성 action | 결과 enum only, endpoint 금지 | +| Worker 생성 불가 | `WORKER_UNAVAILABLE` | no | 메인 스레드 대체 경로 또는 기능 저하 | 무음 또는 느려짐 고지 | reason enum only | +| worker task timeout 후 terminate | `WORKER_TASK_TIMEOUT` | no | worker 종료 후 재생성 | 작업 실패 + retry action | duration bucket + task 이름 | +| service worker 등록/갱신 실패 | `SW_REGISTRATION_FAILED` | no | SW 없이 계속 — **제품 흐름 차단 금지** | 무음, 관련 기능만 저하 고지 | phase + reason enum | +| Background Sync API 미지원 | `BACKGROUND_SYNC_UNSUPPORTED` | no | 온라인 복귀 시 전면 재시도로 대체 | 지연 전송 불가 고지 | 결과 enum only | +| `idempotency: keyed`가 아닌 mutation의 재생 시도 | `BACKGROUND_SYNC_REPLAY_REJECTED` | **no** | 큐에서 제거, 재생하지 않음 | contact-support — 중복 write 방지가 우선 | operation ID + 거부 사유 | | unknown thrown value | `UNKNOWN_FAILURE` | no | nearest safe boundary | generic reference | type allowlist only | Normalization은 total function이어야 한다. response/adapter/browser exception이 위 named branch와 일치하지 않거나 mapper 자체가 실패하면 최종 catch-all이 raw value를 폐기하고 `UNKNOWN_FAILURE`를 반환한다. normalized failure를 만들지 못한 채 throw를 presentation으로 통과시키는 경로는 허용하지 않는다. @@ -1483,6 +1509,41 @@ Cache data가 release/config/API schema version과 incompatible하면 reuse하 - mutation/idempotency record처럼 correctness에 영향을 주는 값은 storage fallback을 임의 적용하지 않는다. - token, secret, raw API response, error body, PII는 default registry에 등록할 수 없다. +### 9.5 실시간 surface state + +§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.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`만 남긴다(§5.8). + +### 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`, 기능 저하만, 제품 흐름 차단 금지 | + --- ## 10. Rendering, Accessibility, and User Safety @@ -1743,9 +1804,16 @@ CI runner CPU와 throttling 값이 확정되지 않았으므로 command를 실 | `FE-NFR-013` | LCP field p75 | `FE-NFR-C03` | ≤ 2.5s | none | | `FE-NFR-014` | CLS field p75 | `FE-NFR-C03` | ≤ 0.10 | none | | `FE-NFR-015` | INP field p75 | `FE-NFR-C03` | ≤ 200ms | none | +| `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 | Field Web Vitals는 consent/privacy boundary, route-ID aggregation, 28-day window, production release ID를 함께 기록해야 한다. minimum eligible sample threshold는 telemetry baseline을 얻은 뒤 owner가 확정할 `deferred` decision이므로, 그 전에는 `FE-GATE-018`을 PASS로 올릴 수 없다. lab result를 production percentile로 표현하지 않는다. +`FE-NFR-020`은 `FE-NFR-001`(initial JS gzip ≤ 200 KiB) 예산을 신규 capability가 잠식하지 않음을 수치로 방어하는 항목이다. capability를 켜면 번들이 늘어나는 것은 정상이며, 이 NFR이 막는 것은 **끄고도 늘어나는** 경우다. + ### 14.3 Planned commands and expected assertions 아래 command는 repository가 생긴 뒤 package script로 제공할 contract다. **이 검토에서 실행되지 않았다.**