From a19e1c26aa340bd13bdef5ecf218be91a3ef8d4d Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Tue, 28 Jul 2026 14:17:24 +0900 Subject: [PATCH] =?UTF-8?q?docs(hub):=20FE-REG-CAPABILITY=20=EC=8B=A0?= =?UTF-8?q?=EC=84=A4(8->9)=EA=B3=BC=20=EA=B8=B0=EC=A1=B4=208=EA=B0=9C=20re?= =?UTF-8?q?gistry=20additive=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - §5.11 capability registry — envFlagKey 는 FE-REG-ENV 행 포인터이며 키의 타입·검증은 env registry 가 계속 소유(재진술 금지) - FE-REG-API 에 protocol/transferMode/operationRef/eventSchema/ resumeStrategy. 실시간 구독은 별도 장부가 아니라 stream operation 이다. stream 행의 timeoutMs 는 연결 수립 timeout 이지 스트림 수명이 아니다 - FE-REG-STORAGE 에 opfs/cacheStorage backend, payloadClass, evictionOrder. UPLOAD_PART_STATE 는 correctness 값이라 quota fallback 없음 - error kind 26종. TRANSFER_ABORTED/SUBSCRIPTION_LEAKED/SW_UPDATE_PENDING 은 각각 기존 kind 재사용/gate 결함/surface state 라 제외 --- ...-skeleton-frontend-operational-contract.md | 131 ++++++++++++++++-- 1 file changed, 122 insertions(+), 9 deletions(-) diff --git a/raw/project-notes/ca-skeleton-frontend-operational-contract.md b/raw/project-notes/ca-skeleton-frontend-operational-contract.md index 0753deb..39594cc 100644 --- a/raw/project-notes/ca-skeleton-frontend-operational-contract.md +++ b/raw/project-notes/ca-skeleton-frontend-operational-contract.md @@ -718,6 +718,7 @@ artifacts/ | `FE-REG-QUERY` | query key factory/invalidation | `src/contracts/query-keys.js` | `feature-server-state-caching-contract` | page 안에서 ad hoc array key 생성 | | `FE-REG-TELEMETRY` | event/attribute/redaction | `src/contracts/telemetry.js` | `feature-frontend-observability-logging-trace-contract` | 자유 문자열 event 전송 | | `FE-REG-RELEASE` | build/config/API/release token | `src/contracts/release-tokens.js` | `feature-frontend-release-cache-rollback-contract` | string version 비교 또는 cache key 직접 작성 | +| `FE-REG-CAPABILITY` | capability 활성 조건/owner/gate/fallback | `src/contracts/capabilities.js` | `feature-frontend-env-runtime-config-contract` | registry 없이 config를 직접 읽어 기능 분기 | ### 5.2 Route registry minimum schema @@ -755,6 +756,13 @@ Initial planned rows: | `requestSchema` | yes | body가 없으면 explicit `none`; params/search도 검증 | | `responseSchema` | yes | success envelope의 payload schema reference | | `owner` | yes | owning feature or branch slug | +| `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**을 뜻하고 스트림 총 수명에는 적용하지 않는다. 스트림 수명은 `FE-D033`의 재연결·terminal 전이 계약이 소유한다. 이 구분이 없으면 정상적으로 살아 있는 구독이 10초마다 timeout으로 끊긴다. Initial planned rows: @@ -763,6 +771,16 @@ Initial planned rows: | `LIST_SAMPLE_RESOURCES` | `GET` | `/api/sample/resources` | `external-session` | `10000` | `safe` | `SampleResourceListQuery` | `SampleResourceListPayload` | `feature-sample-feature-slice-contract-fixture` | | `CREATE_SAMPLE_RESOURCE` | `POST` | `/api/sample/resources` | `external-session` | `10000` | `keyed` | `CreateSampleResourceCommand` | `SampleResourcePayload` | `feature-sample-feature-slice-contract-fixture` | +Capability 확장 field를 쓰는 planned rows (전부 `feature-sample-feature-slice-contract-fixture` owner, 대응 capability가 OFF면 등록만 되고 호출되지 않음): + +| operationId | method | path | protocol | transferMode | idempotency | operationRef | eventSchema | resumeStrategy | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| `STREAM_SAMPLE_EVENTS` | `GET` | `/api/sample/events` | `sse` | `stream` | `safe` | `none` | `SampleEventUnion` | `last-event-id` | +| `PRESIGN_SAMPLE_UPLOAD` | `POST` | `/api/sample/uploads/presign` | `rest` | `unary` | `keyed` | `none` | — | `none` | +| `DOWNLOAD_SAMPLE_OBJECT` | `GET` | `/api/sample/objects/:id/content` | `rest` | `download` | `safe` | `none` | — | `range` | + +`PRESIGN_SAMPLE_UPLOAD`는 presign을 **획득**하는 operation이며 byte 전송이 아니다. 실제 전송은 registry에 등록하지 않는 제3자 URL로 나가고, `FE-D029`에 따라 session credential을 첨부하지 않는다. + ### 5.4 Environment registry minimum schema | Key | Phase | Classification | Required | Default | Failure | @@ -781,30 +799,57 @@ Initial planned rows: | `CONFIG_SCHEMA_VERSION` | runtime | public | yes | none | compatibility fail | | `API_CONTRACT_VERSION` | runtime | public | yes | none | compatibility fail | | `RELEASE_MANIFEST_URL` | runtime | public | yes | `/release-manifest.json` | mismatch detection degrade/fail per policy | +| `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` | 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 | `public-sensitive`는 browser에서 볼 수 있지만 로그·telemetry에 원문을 남기지 않는 endpoint-like value를 뜻한다. secret 분류가 아니다. +`PUSH_PUBLIC_KEY`는 VAPID **공개**키이므로 `public`이다. 대응 개인키는 backend 소유이며 이 registry에 등록할 수 없다. + +capability flag 6개의 기본값이 전부 `false`인 것은 `FE-D026`의 default-OFF 자세이며, 이 기본값 조합으로 build했을 때 대응 adapter가 번들에 없어야 한다는 사실은 `FE-GATE-033`이 검증한다. + ### 5.5 Storage registry minimum schema | Field | Required | Rule | | --- | --- | --- | | `logicalName` | yes | 의미 이름, raw key가 아님 | | `physicalKey` | yes | `::v:` | -| `backend` | yes | `memory`, `sessionStorage`, `localStorage`, `indexedDB` | +| `backend` | yes | `memory`, `sessionStorage`, `localStorage`, `indexedDB`, `opfs`, `cacheStorage` | | `classification` | yes | `public-preference`, `opaque-cache`, `sensitive-forbidden` | | `schemaVersion` | yes | incompatible change 시 increment | | `ttl` | conditional | persistent cache는 expiry 필수 | | `migration` | conditional | previous version을 읽으면 migration 또는 discard | -| `quotaFallback` | yes | memory/no-persist/feature-disable 중 하나 | +| `quotaFallback` | yes | memory/no-persist/feature-disable 중 하나. correctness에 영향을 주는 값은 `없음`으로 선언 | +| `payloadClass` | yes | `structured`(기본) 또는 `binary`. `binary`는 `sensitive-forbidden` classification과 조합할 수 없음 | +| `evictionOrder` | yes | quota 압박 시 제거 순서(정수, 낮을수록 먼저). 제거 불가 값은 `null` | Initial planned rows: -| logicalName | backend | classification | TTL / fallback | -| --- | --- | --- | --- | -| `COLOR_SCHEME` | `localStorage` | `public-preference` | no TTL / system default | -| `CHUNK_RELOAD_GUARD` | `sessionStorage` | `opaque-cache` | session / no second auto reload | -| `QUERY_PERSISTENCE` | disabled | `sensitive-forbidden` default | opt-in contract required | -| `AUTH_TOKEN` | forbidden | `sensitive-forbidden` | external auth owner only | +| logicalName | backend | classification | payloadClass | TTL / fallback | evictionOrder | +| --- | --- | --- | --- | --- | --- | +| `COLOR_SCHEME` | `localStorage` | `public-preference` | `structured` | no TTL / system default | `4` | +| `CHUNK_RELOAD_GUARD` | `sessionStorage` | `opaque-cache` | `structured` | session / no second auto reload | `null` | +| `QUERY_PERSISTENCE` | disabled | `sensitive-forbidden` default | `structured` | opt-in contract required | — | +| `AUTH_TOKEN` | forbidden | `sensitive-forbidden` | — | external auth owner only | — | +| `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` | + +`UPLOAD_PART_STATE`의 `quotaFallback`이 `없음`이고 `evictionOrder`가 `null`인 것은 §9.4의 "correctness에 영향을 주는 값은 storage fallback을 임의 적용하지 않는다"를 이 행에 적용한 것이다. part 상태가 조용히 memory로 넘어가면 새로고침 한 번에 이어받기가 불가능해지고, 사용자는 그 사실을 전송이 실패한 뒤에야 알게 된다. ### 5.6 Error registry minimum schema @@ -846,9 +891,37 @@ STORAGE_QUOTA_EXCEEDED RENDER_FAILURE TELEMETRY_FAILURE QUERY_CACHE_FAILURE +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 UNKNOWN_FAILURE ``` +의도적으로 만들지 않은 kind가 셋 있다. `TRANSFER_ABORTED`는 기존 `REQUEST_ABORTED`를 재사용한다. `SUBSCRIPTION_LEAKED`는 사용자에게 보이는 실패가 아니라 `FE-GATE-031`이 잡는 **결함**이므로 kind가 아니라 fixture 이름이다. `SW_UPDATE_PENDING`도 실패가 아니라 §9.7의 surface state다. + ### 5.7 Query key registry minimum schema Query key는 factory로만 생성한다. @@ -867,6 +940,9 @@ queryKeys.resource.detail(resourceId) | invalidation | mutation outcome과 mapping된 factory만 invalidate | | version | API/schema breaking change 시 namespace version bump | | persistence | default disabled; opt-in 시 release/config version partition | +| `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`을 파티션 키에 포함. 불일치 시 복원하지 않고 폐기 | ### 5.8 Telemetry registry minimum schema @@ -876,7 +952,7 @@ queryKeys.resource.detail(resourceId) | `trigger` | yes | 발생 시점 단일 정의 | | `requiredAttributes` | yes | low-cardinality only | | `optionalAttributes` | yes | absence-safe | -| `forbiddenAttributes` | yes | token, email, raw URL/query/body, storage value | +| `forbiddenAttributes` | yes | token, email, raw URL/query/body, storage value, 파일명, object key, presigned URL, 구독 endpoint | | `sampling` | yes | error/security event는 별도 정책 | | `delivery` | yes | best-effort, audit channel 아님 | @@ -889,6 +965,13 @@ Initial planned events: | `ui.render.failed` | React boundary catch | `route_id`, `build_id`, `component_boundary` | | `release.mismatch.detected` | chunk/config/API version mismatch | `build_id`, `active_release_id`, `mismatch_kind` | | `telemetry.delivery.dropped` | sink/queue failure | `reason`, `queue_size_bucket` | +| `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` | service worker 신 버전 활성화 | `build_id`, `previous_build_id` | +| `background.sync.replayed` | background sync 재생 결과 | `operation_id`, `outcome` | ### 5.9 Release token registry minimum schema @@ -902,6 +985,7 @@ Initial planned events: | `assetManifestHash` | build output | chunk integrity/mismatch | | `releaseId` | deploy system | rollback target | | `builtAt` | CI | diagnostics, not cache identity | +| `serviceWorkerVersion` | build output | service worker 스크립트와 release의 coherence. rollback 시 SW도 함께 되돌아갔는지 판정 (`FE-D034`, `DELEG-FE-011`) | ### 5.10 Registry change protocol @@ -914,6 +998,35 @@ Initial planned events: 7. release note에 affected `FE-OC-*`와 rollback condition을 적는다. 8. orphan token scan이 0건이어야 merge할 수 있다. +### 5.11 Capability registry minimum schema + +> 이 절은 §5.10 **뒤에 붙였다**. §5.5 뒤에 끼워 넣으면 §5.6~§5.10이 밀려 이 문서와 27개 branch-note의 절 참조가 전부 어긋난다. 절 번호 순서보다 기존 참조 안정성이 우선이다. + +`FE-REG-CAPABILITY`는 `FE-D026`이 정한 default-OFF 자세를 강제하는 장부다. flag의 타입·기본값·검증은 `FE-REG-ENV`가 계속 소유하고, 이 registry는 **활성화 계약**(누가 소유하고, 어떤 gate가 지켜보고, 못 켜면 무엇으로 대체하는가)만 가진다. + +| Field | Required | Rule | +| --- | --- | --- | +| `capabilityId` | yes | stable `UPPER_SNAKE_CASE`; rename은 breaking | +| `envFlagKey` | yes | `FE-REG-ENV` 행 **포인터**. 키의 타입·기본값·검증 규칙을 여기 재진술하지 않음 | +| `owner` | yes | 소유 branch slug | +| `requiredGate` | yes | 활성화 시 반드시 PASS해야 하는 `FE-GATE-*` | +| `browserRequirement` | yes | feature detection 식별자. 미지원이면 flag가 ON이어도 활성화되지 않음 | +| `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` | + +`src/workers/*.worker.js` 또는 `public/sw.js`가 `domain` 순수 모듈을 참조하면 그 사실을 해당 행에 기록한다(§4.3). + --- ## 6. Build-time, Runtime, Secret Configuration