Files
tech-log-frontend/docs/reviews/adapters/03-storage-and-browser-files.md
T
DongHyeonkaandClaude Opus 5 4bff9ca151 chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.

The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.

`template.lock.json` now pins the synced revision and tree.

Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:04:58 +09:00

52 KiB

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을 검증하지만 원문 hrefdocument.baseURI로 실행한다. <base>가 있으면 검증한 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. rollbackBestEffortworker.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

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<BrowserDataResult<OpfsCleanupEffect>>;
}

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가 다름

근거

  • safeBrowserManagedTargetnew 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에 <base href="https://evil.example/">가 있으면 검증은 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만 실행한다.
type ResolvedBrowserManagedTarget = Readonly<{ absoluteHref: string }>;

function resolveBrowserManagedTarget(
  href: string,
  baseOrigin: string,
  policy: Readonly<{ allowCrossOrigin: boolean; allowQuery: boolean }>,
): BrowserDataResult<ResolvedBrowserManagedTarget>;

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 거절만 검증해 <base> 불일치를 잡지 못한다.

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 저하다.

수정

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로 분리한다.

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를 실행 가능하게 강화한다:
export interface OldWriterDrainLease {
  readonly leaseId: string;
  readonly validUntilEpochMs: number;
  assertValid(signal?: AbortSignal): Promise<BrowserDataResult<void>>;
  release(): Promise<void>;
}
export interface IndexedDbDataMigrationPolicy<WireValue> {
  acquireOldWriterDrainLease(input: Readonly<{
    migrationId: string;
    targetCodecVersion: number;
    scope: IndexedDbDatasetScope;
    signal?: AbortSignal;
  }>): Promise<BrowserDataResult<OldWriterDrainLease>>;
  // 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다.
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<BrowserDataResult<Readonly<{
    width: number;
    height: number;
    frameCount: number;
    decodedBytes: number;
  }>>>;
}

parser 산술은 overflow-safe여야 하고 native createImageBitmap 결과는 항상 close(). timeout/abort/failure면 createObjectURL을 호출하지 않는다.

Stream / cancellation

  • application boundary는 ByteSource.stream(signal): AsyncIterable<BrowserDataResult<Uint8Array>>를 유지한다. 첫 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에 노출할 필요가 있으면 BrowserDataFailureCodeINCOMPATIBLE을 추가하고 모든 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.tsopfs-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

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<BrowserDataResult<PublicCacheMaintenancePage>>;
inspectOwned(input?: PublicCacheMaintenanceInput):
  Promise<BrowserDataResult<PublicCacheMaintenancePage>>;

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

실행 명령

# 가장 빠른 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 전부 통과했다.

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가 생긴 후에만 승격한다.