# Storage / browser-file adapters 구현 준비 코드 리뷰 검토 저장소: `/home/donghyeon/workspace/desktop-server-git/clean-architecture-frontend-template` 검토 범위: `src/adapters/storage/**`, `src/adapters/browser-files/**`, `src/adapters/browser-file-storage/**`, `src/adapters/cache-storage/**` 및 직접 연결된 application port, contract, bootstrap, test, architecture/operations 문서 검토 방식: 구현 파일을 수정하지 않은 read-only 리뷰. 아래 line은 현재 worktree 기준이다. ## 0. 결론과 우선순위 | ID | 판정 | 심각도 | 확신도 | 요약 | | --- | --- | --- | --- | --- | | STO-01 | 확정 결함 | **Critical** | 높음 | OPFS pre-commit 보상 cleanup 실패/취소를 무시하고 journal을 rollback한다. 늦게 도착한 generation-only cleanup이 후속 write의 같은 logical generation을 삭제할 수 있고, 그렇지 않아도 복구 근거와 quota를 잃는다. | | STO-02 | 확정 결함 | **High** | 높음 | browser-managed download는 `baseOrigin`으로 상대 URL을 검증하지만 원문 `href`를 `document.baseURI`로 실행한다. ``가 있으면 검증한 origin과 실제 navigation origin이 달라진다. | | STO-03 | 확정 결함 | **Medium** | 높음 | public cache policy가 `allowedVaryHeaderNames`를 허용하면서 response allowlist에서 `vary`를 제거하는 모순을 허용한다. stage는 성공할 수 있지만 저장 variant가 충돌하고 activation이 실패한다. | | STO-04 | 확정 결함 | **Medium** | 높음 | 동일 manifest 재-stage가 marker와 count만 신뢰한다. marker 작성 뒤 browser eviction/부분 손상된 candidate를 성공으로 재사용하여 self-heal하지 못한다. activation은 fail-closed지만 staging success 의미가 약해진다. | | STO-05 | 확정 결함 | **Medium** | 높음 | cache `activateRelease`/`cleanupOwned`가 네트워크 fetch를 쓰지 않는데도 공통 availability guard가 `fetcher`를 필수로 요구한다. offline activation/rollback/cleanup이 불필요하게 `UNSUPPORTED`가 된다. | | STO-06 | 확정 계약 위반 | **Medium** | 높음 | IndexedDB codec migration은 commit transaction 내부의 연속 native operation 사이에 monotonic deadline을 재확인하지 않는다. 문서/port의 cooperative duration contract보다 오래 실행될 수 있다. | | STO-07 | hardening 후보 | **Medium** | 높음 | OPFS worker envelope에 protocol version/response kind가 없고 client response parser가 `{requestId, ok}`만 검사한다. page/worker release 불일치와 malformed response를 `INCOMPATIBLE`로 닫을 수 없다. | | STO-08 | 브라우저 검증 필요 | **Low** | 중간 | enhanced open/save picker 함수를 `Window`가 아니라 options 객체에 bind한다. Web IDL brand check가 있는 engine에서는 `Illegal invocation` 가능성이 있으나 현재 unit fake는 이를 검증하지 않는다. 실제 browser test로 먼저 확정한다. | | GAP-01 | 문서화된 미구현 | **High readiness gap** | 높음 | preview pixel/decoded-byte/frame/decode probe가 없다. VD-15가 이미 `DESIGNED_NOT_IMPLEMENTED`로 명시했으므로 regression으로 오인하지 말고, untrusted image preview 조립의 promotion blocker로 취급한다. | | GAP-02 | 문서화된 미구현 | **High readiness gap** | 높음 | Cache inspect/cleanup은 cursor/count/deadline 없이 전체 owned namespace를 순회한다. VD-15가 정확히 현 상태를 기록한다. | | GAP-03 | 문서화된 미구현 | **High readiness gap** | 높음 | origin-wide pressure/write-admission/GC, OPFS/Cache forward migration, real OPFS preflight가 아직 없다. 기존 per-store primitive를 완성 증거로 삼지 않는다. | 즉시 순서는 **STO-01 write 차단/수정 → STO-02 canonical URL 실행 → STO-03~05 cache 불변식 → STO-06/07 hardening**이다. GAP 항목은 해당 capability를 제품에 선택·조립하기 전에 별도 promotion gate로 구현한다. ## 1. 누락 없는 범위 inventory: 책임과 의존성 ### 1.1 `browser-file-storage` | 파일 | 책임 | 주요 의존성 / 리뷰 결과 | | --- | --- | --- | | `src/adapters/browser-file-storage/index.ts` | browser data 공통 Result와 StorageManager adapter barrel export | 내부 두 모듈만 export. 경계가 작고 유지 대상. | | `src/adapters/browser-file-storage/result.ts` | native 예외를 closed `BrowserDataFailure`로 정규화하고 안전한 observation 제공 | `application/ports/browser-file-storage/shared.ts`; raw path/name/message 비노출, observer 예외 격리가 좋다. | | `src/adapters/browser-file-storage/storage-manager-adapter.ts` | `estimate/persisted/persist` snapshot, pressure bucket, user-activation-bound persistence 요청 | storage durability port/result. estimate를 예약량으로 오인하지 않고 irreversible `persist()` truth를 보존한다. origin coordinator는 의도적으로 없음(GAP-03). | ### 1.2 `browser-files` | 파일 | 책임 | 주요 의존성 / 리뷰 결과 | | --- | --- | --- | | `src/adapters/browser-files/browser-file-picker.ts` | native input baseline 및 enhanced system picker, activation/abort/dismissal, vault capture | file port, vault, policy registry. baseline/enhancement 분리가 좋다. `showOpenFilePicker.bind(options)`는 STO-08. | | `src/adapters/browser-files/browser-file-policy-registry.ts` | composition-owned selection/inspection/preview/download policy 등록·identity 확인·hard-cap reduction | file contracts, `file-policy.ts`. `WeakSet`/identity binding과 frozen snapshot을 유지한다. | | `src/adapters/browser-files/browser-file-vault.ts` | transient native File/handle 보관, opaque ref, inspection receipt, bounded range/source | file port/shared/result/policy registry. File이 application 경계를 넘지 않고 receipt가 exact file/profile에 묶이는 설계가 좋다. | | `src/adapters/browser-files/create-browser-file-runtime.ts` | vault/picker/preview/download를 선택적으로 조립하고 일괄 dispose | 위 adapters 및 application contracts. optional capability를 제품 선택 없이 bootstrap에 암묵 조립하지 않는 점을 유지. preview 조립 전 GAP-01 gate 필요. | | `src/adapters/browser-files/download-delivery-adapter.ts` | browser handoff, foreground save stream, bounded object URL download, integrity/progress/cancellation | file/authorized-download ports, policy registry, object URL lease, Result. STO-02와 STO-08; stream close truth/backpressure는 유지. | | `src/adapters/browser-files/file-observer.ts` | file-safe observation DTO를 공통 browser observation으로 변환 | shared port/result. raw filename/ref 비노출 유지. | | `src/adapters/browser-files/file-policy.ts` | policy input validation, MIME/extension/signature/hard byte caps, immutable resolved policy | file/shared contracts. closed allowlist 및 absolute ceiling을 유지. | | `src/adapters/browser-files/index.ts` | browser-file public exports | 위 모듈. native implementation detail export 확장을 피한다. | | `src/adapters/browser-files/object-url-lease.ts` | 중앙 object URL lease cap/registry, transient preview, idempotent revoke/dispose | file/shared contracts, vault, policy registry. URL lifecycle은 좋으나 `create()` 256-303은 decode probe 없이 URL을 발급(GAP-01). | ### 1.3 `cache-storage` | 파일 | 책임 | 주요 의존성 / 리뷰 결과 | | --- | --- | --- | | `src/adapters/cache-storage/index.ts` | public cache policy/adapter barrel | optional public static cache만 export; private/range cache로 일반화하지 않는다. | | `src/adapters/cache-storage/public-cache-policy.ts` | same-origin/public-only release 정책, URL/header/query/size/retention hard limits | cache ports/shared. STO-03 policy cross-field invariant 누락. 기본 policy에는 `vary`가 있어 기본-path 테스트는 통과한다. | | `src/adapters/cache-storage/public-response-cache-adapter.ts` | manifest canonicalization/digest, anonymous fetch, bounded body 검증, candidate marker-last staging, explicit activation, exact lookup/reverify, owned cleanup/inspect | cache ports/result/policy, CacheStorage/fetch/Crypto/Web Lock snapshot. STO-03~05 및 GAP-02. private/auth/opaque/206 거절과 current+previous 보존은 유지. | ### 1.4 `storage` root / IndexedDB | 파일 | 책임 | 주요 의존성 / 리뷰 결과 | | --- | --- | --- | | `src/adapters/storage/browser-storage-adapter.ts` | registry key별 local/session/memory 저장, TTL, failure overlay/tombstone, quota fallback | `storage-keys`, `StoragePort`, codec, diagnostics. strict registry와 stale persistent suppression을 유지. adjacent physical-key migration/sweep는 문서상 미구현. | | `src/adapters/storage/browser-storage-codec.ts` | bounded exact JSON envelope, exotic/accessor/unsafe-key/cycle/depth/node 거절 | 독립 codec. prototype pollution/JSON silent coercion 방어가 좋다. | | `src/adapters/storage/indexeddb/index.ts` | IndexedDB runtime/maintenance/governance/migration export | native IDB type을 application port 밖으로 내보내지 않는 구조 유지. | | `src/adapters/storage/indexeddb/indexeddb-failure.ts` | IDB/DOM failure를 closed browser failure로 변환 | common Result. raw native detail 비노출 유지. | | `src/adapters/storage/indexeddb/indexeddb-governance.ts` | opaque dataset scope/physical DB identity 및 frozen policy binding | indexeddb/shared ports. account/business ID를 physical name에 쓰지 않는 양방향 binding 유지. | | `src/adapters/storage/indexeddb/indexeddb-maintenance.ts` | post-open codec migration 및 idempotency receipt prune, keyset checkpoint, budget/revision fencing | IndexedDB port/types/failure/governance. async transform outside tx, row+sidecar+budget+checkpoint atomic commit은 좋다. STO-06 및 temporal drain lease 개선 후보. | | `src/adapters/storage/indexeddb/indexeddb-migrations.ts` | additive-only contiguous DDL planner/validator | indexeddb types. destructive DDL 거절 유지. | | `src/adapters/storage/indexeddb/indexeddb-runtime.ts` | generic repository open/read/query/CAS/delete, idempotency, retention, lifecycle purge, connection lifecycle | indexeddb ports/types/governance/failure/migrations. transaction `complete` truth, versionchange close, shared open/abort isolation, exact budgets 유지. lifecycle proof는 현재 문서 계약(형식 검증 후 폐기)과 일치하므로 결함으로 분류하지 않았다. | | `src/adapters/storage/indexeddb/indexeddb-types.ts` | adapter-local codec/query/schema/dependency contracts | application indexeddb/shared ports. `isOldWriterDrainConfirmed()` boolean은 provider가 전체 window를 보장한다는 문서 전제; lease형으로 강화 권고. | ### 1.5 `storage/opfs` | 파일 | 책임 | 주요 의존성 / 리뷰 결과 | | --- | --- | --- | | `src/adapters/storage/opfs/browser-opfs-runtime.ts` | OPFS support inspection 및 journal/worker/byte-store composition | OPFS ports, journal, byte-store, policy, worker client. property probe를 real readiness로 주장하지 않음(GAP-03). | | `src/adapters/storage/opfs/index.ts` | OPFS runtime/journal/policy/protocol/client exports | optional capability barrel. | | `src/adapters/storage/opfs/indexeddb-opfs-journal.ts` | logical object/journal/budget/chunk refcount의 IDB authority; begin/files-ready/commit/rollback/reconcile pages | OPFS ports, IDB failure, policy. journal+object+budget CAS atomicity가 좋다. STO-01 수정에서 incomplete row를 cleanup 확인 전 삭제하지 않아야 한다. | | `src/adapters/storage/opfs/opfs-byte-store-adapter.ts` | logical journal과 physical worker를 saga로 조정, put/open/remove, reconcile/policy maintenance | OPFS/shared ports, journal, worker gateway, policy. STO-01의 journal/physical compensation ordering 결함 위치. | | `src/adapters/storage/opfs/opfs-policy.ts` | root/lock/chunk/object/RPC/reconcile/GC hard limits 및 scope validation | shared/opfs ports. opaque physical path와 absolute caps 유지. | | `src/adapters/storage/opfs/opfs-worker-client.ts` | request correlation/timeout/abort/transferable chunking, worker gateway, streamed reads | protocol/policy/shared Result. STO-01의 untracked abort cleanup 및 STO-07의 shallow response parse. | | `src/adapters/storage/opfs/opfs-worker-protocol.ts` | page↔DedicatedWorker request/response union 및 gateway contract | OPFS/shared ports. STO-07; protocol version/kind/effect certainty 추가 필요. | | `src/adapters/storage/opfs/opfs-worker-runtime.ts` | DedicatedWorker OPFS physical layout, lock lease, immutable chunk, manifest/staging receipt, abort/finalize/remove/GC | protocol/policy/OPFS ports/Web Lock/Crypto. STO-01의 generation-only cleanup과 lease release 순서. sync handle `finally close` 등은 유지. | ### 1.6 직접 연결 경계와 조립 - `src/application/ports/browser-file-storage/{shared,file,indexeddb-port,opfs-ports,cache-storage-ports,storage-durability-port}.ts`와 barrel을 읽었다. native `File/Blob/Cache/IDB*/Response/ReadableStream`을 application으로 노출하지 않는 포트 방향은 올바르다. - `src/application/ports/storage-port.ts`, `src/contracts/storage-keys.ts`를 대조했다. Web Storage는 registry-owned typed key만 허용한다. - `src/bootstrap/runtime-adapters.ts:17,260-266`은 Web Storage만 기본 조립한다. file/IndexedDB/OPFS/Cache가 없는 것은 문서의 `AVAILABLE_NOT_COMPOSED`와 일치하며 결함이 아니다. ## 2. 구체적 findings와 구현 방법 ### STO-01 — OPFS 보상 cleanup이 journal보다 늦게 완료되거나 실패할 때 후속 generation 삭제 가능 **근거와 실패 연쇄** 1. 새 logical generation은 현재 committed generation+1로 재사용된다: `src/adapters/storage/opfs/opfs-byte-store-adapter.ts:158-195`(특히 183-195). 2. `preparePut` 또는 `markFilesReady` 실패 시 `rollbackBestEffort`를 호출한다: 같은 파일 `222-242`. 3. `rollbackBestEffort`는 `worker.cleanupTransaction(..., callerSignal)`의 `BrowserDataResult`를 검사하지 않고, 곧바로 `journal.rollback`을 호출한다: `833-845`. caller signal이 이미 abort되었으면 cleanup RPC는 시작조차 못 한다. 4. worker client도 prepare 단계 실패/timeout 때 별도의 un-signaled `ABORT_PUT`을 보내지만 timeout/실패를 삼키며 “journal reconciliation이 반복한다”고 가정한다: `src/adapters/storage/opfs/opfs-worker-client.ts:190-198,229-307`. 그런데 3번이 journal row를 삭제한다. 5. physical cleanup은 staging receipt에서 `(scope, objectId, generation)`만 읽어 해당 generation 디렉터리를 삭제한다: `src/adapters/storage/opfs/opfs-worker-runtime.ts:584-607,717-761`. manifest/path에 transaction-unique physical generation identity가 없다. 6. `abortPut`은 mutation lease를 먼저 release한 뒤 generation 삭제를 수행한다: 같은 파일 `501-529`(특히 519-527). `cleanupTransaction` 자체도 mutation lease를 얻지 않는다. 따라서 T1 cleanup RPC가 timeout 뒤 worker에서 계속되거나 T1 `ABORT_PUT`이 늦게 실행되는 동안 coordinator가 T1 journal을 rollback하면 T2가 같은 object의 동일 logical generation을 다시 시작할 수 있다. 늦은 T1 cleanup은 T2의 물리 디렉터리를 삭제할 수 있다. 삭제까지 겹치지 않아도 journal 부재로 stale staging/immutable chunks가 영구 잔존해 quota pressure를 만든다. **패턴과 수정** - cross-API ACID를 주장하지 말고 **durable saga + transactional outbox/compensation state**를 유지한다. - “physical cleanup confirmed” 전에는 PREPARING/FILES_READY journal row와 budget reservation을 rollback하지 않는다. cleanup은 caller signal과 분리한 composition-owned bounded signal을 사용한다. - worker client 내부에서 fire-and-forget abort를 중복 발행하지 않는다. coordinator가 `abortPreparedPut()` 한 번을 소유하고 결과가 `CLEANED|ALREADY_CLEAN`일 때만 journal rollback한다. timeout/crash는 `EFFECT_UNKNOWN`으로 남겨 reconcile한다. - 장기적으로 **transaction-unique physical generation/fencing token**을 path, receipt, manifest, journal에 저장한다. stale T1 cleanup은 T1 token 경로만 삭제하고 T2를 건드릴 수 없어야 한다. - cleanup/abort는 같은 origin mutation Web Lock을 physical 삭제 완료까지 보유한다. lease를 먼저 release하지 않는다. **권장 새/변경 signature** ```ts declare const opfsPhysicalGenerationBrand: unique symbol; export type OpfsPhysicalGenerationId = string & { readonly [opfsPhysicalGenerationBrand]: "OpfsPhysicalGenerationId"; }; export type OpfsPreparedObjectV2 = Readonly<{ physicalSchemaVersion: 2; physicalGenerationId: OpfsPhysicalGenerationId; descriptor: DurableObjectDescriptor; // logical generation은 그대로 유지 chunks: readonly OpfsChunkReference[]; }>; export type OpfsCleanupEffect = | Readonly<{ kind: "CLEANED" | "ALREADY_CLEAN" }> | Readonly<{ kind: "EFFECT_UNKNOWN" }>; export interface OpfsWorkerGateway { abortPreparedPut(request: Readonly<{ scope: OpfsStorageScope; transactionId: string; physicalGenerationId: OpfsPhysicalGenerationId; signal?: AbortSignal; // coordinator-owned compensation signal만 전달 }>): Promise>; } ``` P0에서는 v1 read를 유지하면서 새 write만 v2/token path로 쓴다. `EFFECT_UNKNOWN`은 성공 Result로 취급하지 말고 journal 유지 + `OBJECT_RECONCILE`를 반환한다. **기존 테스트와 false-positive 방지** - `tests/unit/opfs-byte-store.test.ts:504-540`의 “keeps a committed journal row for reconciliation when cleanup fails”는 logical commit 뒤 finalize 실패만 검증한다. PREPARING/FILES_READY 보상 실패를 다루지 않는다. - `tests/unit/opfs-worker-runtime.test.ts:232-408`은 BEGIN cancel/APPEND-vs-ABORT serialization/authority isolation을 검증하지만, journal rollback 뒤 다른 worker/context가 재사용한 generation에 대한 늦은 cleanup을 만들지 않는다. - `indexeddb-opfs-journal.ts:997-1008`의 unique `logicalKey` index는 **journal row가 남아 있는 동안** T2를 막는다. 바로 그 row를 조기에 삭제하는 것이 문제이므로 이 index가 반증이 아니다. ### STO-02 — 검증 URL과 실제 download navigation URL의 base가 다름 **근거** - `safeBrowserManagedTarget`은 `new URL(href, new URL(baseOrigin))`으로 protocol/origin/query/hash를 검증한다: `src/adapters/browser-files/download-delivery-adapter.ts:1248-1269`. - 성공 후 canonical `URL.href`가 아니라 원문 문자열을 host로 넘긴다: `435-459`. - 실제 anchor는 `anchor.href = href`라서 document의 current `baseURI`를 기준으로 해석한다: `47-68`. 예: configured `baseOrigin=https://app.example`, capability `href="downloads/report"`, document에 ``가 있으면 검증은 app origin을 통과하지만 실제 anchor는 evil origin으로 향한다. capability receipt의 server binding이 있더라도 adapter의 same-origin 정책 주장이 깨진다. **패턴과 수정** - **Parse once / canonicalize then execute** 패턴을 적용한다. validator가 boolean이 아니라 canonical absolute URL을 반환하고 정확히 그 값을 handoff한다. - cross-origin을 허용하는 별도 policy에서도 username/password/hash/query 규칙을 적용한 canonical string만 실행한다. ```ts type ResolvedBrowserManagedTarget = Readonly<{ absoluteHref: string }>; function resolveBrowserManagedTarget( href: string, baseOrigin: string, policy: Readonly<{ allowCrossOrigin: boolean; allowQuery: boolean }>, ): BrowserDataResult; ``` `context.options.host.handoff(target.value.absoluteHref, fileName)`로 변경한다. 더 엄격한 선택은 capability resolver가 absolute `https:` URL만 발행하게 하고 상대 URL을 거절하는 것이다. **기존 테스트 대조** - `tests/unit/browser-file-download.test.ts:222-255`는 raw 상대 path가 host에 그대로 전달된다고 고정한다. 이 기대값을 canonical `https://app.example/downloads/artifact-1`로 바꿔야 한다. - `257-285`는 이미 absolute evil/query URL 거절만 검증해 `` 불일치를 잡지 못한다. ### STO-03 — Vary 허용/보존 policy가 모순될 수 있음 **근거** - policy validation은 vary name이 request allowlist에 포함되는지만 본다: `src/adapters/cache-storage/public-cache-policy.ts:110-141`, 특히 `132-134`. response allowlist에 `vary`가 있는지는 확인하지 않는다. - network response의 Vary는 exact request headers와 검증한다: `src/adapters/cache-storage/public-response-cache-adapter.ts:1140,1228-1262`. - 이후 `unknownResponseHeaderAction="STRIP"`이면 response allowlist에 없는 `vary`를 제거하고(`1264-1280`), 제거된 headers로 Cache에 put한다(`472-479`). 동일 URL variant가 충돌한다. - activation은 모든 entry를 다시 digest/type/Vary 검증하므로 `592-617`에서 fail-closed한다. 따라서 현재 증거로 private-data disclosure를 주장하면 과장이다. 실제 영향은 impossible candidate에 대한 stage 성공, variant loss, activation/rollback availability 저하다. **수정** ```ts if ( policy.allowedVaryHeaderNames.length > 0 && !policy.allowedResponseHeaderNames.includes("vary") ) throw new TypeError("Vary must be preserved when variants are enabled."); ``` 방어를 겹치려면 `sanitizedResponseHeaders`가 검증된 `Vary`를 generic strip과 무관하게 반드시 보존하도록 한다. **Policy cross-field invariant + fail-fast composition** 패턴이다. `tests/unit/public-response-cache.test.ts:1030-1155`는 default response allowlist가 이미 `vary`를 포함(`public-cache-policy.ts:54-63`)하므로 이 custom-policy 조합을 놓친다. ### STO-04 — existing cache marker만 확인하는 stage idempotence `src/adapters/cache-storage/public-response-cache-adapter.ts:424-442`는 cache name이 있고 marker의 release ID/digest/count가 맞으면 모든 cached response의 존재/내용을 보지 않고 stage 성공을 반환한다. marker-last는 첫 stage crash에는 강하지만 marker 이후 browser pressure eviction, manual deletion, partial corruption에는 충분하지 않다. activation이 `592-617`에서 재검증하므로 unsafe publish는 막지만, 같은 manifest로 restage해도 손상 candidate를 복구하지 못한다. **수정:** `verifyReleaseCandidate(cache, normalized, policy, crypto, signal)`를 factor하고 stage fast path와 activation이 공유한다. 기존 candidate가 missing/mismatch면 owned candidate만 삭제하고 network restage한다. verification 중 abort/unknown error면 active pointer는 건드리지 않고 candidate를 유지 또는 정책대로 삭제하되 성공을 반환하지 않는다. 이는 **idempotent repair, marker as claim not evidence** 패턴이다. ### STO-05 — cache mutation availability가 fetcher에 과결합 `mutationAvailability`는 storage+fetcher+lock 모두를 요구한다: `public-response-cache-adapter.ts:1643-1651`. stage 호출 `392-396`에는 맞지만, fetch하지 않는 activate `538-542`와 cleanup `695-699`에도 같은 guard를 쓴다. 이미 검증된 release를 offline에서 활성화/rollback하거나 quota recovery cleanup하는 기능을 차단한다. **수정:** operation별 capability guard로 분리한다. ```ts function stageAvailability(d: Dependencies): BrowserFailureResult | null; // cacheStorage + mutationLock + fetcher function localMutationAvailability( d: Dependencies, operation: "CACHE_ACTIVATE" | "CACHE_DELETE", ): BrowserFailureResult | null; // cacheStorage + mutationLock ``` **Dependency segregation**을 적용하고 recovery도 `ONLINE_ONLY`가 아니라 실제 operation에 맞는 `RETRY/REHYDRATE`로 유지한다. ### STO-06 — IndexedDB migration commit 중 duration budget 재확인 없음 - port는 async storage operation 사이 cooperative duration budget을 명시한다: `src/application/ports/browser-file-storage/indexeddb-port.ts:81-89`. - docs도 각 native operation 사이 monotonic deadline 확인을 요구한다: `docs/architecture/browser-file-and-origin-storage.md:611-615`. - transform phase는 clock을 확인한다: `src/adapters/storage/indexeddb/indexeddb-maintenance.ts:940-966`. - 그러나 `commitPrepared`의 read/write/budget/sidecar/checkpoint chain은 `969-1233` 동안 clock을 호출하지 않는다. 최대 500 rows의 IDB callbacks가 invocation deadline 이후에도 계속될 수 있다. **수정:** transaction을 시작하기 전 composition-owned `minimumCommitReserveMs`를 확인하고, prepared row 수를 budget에 맞춰 더 작게 제한한다. transaction을 연 뒤에는 각 record 시작 시 monotonic deadline을 확인하여 아직 어떤 write도 시작하지 않은 다음 record에서 transaction을 정상 종료하고 last-safe checkpoint까지만 commit한다. 이미 시작한 record의 row/sidecar/budget은 원자 완료하거나 tx 전체 abort해야 하며 부분 truth를 반환하면 안 된다. clock failure는 transaction abort + `UNAVAILABLE`다. `tests/unit/indexeddb-maintenance.test.ts:562-597`은 transform 시작 전 budget exhaustion만 검증하므로 commit callback 중 clock advance 케이스를 추가한다. ### STO-07 — OPFS worker protocol version/strict response correlation 부재 - request/response envelope에 `protocolVersion`과 echoed `kind`가 없다: `src/adapters/storage/opfs/opfs-worker-protocol.ts:15-137`. - worker는 requestId+known kind만 1차 검사한다: `opfs-worker-runtime.ts:1643-1669`. - client는 `{requestId:string, ok:boolean}`만 검사한다: `opfs-worker-client.ts:666-675`. 실패 object/failure code/kind를 strict validate하지 않고 `response.failure.code`를 사용(`171-184`)한다. - VD-15는 real preflight에서 protocol/schema mismatch를 `INCOMPATIBLE`로 닫으라고 한다: `docs/architecture/decisions/VD-15-origin-storage-lifecycle-and-migration.md:476-484`. **수정:** `OPFS_WORKER_PROTOCOL_VERSION = 2 as const`; 모든 request/response에 version과 kind를 넣고 pending request가 expected kind를 보관한다. closed failure-code set과 per-kind value parser를 적용한다. 먼저 `HELLO/CAPABILITIES` handshake에서 supported physical schema와 protocol version을 교환하고 mismatch면 write/read를 금지한다. generic cancel은 초기 correctness 필수가 아니다. PUT은 effect certainty가 필요한 명시적 `ABORT_PUT`; read/verify RPC는 client-side abandon으로 충분하며, 자원 최적화가 필요할 때만 `CANCEL_REQUEST { targetRequestId }`를 추가한다. ### STO-08 — picker function receiver binding은 browser test로 먼저 확정 - open: `src/adapters/browser-files/browser-file-picker.ts:431-436` - save/open-authorized callbacks: `src/adapters/browser-files/download-delivery-adapter.ts:201-235` platform `Window.showOpenFilePicker/showSaveFilePicker`를 options object에 bind할 이유가 없고 Web IDL receiver brand check 가능성이 있다. 다만 현재 코드가 host facade 콜백을 의도했을 수도 있어 확정 전 browser matrix가 필요하다. 우선 실제 `window.showOpenFilePicker`를 전달한 capability test를 추가한다. 실패가 재현되면 API를 `SystemPickerHost { open; save? }`로 만들고 composition에서 올바른 owner에 bind한 host만 주입한다. arbitrary callback(`openAuthorizedSource`, integrity factory)은 bind하지 않고 함수 snapshot 그대로 호출한다. ## 3. 명시적 architecture 결정 ### Transaction / crash recovery - IndexedDB 한 domain mutation은 한 native transaction으로 row, retention sidecar, budget, idempotency receipt/checkpoint를 commit한다. request success가 아니라 transaction `complete`가 성공 truth다. - IDB와 OPFS/Cache 사이에는 atomic transaction이 없다. OPFS는 journal-authoritative durable saga다. phase는 monotonic이고 physical side effect가 불명확하면 incomplete journal을 유지한다. - compensation은 원 caller abort와 분리된 bounded signal로 실행한다. cleanup success가 확인될 때만 journal/budget rollback; unknown이면 reconcile owner에게 넘긴다. - committed object를 in-place repair하지 않는다. 새 physical token/generation에 copy/verify 후 logical CAS publish한다. ### Migration / rollback - 독립 version 축(IDB DDL, record codec, OPFS journal, OPFS physical, Cache control/release)을 합치지 않는다. - 공통 순서는 expand → old-writer drain lease → bounded migrate/copy → atomic publish → N-1 observe/rollback window → 별도 contract release다. - schema downgrade, whole DB/root/cache delete, read-time unbounded rewrite는 금지한다. - IDB `isOldWriterDrainConfirmed()`는 현재 provider가 전체 migration/contract window를 보장한다는 문서 전제라 현 결함은 아니다. 다음 interface로 temporal guarantee를 실행 가능하게 강화한다: ```ts export interface OldWriterDrainLease { readonly leaseId: string; readonly validUntilEpochMs: number; assertValid(signal?: AbortSignal): Promise>; release(): Promise; } export interface IndexedDbDataMigrationPolicy { acquireOldWriterDrainLease(input: Readonly<{ migrationId: string; targetCodecVersion: number; scope: IndexedDbDatasetScope; signal?: AbortSignal; }>): Promise>; // migrate/measure 기존 계약 유지 } ``` lease는 batch commit 직전 재검증하고, product rollout owner는 migration 완료 후 rollback/contract window까지 global fence를 유지한다. ### Quota / pressure / eviction - StorageManager estimate는 rough signal일 뿐 free-space reservation이 아니다. 실제 `QuotaExceededError`가 authority다. - per-dataset hard budget은 그대로 유지하고, origin coordinator는 Web Lock leader 한 개가 hysteresis(`70/85%`, 하향 `65/80%` 2회)를 적용한다. - GC 순서: incomplete candidate/stale staging → expired reconstructable → grace 지난 unreferenced chunk → inactive public release → confirmed synced copy → 중지. user-authored/unsynced는 자동 삭제 금지. - 기본 invocation 100 items/5s, 절대 500/30s. cursor는 owner/policy/release epoch에 binding한다. - quota retry는 실제 quota rollback, 동일 idempotency/revision/digest, external publish 없음, GC가 실제 제거/pressure 하향, 새 admission token 조건을 모두 만족할 때 정확히 1회만 허용한다. ### Lease / destructive authority - OPFS mutation Web Lock은 physical delete/cleanup 완료까지 보유한다. transaction-unique physical token이 stale cleanup fencing이다. - object URL은 registry lease로만 만들고 persistence/log/analytics/global cache에 넣지 않는다. release/dispose는 idempotent다. - IndexedDB lifecycle authority는 현재 문서대로 composition callback의 short-lived proof를 형식 검증 후 즉시 폐기한다. OPFS와 동일한 replay 방지가 제품 threat model에 필요하면 provider+atomic consumer의 one-shot lease로 별도 강화하되 application caller에게 token을 노출하지 않는다. ### Object URL / preview - 현재 encoded size/signature/media/active-content denylist는 유지한다. - 제품 untrusted image preview를 선택하기 전 object URL 발급 **앞**에 bounded header parser + native decode probe를 둔다. static JPEG/PNG/WebP/AVIF 등 명시 allowlist만; SVG/PDF/HTML/XML과 animated image는 별도 격리/re-encode capability가 없으면 attachment-only다. ```ts export interface PreviewSafetyProbe { inspect(input: Readonly<{ file: File; // adapter-local only mediaType: string; maxEncodedBytes: number; maxPixels: number; maxDecodedBytes: number; maxFrames: number; deadlineMs: number; signal: AbortSignal; }>): Promise>>; } ``` parser 산술은 overflow-safe여야 하고 native `createImageBitmap` 결과는 항상 `close()`. timeout/abort/failure면 `createObjectURL`을 호출하지 않는다. ### Stream / cancellation - application boundary는 `ByteSource.stream(signal): AsyncIterable>`를 유지한다. 첫 failure에서 producer/reader/writer를 모두 닫고 raw DOMException/EOF 성공으로 바꾸지 않는다. - save stream은 backpressure를 따르고 `writer.close()` 완료 truth가 늦은 abort보다 우선한다. partial destination append/resume로 주장하지 않는다. - Blob/object URL buffer는 hard cap 아래 fallback에서만 허용한다. public cache는 exact length/digest 검증 때문에 bounded buffer를 유지하되 cap을 넘으면 reader cancel. - pre-start abort는 side effect 0. IDB 중간 abort는 tx abort. irreversible prompt/persist/close가 완료된 뒤에는 platform truth가 이긴다. - worker mutation timeout은 effect unknown이지 rollback 확인이 아니다. read RPC는 응답을 버릴 수 있지만 mutation은 journal/explicit abort protocol로 종결한다. ### Worker protocol - versioned handshake, request kind echo, requestId+kind correlation, strict discriminated parser, closed error set을 채택한다. - wrong version/schema는 `INCOMPATIBLE` health로 write/read 금지. 이를 failure surface에 노출할 필요가 있으면 `BrowserDataFailureCode`에 `INCOMPATIBLE`을 추가하고 모든 exhaustive mapper/fixture를 함께 갱신한다. 단순 `UNAVAILABLE` retry loop로 숨기지 않는다. - generic `CANCEL_REQUEST`는 read CPU/resource 최적화로 후순위. PUT correctness는 transaction-scoped `ABORT_PUT`과 durable journal이 담당한다. ### Cache security / eviction - anonymous same-origin public GET, credentials omit, exact query/request headers/Vary/type/length/digest만 cache한다. auth/private/no-store/no-cache/opaque/redirect/206/range는 계속 금지한다. - verified marker는 모든 entries 이후 마지막에 쓰되 marker만 증거로 믿지 않는다. stage reuse와 activation/lookup에서 response를 재검증한다. - current+verified previous release를 유지하고 rollback도 동일 activation validation을 다시 통과한다. - partial eviction/miss는 `STORAGE_EVICTED` 또는 integrity failure로 fail-closed하고 network rehydrate한다. owned prefix 밖 cache나 user data는 절대 삭제하지 않는다. ## 4. 정확한 파일 변경 계획 ### Phase 0 — 즉시 correctness/security fix **수정** - `src/application/ports/browser-file-storage/opfs-ports.ts`: v1|v2 prepared object read union, `OpfsPhysicalGenerationId`, journal row physical identity. - `src/adapters/storage/opfs/opfs-worker-protocol.ts`: explicit abort/cleanup effect, protocol v2 envelope/kind correlation. - `src/adapters/storage/opfs/opfs-worker-client.ts`: fire-and-forget duplicate abort 제거, strict response parser, coordinator-owned confirmed abort. - `src/adapters/storage/opfs/opfs-worker-runtime.ts`: tokenized physical path/receipt/manifest, cleanup lock 보유, exact token delete. - `src/adapters/storage/opfs/opfs-byte-store-adapter.ts`: cleanup result 확인 전 journal rollback 금지; independent compensation deadline; unknown effect reconcile. - `src/adapters/storage/opfs/indexeddb-opfs-journal.ts`: v2 prepared/journal validation, incomplete row 유지 및 migration metadata. - `tests/unit/opfs-byte-store.test.ts`, `tests/unit/opfs-worker-runtime.test.ts`, `tests/unit/indexeddb-opfs-journal.test.ts`: 아래 race/crash tests. - `src/adapters/browser-files/download-delivery-adapter.ts`: boolean validator를 canonical resolver로 변경; absolute URL 실행. - `tests/unit/browser-file-download.test.ts`: canonical URL 및 hostile base regression. - `src/adapters/cache-storage/public-cache-policy.ts`: Vary preservation cross-field invariant. - `src/adapters/cache-storage/public-response-cache-adapter.ts`: stage candidate full verify/self-repair, availability 분리. - `tests/unit/public-response-cache.test.ts`: custom Vary, damaged candidate, no-fetcher activate/cleanup. ### Phase 1 — bounded lifecycle / protocol / preview promotion **생성** - `src/application/ports/browser-file-storage/origin-storage-lifecycle-port.ts` - `src/adapters/storage/origin-storage-lifecycle-coordinator.ts` - `tests/unit/origin-storage-lifecycle-coordinator.test.ts` - `src/adapters/browser-files/browser-image-preview-probe.ts` - `tests/unit/browser-image-preview-probe.test.ts` - `src/adapters/storage/opfs/opfs-physical-migration.ts` - `tests/unit/opfs-physical-migration.test.ts` - `tests/fixtures/origin-storage/opfs-v1-populated.ts` - `tests/fixtures/origin-storage/cache-v1-populated.ts` **수정** - `src/application/ports/browser-file-storage/index.ts`: 새 lifecycle port export. - `src/application/ports/browser-file-storage/file.ts`: preview safety policy/result를 native-free 형태로 추가하거나 probe를 adapter-internal dependency로 유지. - `src/application/ports/browser-file-storage/cache-storage-ports.ts`: bounded maintenance page/cursor input. - `src/application/ports/browser-file-storage/indexeddb-port.ts`, `src/adapters/storage/indexeddb/indexeddb-types.ts`: drain lease contract. - `src/adapters/storage/indexeddb/indexeddb-maintenance.ts`: commit reserve/deadline checks 및 lease revalidation. - `src/adapters/browser-files/object-url-lease.ts`, `src/adapters/browser-files/create-browser-file-runtime.ts`: probe success 전 URL 생성 금지. - `src/adapters/cache-storage/public-response-cache-adapter.ts`: cursor/deadline bounded inspect/cleanup. - `src/adapters/storage/opfs/browser-opfs-runtime.ts`: real worker/lock/journal/write-read-delete-cleanup preflight 조립 hook. - `tests/browser-capabilities/{browser-files,opfs-runtime,public-cache-storage,indexeddb-runtime}.spec.ts`와 `opfs-test.worker.ts`: real engine evidence. - `docs/architecture/browser-file-and-origin-storage.md`, `docs/architecture/decisions/VD-15-origin-storage-lifecycle-and-migration.md`, `docs/operations/browser-file-storage-recovery.md`, `docs/operations/client-cache-and-storage-recovery.md`: 상태를 구현 후에만 `AVAILABLE_NOT_COMPOSED`로 승격. **삭제/이동**: 없음. v1 reader/fixtures와 old cache prefix는 rollback window 종료 전 삭제하지 않는다. barrel 재배치도 불필요하다. ### Cache bounded port signature ```ts declare const publicCacheCursorBrand: unique symbol; export type PublicCacheMaintenanceCursor = string & { readonly [publicCacheCursorBrand]: "PublicCacheMaintenanceCursor"; }; export type PublicCacheMaintenanceInput = Readonly<{ maxCaches?: number; // default 100, absolute 500 maxDurationMs?: number; // default 5_000, absolute 30_000 cursor?: PublicCacheMaintenanceCursor; signal?: AbortSignal; }>; export type PublicCacheMaintenancePage = Readonly<{ inspectedCaches: number; deletedCaches: number; retainedCaches: number; unreadableCaches: number; nextCursor: PublicCacheMaintenanceCursor | null; moreAvailable: boolean; deadlineReached: boolean; }>; cleanupOwned(input?: PublicCacheMaintenanceInput): Promise>; inspectOwned(input?: PublicCacheMaintenanceInput): Promise>; ``` cursor는 caller-readable cache name이 아니며 owned prefix, active pointer epoch, policy fingerprint에 서명/opaque binding한다. stale cursor는 `STALE_RESULT`. ## 5. TDD 계획: 이름, 입력, 기대 결과 | 테스트 이름 | 핵심 입력/fixture | 기대 결과 | | --- | --- | --- | | `keeps PREPARING journal when compensating cleanup is aborted or unavailable` | `preparePut` failure; caller signal aborted; worker cleanup `ABORTED/UNAVAILABLE` | `journal.rollback` 미호출, reservation/journal 유지, `OBJECT_RECONCILE` recovery; 후속 same object begin conflict | | `delayed stale cleanup cannot delete a reused logical generation` | T1 generation 1 abort RPC 지연; T2 generation 1 v2 token으로 commit; T1 cleanup resume | T1 token path만 제거; T2 verify/open bytes 성공; T2 manifest/chunks 유지 | | `holds the OPFS mutation lease until exact physical cleanup completes` | cleanup delete promise를 gate하고 concurrent begin 시도 | delete 완료 전 T2 lease 미획득; release 후 진행 | | `does not roll back journal after an unknown worker mutation effect` | cleanup RPC timeout 후 worker operation pending | incomplete journal 유지; reconcile가 exact transaction을 종결 | | `rejects mismatched OPFS worker protocol and response kind` | v1 response 또는 requestId는 같지만 wrong kind/malformed failure | `INCOMPATIBLE`/closed failure; pending request success로 resolve하지 않음; write side effect 0 | | `hands off the canonical URL validated against baseOrigin` | `href="downloads/a"`, baseOrigin app, document base evil | host receives `https://app.example/downloads/a`; evil URL never assigned | | `rejects a policy that enables variants but strips Vary` | allowed vary `accept-language`, allowed response headers without `vary`, STRIP | composition `TypeError`, Cache/fetch side effect 0 | | `preserves Vary for every stored custom variant` | en/ko same URL with exact request header | stage+activate+both exact match succeed; stored response has Vary | | `restages an evicted entry even when the release marker remains` | successful stage 후 one asset delete, same manifest stage again | missing asset re-fetch; all entries reverify; success only after repair | | `activates and cleans a prestaged cache without a fetcher` | seeded valid cache/pointer, cacheStorage+lock, no fetcher | activate/cleanup success; no network call | | `stops codec migration commit at the cooperative deadline` | fake clock advances during IDB record callbacks, prepared N rows | only last atomically safe prefix+checkpoint commit; `MORE`, `budgetExhausted`; no orphan sidecar/budget delta | | `requires an old-writer drain lease to remain valid before batch commit` | lease valid at acquire, expires before commit | tx write 0/abort; `BLOCKED`; checkpoint unchanged | | `rejects oversized raster dimensions before object URL creation` | small encoded PNG with huge width/height or overflow dimensions | `LIMIT_EXCEEDED/POLICY_REJECTED`; `createObjectURL` 0 calls | | `closes a decoded bitmap on preview abort and failure` | probe aborts after native decode begins | bitmap `close` once, URL 0, closed `ABORTED` | | `rejects animated and truncated preview containers` | animated WebP/GIF, truncated PNG/JPEG | fail before URL, no leaked decoder resource | | `pages cache cleanup by count deadline and opaque cursor` | 700 owned caches + foreign caches; max 100/5s | <=100 inspected, foreign untouched, `moreAvailable`, bound cursor; repeated pages converge | | `rejects cache maintenance cursor after active pointer epoch changes` | page1 cursor 후 activation | `STALE_RESULT`, delete 0 | | `retries quota failure exactly once only after productive GC` | reconstructable write quota fail, GC deleted >0, same idempotency/digest | attempt 2 최대 한 번; second fail no third; user-authored untouched | | `uses the real Window receiver for enhanced system pickers` | actual browser `window.showOpenFilePicker/showSaveFilePicker` facade (feature-gated) | supported engine에서 illegal invocation 없음; dismissal closed outcome | ### 실행 명령 ```bash # 가장 빠른 red/green loop corepack pnpm exec vitest run \ tests/unit/opfs-byte-store.test.ts \ tests/unit/opfs-worker-runtime.test.ts \ tests/unit/indexeddb-opfs-journal.test.ts \ tests/unit/browser-file-download.test.ts \ tests/unit/public-response-cache.test.ts \ tests/unit/indexeddb-maintenance.test.ts \ tests/unit/browser-image-preview-probe.test.ts \ tests/unit/origin-storage-lifecycle-coordinator.test.ts # 정적 경계 corepack pnpm check:types corepack pnpm check:architecture corepack pnpm check:browser-file-storage-boundaries corepack pnpm lint # 실제 browser/storage semantics corepack pnpm exec playwright test --config playwright.capabilities.config.ts \ tests/browser-capabilities/browser-files.spec.ts \ tests/browser-capabilities/indexeddb-runtime.spec.ts \ tests/browser-capabilities/opfs-runtime.spec.ts \ tests/browser-capabilities/public-cache-storage.spec.ts \ tests/browser-capabilities/storage-manager.spec.ts # 전체 회귀 corepack pnpm test:unit corepack pnpm test:browser-file-storage-removal corepack pnpm verify:documentation ``` ## 6. 데이터 호환성, migration, deployment, rollback 순서 1. **즉시 containment:** 제품에 OPFS v1 write가 조립돼 있다면 kill switch로 신규 write를 read-only/export-required로 전환한다. read/export와 journal reconcile는 유지한다. file/IDB/OPFS/cache가 template bootstrap 기본 조립이 아니라는 사실은 영향 범위를 줄이지만 product-specific composition을 확인해야 한다. 2. **N expand release:** journal DDL을 additive upgrade하고 v1+v2 `OpfsPreparedObject` reader를 배포한다. worker protocol v2 handshake를 먼저 넣되 v1 data read는 지원한다. v2 physical path는 unique token을 포함하고 새 write만 v2로 쓴다. 3. **old writer drain:** 모든 N-1 page/worker가 write를 중단했다는 release/lease evidence를 확인한다. BroadcastChannel hint만으로 판단하지 않는다. v2 write traffic은 SHADOW/canary부터 연다. 4. **resume/reconcile:** PREPARING/FILES_READY v1 journal을 bounded하게 처리한다. cleanup effect가 불명확하면 row를 삭제하지 않는다. logical committed v1은 authority이며 in-place 수정하지 않는다. 5. **copy-on-write migration:** v1 committed object → v2 staging/token path → bounded chunk read/copy → manifest/tree digest verify → journal generation/fencing CAS publish. publish 전 crash는 v1, publish 후 crash는 v2가 authority다. 6. **Cache migration:** old active verified release를 byte rewrite하지 말고 새 prefix/control schema에 network restage → full verify → explicit activation. current+previous와 old prefix를 rollback/grace window 동안 유지한다. 7. **Web Storage:** current keys는 registry `DISCARD` semantics를 유지한다. adjacent migration이 제품에 필요할 때만 exact owned old physical key를 read-once/validate/write-current/delete-old한다. 전체 localStorage sweep 금지. 8. **Canary observation:** multi-tab/worker timeout, crash between every phase, partial eviction, quota fault, N-1 read-only/online-only fixture를 통과한다. user-authored bytes export/sync path도 확인한다. 9. **Rollback:** traffic admission과 새 writer부터 끈다. schema/database version을 내리지 않는다. compatible N reader 또는 N-1 online-only/read-only bundle로 전환하고, OPFS는 publish authority에 따라 v1/v2 source를 선택한다. Cache는 검증된 previous release로 같은 activate protocol을 실행한다. 10. **Contract release:** 모든 active/rollback clients drain, grace/authority evidence, historical fixtures 후에만 v1 physical generation/old cache prefix를 bounded cursor cleanup한다. DB/root/cache blanket delete는 하지 않는다. ## 7. 유지해야 할 좋은 설계 - closed `BrowserDataResult`, safe recovery vocabulary, observer exception 격리 및 PII/path/name 비노출. - File policy가 composition-owned immutable identity이고 selection/inspection/preview/download receipt가 exact file/profile에 binding되는 구조. - native input baseline과 optional enhanced picker 분리, user activation 전에 await하지 않는 규칙, dismissal과 failure 구분. - 중앙 object URL lease cap, idempotent revoke/dispose, typed Blob, active-content denylist. - `ByteSource` chunk별 Result/cancellation, download backpressure, close 완료 truth, bounded object URL fallback. - Web Storage의 typed registry, physical key versioning, strict exact JSON codec, TTL, quota memory overlay와 tombstone. - IndexedDB의 opaque physical identity/governance binding, additive-only planner, transaction-complete semantics, CAS/idempotency/retention/budget atomicity, versionchange late-close. - OPFS의 IDB logical authority, phase journal, immutable digest chunks/refcount, hard budget reservation, fail-closed staging GC, no user-readable physical paths. - Cache의 anonymous public-only same-origin policy, exact query/header/Vary/type/length/digest, marker-last candidate, explicit activation, current+previous retention, owned-prefix-only cleanup, read/activate 재검증. - optional adapters를 bootstrap에서 자동 조립하지 않고 `AVAILABLE_NOT_COMPOSED`로 남긴 현재 composition posture. ## 8. 기존 테스트·문서 대조와 false-positive 경계 ### 실행한 기존 검증 다음 명령을 이 리뷰 중 실행했고 **7 files / 105 tests 전부 통과**했다. ```bash corepack pnpm exec vitest run \ tests/unit/opfs-byte-store.test.ts \ tests/unit/opfs-worker-runtime.test.ts \ tests/unit/public-response-cache.test.ts \ tests/unit/browser-file-download.test.ts \ tests/unit/indexeddb-maintenance.test.ts \ tests/unit/indexeddb-runtime.test.ts \ tests/unit/storage-registry.test.ts --reporter=default ``` 이는 finding이 현재 green suite가 보호하지 않는 interleaving/custom-policy/browser-base case임을 뜻하며, 기존 behavior가 전반적으로 깨졌다는 뜻은 아니다. ### 반증/과장 방지 표 | 의심 항목 | 기존 증거 | 최종 판단 | | --- | --- | --- | | OPFS commit 뒤 finalize cleanup 실패 | `opfs-byte-store.test.ts:504-540`가 COMMITTED row 보존 검증 | 보호됨. STO-01은 **commit 전 cleanup 실패/늦은 RPC + generation reuse**로 좁힘. | | OPFS concurrent operations | `opfs-worker-runtime.test.ts:232-408`가 lock wait cancel, APPEND/ABORT, authority isolation 검증 | 같은 worker의 active put 일부는 보호됨. journal 조기 rollback 후 cross-context late cleanup은 미검증. | | Cache Vary가 곧 private leak | activation/lookup이 response를 재검증(`public-response-cache-adapter.ts:592-617,341-365`) | 직접 disclosure 주장은 철회. stage success/variant loss/activation availability 결함으로 Medium. | | Cache 기본 policy Vary | default response allowlist에 `vary` 포함(`public-cache-policy.ts:54-63`), unit `1030-1155` green | 기본은 보호됨. custom policy cross-field invariant만 결함. | | damaged cache가 active로 publish | activation full reverify | publish는 fail-closed. STO-04는 idempotent stage/self-repair contract. | | Web Storage schema mismatch | `storage-registry.test.ts:231-244`가 current physical key의 old envelope discard 검증 | 보호됨. old **physical key** sweep/adjacent migration은 문서상 미구현이며 현재 작은 preference의 readiness gap. | | IndexedDB transaction success/abort | `indexeddb-runtime.test.ts:319-380`가 commit failure rollback과 abort 검증 | 보호됨. STO-06은 migration commit-loop duration budget에 한정. | | IndexedDB old-writer drain이 전혀 없음 | maintenance test `269-301`, docs `604-609`가 provider confirmation을 전제 | 현 계약상 provider 책임이므로 결함으로 세지 않음. temporal lease는 enforceability 강화. | | preview decode safety가 몰래 누락 | `browser-file-and-origin-storage.md:360-365`, VD-15 `19-31,574+`, runbook `96-108`가 미구현을 명시 | regression 아님. 제품 preview promotion blocker(GAP-01). | | Cache unbounded cleanup이 발견되지 않은 bug | VD-15 `486-515`, runbook `382-429`가 정확히 명시 | known `DESIGNED_NOT_IMPLEMENTED` readiness gap(GAP-02). | | origin pressure/migration coordinator 부재 | VD-15 `19-31,90-103`, `browser-file-storage-recovery.md:10-14` | known gap. 기존 per-store maintenance를 coordinator로 오인하지 않는다. | | optional adapters가 bootstrap에 없음 | `runtime-adapters.ts:260-266`; docs status `AVAILABLE_NOT_COMPOSED` | 의도된 skeleton posture, 결함 아님. | | picker receiver | unit tests가 모두 arrow/fake callback을 사용 | 확정 증거 부족. STO-08은 browser test 선행의 낮은 심각도 hypothesis로 격리. | ## 9. 리뷰 범위 밖으로 확장하지 않은 항목 - Service Worker lifecycle, private/range cache, persistent directory/file handles, Range resumable download는 문서상 별도 `NOT_SELECTED`/`DESIGNED_NOT_IMPLEMENTED` capability다. public cache/file adapter에 섞어 고치지 않는다. - application/product dataset, schema, rollout authority가 없으므로 optional IndexedDB/OPFS/Cache를 현재 default bootstrap에 새로 조립하지 않는다. - 전체 origin eviction은 모든 IndexedDB/OPFS/Cache metadata가 함께 사라질 수 있어 client-only로 완전 판별할 수 없다. server rehydrate/export UX와 generation/session authority가 필요하다. --- 최종 권고: STO-01은 production composition이 하나라도 있으면 release blocker로 취급한다. STO-02는 작은 canonicalization patch로 즉시 닫을 수 있다. Cache 세 항목은 동일 변경 묶음으로 TDD하고, VD-15 gap들은 상태 문서를 먼저 바꾸지 말고 executable unit+browser evidence와 rollback fixture가 생긴 후에만 승격한다.