{
  "slug": "expo-media",
  "name": "@gj-kit/expo-media",
  "version": "0.6.1",
  "description": "Hardened Expo and React Native media pipeline utilities for upload, picker, metadata, durable files, and device libraries.",
  "homepage": "https://gj-kit.github.io/gj-kit/packages/expo-media/",
  "repository": "git+https://github.com/gj-kit/gj-kit.git",
  "license": "MIT",
  "engines": {
    "node": ">=20"
  },
  "peerDependencies": {
    "expo": ">=56.0.0 <58.0.0",
    "expo-file-system": ">=56.0.0",
    "expo-image-manipulator": ">=56.0.0",
    "expo-image-picker": ">=16.0.0",
    "expo-media-library": ">=56.0.5",
    "expo-video-thumbnails": ">=8.0.0",
    "react-native": ">=0.71.0"
  },
  "peerDependenciesMeta": {
    "expo": {
      "optional": true
    },
    "expo-file-system": {
      "optional": true
    },
    "expo-image-manipulator": {
      "optional": true
    },
    "expo-image-picker": {
      "optional": true
    },
    "expo-media-library": {
      "optional": true
    },
    "expo-video-thumbnails": {
      "optional": true
    },
    "react-native": {
      "optional": true
    }
  },
  "entries": [
    {
      "subpath": ".",
      "id": "root",
      "declarationTarget": "./dist/index.d.cts",
      "symbols": [
        {
          "name": "assertNeverMediaError",
          "slug": "assert-never-media-error",
          "kind": "function",
          "declaration": "/**\n * switch의 `default`에서 호출하면, 라이브러리가 코드를 추가할 때 소비자에게 컴파일 에러가 난다.\n * ⚠ 강제하지 않는다 — 제공만 한다(§6.1-⑫). 코드 추가는 비파괴 변경이어야 하므로\n * 라이브러리가 소비자에게 exhaustive 분기를 요구할 수는 없다.\n */\ndeclare function assertNeverMediaError(code: never): never;",
          "sourceDocumentation": "switch의 `default`에서 호출하면, 라이브러리가 코드를 추가할 때 소비자에게 컴파일 에러가 난다.\n⚠ 강제하지 않는다 — 제공만 한다(§6.1-⑫). 코드 추가는 비파괴 변경이어야 하므로\n라이브러리가 소비자에게 exhaustive 분기를 요구할 수는 없다."
        },
        {
          "name": "BinaryPosterAdapter",
          "slug": "binary-poster-adapter",
          "kind": "interface",
          "declaration": "/** 바이너리 → 포스터 바이너리. 기본 구현은 `\"./web\"`의 canvas 포스터. */\ninterface BinaryPosterAdapter {\n    /** ⚠ 실패는 `null`. 근거는 `LocalPosterAdapter`와 동일. */\n    posterFromBinary(input: {\n        readonly source: BinarySource;\n        readonly atMs: number;\n    }): Promise<BinarySource | null>;\n}",
          "sourceDocumentation": "바이너리 → 포스터 바이너리. 기본 구현은 `\"./web\"`의 canvas 포스터."
        },
        {
          "name": "BinarySource",
          "slug": "binary-source",
          "kind": "interface",
          "declaration": "/**\n * DOM lib 없이 Blob/File을 받기 위한 구조적 최소 타입.\n * 브라우저 Blob·RN Blob·Node Blob이 전부 구조적으로 만족하며,\n * vitest에서 plain object로 전 경로를 도는 것을 가능하게 한다(§10.1).\n */\ninterface BinarySource {\n    readonly size: number;\n    readonly type?: string | undefined;\n    arrayBuffer(): Promise<ArrayBuffer>;\n}",
          "sourceDocumentation": "DOM lib 없이 Blob/File을 받기 위한 구조적 최소 타입.\n브라우저 Blob·RN Blob·Node Blob이 전부 구조적으로 만족하며,\nvitest에서 plain object로 전 경로를 도는 것을 가능하게 한다(§10.1)."
        },
        {
          "name": "BinarySourceLoader",
          "slug": "binary-source-loader",
          "kind": "interface",
          "declaration": "/**\n * uri → 바이너리. 웹 피커가 주는 `blob:`/`data:` URI를 업로드 가능한 소스로 바꾼다.\n * 기본 구현은 `\"./web\"`의 `createFetchBinarySourceLoader`(peer 0). 경로 전문은 §5.7.4.\n *\n * 이 seam이 없으면 웹에서 피커 자산을 업로드하는 유일한 방법이 로컬 파일 스트리밍인데,\n * 그 경로는 웹에 존재하지 않는다 — `LocalUploads.uploadPickedAsset`이\n * `MediaError('platform-unsupported')`를 던지는 이유다.\n */\ninterface BinarySourceLoader {\n    fromUri(input: {\n        readonly uri: string;\n        readonly fileName: string;\n    }): Promise<NamedBinarySource>;\n}",
          "sourceDocumentation": "uri → 바이너리. 웹 피커가 주는 `blob:`/`data:` URI를 업로드 가능한 소스로 바꾼다.\n기본 구현은 `\"./web\"`의 `createFetchBinarySourceLoader`(peer 0). 경로 전문은 §5.7.4.\n\n이 seam이 없으면 웹에서 피커 자산을 업로드하는 유일한 방법이 로컬 파일 스트리밍인데,\n그 경로는 웹에 존재하지 않는다 — `LocalUploads.uploadPickedAsset`이\n`MediaError('platform-unsupported')`를 던지는 이유다."
        },
        {
          "name": "BinaryTransport",
          "slug": "binary-transport",
          "kind": "interface",
          "declaration": "/** 웹 Blob PUT · 포스터 PUT. fetch 기반 기본 구현은 `\"./web\"`이 제공한다. */\ninterface BinaryTransport {\n    putBinary(input: PutRequest & {\n        readonly body: BinarySource;\n    }): Promise<{\n        readonly status: number;\n    }>;\n}",
          "sourceDocumentation": "웹 Blob PUT · 포스터 PUT. fetch 기반 기본 구현은 `\"./web\"`이 제공한다."
        },
        {
          "name": "BinaryUploads",
          "slug": "binary-uploads",
          "kind": "interface",
          "declaration": "interface BinaryUploads<TAsset, TCollectionId extends string = string> {\n    uploadBinary(input: {\n        readonly source: NamedBinarySource;\n        readonly collectionId?: TCollectionId | null | undefined;\n        /**\n         * JPEG APP1 파싱이 실패했을 때 **필드 단위로** 병합될 EXIF(§5.3 `mediaMetadataFromJpeg` 규칙 ②).\n         * 웹 피커 경로가 `asset.exif`를 여기로 넘긴다(전신 uploader.ts:709 · §5.7.4).\n         * 없으면 촬영 시각과 위치가 조용히 유실된다 — 이 인자가 복원된 이유다(G6).\n         */\n        readonly fallbackExif?: Readonly<Record<string, unknown>> | undefined;\n        /**\n         * 동영상 포스터. **3상태를 보존한다**(전신 `BlobVideoUploadInput.posterBlob`, uploader.ts:566-569):\n         *   `undefined` = 어댑터로 자동 추출 / `null` = 포스터 없음(추출 시도 금지) / 값 = 주어진 포스터.\n         * 이 3상태를 2상태로 접으면 \"포스터를 일부러 안 만든다\"를 표현할 방법이 사라진다.\n         */\n        readonly poster?: BinarySource | null | undefined;\n        /**\n         * 동영상 재생시간(**밀리초**). 이미지에는 무시된다.\n         * ⚠ 초 단위를 넣지 마라 — 웹의 `HTMLVideoElement.duration`은 **초**다. 호출자가\n         *   `normalizeDurationMs`(§7 하드닝 4)를 거쳐 넘겨야 한다. 20분 동영상이 1200ms로\n         *   저장되면 어떤 duration 상한도 통과한다. `createPickerFlows`의 웹 경로는 이미 거친다.\n         */\n        readonly durationMs?: number | null | undefined;\n        /**\n         * 동영상 픽셀 치수. 이미지에는 무시된다(이미지 치수는 서버가 바이트에서 읽는다).\n         * ⚠ `BinarySource`만으로는 DOM 없이 복원할 수 없으므로 **호출자가 주지 않으면 영구 유실**이다 —\n         *   전신 `BlobVideoUploadInput.width/height`(uploader.ts:86-87)가 완료 페이로드로 가던 값이다.\n         */\n        readonly dimensions?: {\n            readonly width?: number | null | undefined;\n            readonly height?: number | null | undefined;\n        } | undefined;\n    }): Promise<UploadResult<TAsset>>;\n    /**\n     * 웹 드롭 다건.\n     * ⚠ 첫 presign **이전에** 배치 전체를 검증한다 — 혼합 드롭 부분 업로드 방지(§7 하드닝 10).\n     * ⚠ 검증은 `maxFiles` slice **이후**에 수행한다(전신 uploader.ts:623→630 순서 보존).\n     * 규칙의 거처는 `webBatch.ts`다.\n     */\n    uploadDropped(files: readonly NamedBinarySource[], options?: {\n        readonly collectionId?: TCollectionId | null | undefined;\n        readonly maxFiles?: number | undefined;\n    } | undefined): Promise<readonly UploadResult<TAsset>[]>;\n}"
        },
        {
          "name": "BrowserSaveAdapter",
          "slug": "browser-save-adapter",
          "kind": "interface",
          "declaration": "interface BrowserSaveAdapter {\n    /**\n     * DOM 접근을 어댑터 안에 가둔다. `document`/`fetch`는 `\"./web\"`에서 **필수 주입**(§6.1-⑬) —\n     * 전신은 미주입 시 `globalThis.document`로 폴백해 네이티브에서도 조용히 생성됐다.\n     */\n    saveByDownload(input: {\n        readonly url: string;\n        readonly fileName: string;\n    }): Promise<void>;\n}"
        },
        {
          "name": "capturedAtFromExif",
          "slug": "captured-at-from-exif",
          "kind": "function",
          "declaration": "/**\n * Resolves the preferred EXIF datetime field to an ISO instant.\n *\n * Normal `mediaMetadataFromExif` callers should retain its device-local\n * default. Consumers that know an immutable historical event offset can pass\n * it here and avoid silently interpreting a travel photo in the device's\n * *current* time zone. A datetime which already carries an explicit ISO offset\n * retains that offset; malformed timezone-less EXIF values are rejected.\n */\ndeclare function capturedAtFromExif(exif?: Readonly<Record<string, unknown>> | null, options?: ExifCapturedAtOptions | undefined): string | undefined;",
          "sourceDocumentation": "Resolves the preferred EXIF datetime field to an ISO instant.\n\nNormal `mediaMetadataFromExif` callers should retain its device-local\ndefault. Consumers that know an immutable historical event offset can pass\nit here and avoid silently interpreting a travel photo in the device's\n*current* time zone. A datetime which already carries an explicit ISO offset\nretains that offset; malformed timezone-less EXIF values are rejected."
        },
        {
          "name": "ChunkRange",
          "slug": "chunk-range",
          "kind": "type",
          "declaration": "type ChunkRange = {\n    readonly position: number;\n    readonly length: number;\n};"
        },
        {
          "name": "computeChunkRanges",
          "slug": "compute-chunk-ranges",
          "kind": "function",
          "declaration": "/**\n * `size` 바이트 파일을 순차 `[position, length)` 창으로 나눈다. 순수 함수.\n *\n * ⚠ **`chunkBytes` 인자를 공개하지 않는다**(§6.1-⑩). 전신 `hashFile.ts:18`은 기본 인자로 이것을\n * 열어 두었고, \"3의 배수\"라는 제약은 타입으로 표현할 수 없으므로 그 인자가 곧 하드닝 9의\n * 회귀 통로였다. 실제로 전신 테스트가 `computeChunkRanges(100, 1000)`처럼 3의 배수가 아닌 값을\n * 넘기고 있었다.\n */\ndeclare function computeChunkRanges(size: number): readonly ChunkRange[];",
          "sourceDocumentation": "`size` 바이트 파일을 순차 `[position, length)` 창으로 나눈다. 순수 함수.\n\n⚠ **`chunkBytes` 인자를 공개하지 않는다**(§6.1-⑩). 전신 `hashFile.ts:18`은 기본 인자로 이것을\n열어 두었고, \"3의 배수\"라는 제약은 타입으로 표현할 수 없으므로 그 인자가 곧 하드닝 9의\n회귀 통로였다. 실제로 전신 테스트가 `computeChunkRanges(100, 1000)`처럼 3의 배수가 아닌 값을\n넘기고 있었다."
        },
        {
          "name": "createBinaryUploads",
          "slug": "create-binary-uploads",
          "kind": "function",
          "declaration": "declare function createBinaryUploads<TAsset, TCollectionId extends string = string>(config: MediaUploadConfig<TAsset, TCollectionId> & {\n    readonly transport: BinaryTransport;\n    /** 생략 = core 내장 순수 TS SHA-256(§9). `FileSystemAdapter`가 없으므로 바이너리 경로만 쓴다. */\n    readonly hasher?: HashAdapter | undefined;\n    /** 생략 = 자동 포스터 추출 없음. 호출자가 `poster` 값을 직접 줄 수는 있다. */\n    readonly poster?: BinaryPosterAdapter | undefined;\n    readonly posterAtMs?: number | undefined;\n}): BinaryUploads<TAsset, TCollectionId>;"
        },
        {
          "name": "createDeferredLocalUploads",
          "slug": "create-deferred-local-uploads",
          "kind": "function",
          "declaration": "/**\n * Presign → 네이티브 스트리밍 PUT까지만 수행하고, 나중의 도메인 트랜잭션에 연결할 attachment를 반환한다.\n *\n * `api`에는 `createUploadIntent`만 있으면 된다. 이 함수는 `completeUpload`를 흉내 내거나 호출하지 않는다.\n */\ndeclare function createDeferredLocalUploads<TCollectionId extends string = string>(config: DeferredLocalUploadConfig<TCollectionId>): DeferredLocalUploads<TCollectionId>;",
          "sourceDocumentation": "Presign → 네이티브 스트리밍 PUT까지만 수행하고, 나중의 도메인 트랜잭션에 연결할 attachment를 반환한다.\n\n`api`에는 `createUploadIntent`만 있으면 된다. 이 함수는 `completeUpload`를 흉내 내거나 호출하지 않는다."
        },
        {
          "name": "createDeviceLibrary",
          "slug": "create-device-library",
          "kind": "function",
          "declaration": "declare function createDeviceLibrary(input: {\n    readonly adapter: DeviceLibraryAdapter;\n    readonly files: FileSystemAdapter;\n    /**\n     * ⚠ **필수 인자다**(§3.1 · §7 하드닝 7). 스테이징 사본을 만드는 주체가 지우는 주체를 반드시\n     * 갖게 하려는 것이다. 옵셔널로 두면 \"카피는 하는데 지우는 사람이 없는\" 조립이 컴파일을 통과하고,\n     * 업로드한 모든 사진의 원본 사본이 앱 컨테이너에 영구 축적된다 — 타입도 테스트도 못 잡는다.\n     */\n    readonly staging: StagingCache;\n    readonly platform: PlatformAdapter;\n    readonly strings?: MediaStrings | undefined;\n    readonly debug?: MediaDebugOptions | undefined;\n}): DeviceLibrary;"
        },
        {
          "name": "createDeviceUploads",
          "slug": "create-device-uploads",
          "kind": "function",
          "declaration": "declare function createDeviceUploads<TAsset, TCollectionId extends string = string>(input: {\n    readonly device: DeviceLibrary;\n    readonly uploads: LocalUploads<TAsset, TCollectionId>;\n    readonly staging: StagingCache;\n}): DeviceUploads<TAsset, TCollectionId>;"
        },
        {
          "name": "createDurableFileStore",
          "slug": "create-durable-file-store",
          "kind": "function",
          "declaration": "/**\n * Create a durable, app-owned file store from a host storage adapter.\n *\n * The store never trusts caller-supplied paths as complete URIs. It constructs\n * them from individually validated segments, so a cleanup request cannot turn\n * into deletion of a sibling app file through `..`, encoded separators, or a\n * prefix collision.\n */\ndeclare function createDurableFileStore(input: {\n    readonly root: string;\n    readonly files: DurableFileStoreAdapter;\n}): DurableFileStore;",
          "sourceDocumentation": "Create a durable, app-owned file store from a host storage adapter.\n\nThe store never trusts caller-supplied paths as complete URIs. It constructs\nthem from individually validated segments, so a cleanup request cannot turn\ninto deletion of a sibling app file through `..`, encoded separators, or a\nprefix collision."
        },
        {
          "name": "createExpoBinaryTransport",
          "slug": "create-expo-binary-transport",
          "kind": "function",
          "declaration": "/**\n * `BinaryTransport`의 기본 구현 — 전역 `fetch` 기반 PUT.\n *\n * ⚠ `\"./web\"`의 `createFetchBinaryTransport`와 **역할이 다르다.** 저쪽은 `fetch`를 주입받는\n * DOM 엔트리의 구현이고, 이쪽은 골든패스가 아무 조립 없이도 동작하게 하는 기본값이다.\n * peer는 늘지 않는다 — RN도 브라우저도 fetch를 전역으로 갖는다.\n *\n * ⚠ **로컬 파일 업로드에는 쓰지 않는다.** 바이트를 JS 힙으로 올리는 순간 §7 하드닝 1이\n * 지키려던 것(대용량 파일의 네이티브 스트리밍)이 사라진다. 로컬 URI의 정본은\n * `createExpoLocalFileTransport`뿐이고, 이 전송은 이미 메모리에 있는 바이너리\n * (웹 Blob·포스터 프레임)만 다룬다.\n */\ndeclare function createExpoBinaryTransport(): BinaryTransport;",
          "sourceDocumentation": "`BinaryTransport`의 기본 구현 — 전역 `fetch` 기반 PUT.\n\n⚠ `\"./web\"`의 `createFetchBinaryTransport`와 **역할이 다르다.** 저쪽은 `fetch`를 주입받는\nDOM 엔트리의 구현이고, 이쪽은 골든패스가 아무 조립 없이도 동작하게 하는 기본값이다.\npeer는 늘지 않는다 — RN도 브라우저도 fetch를 전역으로 갖는다.\n\n⚠ **로컬 파일 업로드에는 쓰지 않는다.** 바이트를 JS 힙으로 올리는 순간 §7 하드닝 1이\n지키려던 것(대용량 파일의 네이티브 스트리밍)이 사라진다. 로컬 URI의 정본은\n`createExpoLocalFileTransport`뿐이고, 이 전송은 이미 메모리에 있는 바이너리\n(웹 Blob·포스터 프레임)만 다룬다."
        },
        {
          "name": "createExpoDocumentFileStore",
          "slug": "create-expo-document-file-store",
          "kind": "function",
          "declaration": "/**\n * App-owned persistent local-file store backed by Expo's document directory.\n *\n * It deliberately does not share the cache-backed upload staging store: files\n * returned from `copy()` are safe to persist in an application's database.\n */\ndeclare function createExpoDocumentFileStore(input: {\n    readonly root: string;\n}): DurableFileStore;",
          "sourceDocumentation": "App-owned persistent local-file store backed by Expo's document directory.\n\nIt deliberately does not share the cache-backed upload staging store: files\nreturned from `copy()` are safe to persist in an application's database."
        },
        {
          "name": "createExpoFileSystem",
          "slug": "create-expo-file-system",
          "kind": "function",
          "declaration": "/** §5.5 — 골든패스가 기본으로 채우는 파일 I/O 어댑터. */\ndeclare function createExpoFileSystem(): FileSystemAdapter & FileDownloadAdapter;",
          "sourceDocumentation": "§5.5 — 골든패스가 기본으로 채우는 파일 I/O 어댑터."
        },
        {
          "name": "createExpoHasher",
          "slug": "create-expo-hasher",
          "kind": "function",
          "declaration": "/**\n * expo 파일시스템 위의 기본 `HashAdapter`.\n *\n * `files`를 주면 그것을 쓴다 — 킷이 이미 만들어 둔 파일시스템 어댑터를 재사용하기 위한 구멍이다\n * (같은 킷 안에서 캐시 디렉토리 판정이 두 벌 생기지 않게). 생략하면 새로 만든다.\n */\ndeclare function createExpoHasher(input?: {\n    readonly files?: FileSystemAdapter | undefined;\n} | undefined): HashAdapter;",
          "sourceDocumentation": "expo 파일시스템 위의 기본 `HashAdapter`.\n\n`files`를 주면 그것을 쓴다 — 킷이 이미 만들어 둔 파일시스템 어댑터를 재사용하기 위한 구멍이다\n(같은 킷 안에서 캐시 디렉토리 판정이 두 벌 생기지 않게). 생략하면 새로 만든다."
        },
        {
          "name": "createExpoLocalFileTransport",
          "slug": "create-expo-local-file-transport",
          "kind": "function",
          "declaration": "/**\n * §5.5 — 골든패스가 기본으로 채우는 로컬 파일 전송 어댑터.\n *\n * ⚠ **web/SSR에서 쓰지 말 것**(§8.5 · V-B 실측). `expo-file-system`의 web 셰이프에는 이\n * 업로드 경로가 아예 없고 레거시 태스크는 `{ body:'', status:0, headers:{} }`를 돌려주는 no-op다 —\n * 태우면 **조용히 성공한 것처럼 보인다**. 비네이티브의 정본은 `\"./web\"`의\n * `createFetchBinaryTransport`뿐이다.\n */\ndeclare function createExpoLocalFileTransport(): LocalFileTransport;",
          "sourceDocumentation": "§5.5 — 골든패스가 기본으로 채우는 로컬 파일 전송 어댑터.\n\n⚠ **web/SSR에서 쓰지 말 것**(§8.5 · V-B 실측). `expo-file-system`의 web 셰이프에는 이\n업로드 경로가 아예 없고 레거시 태스크는 `{ body:'', status:0, headers:{} }`를 돌려주는 no-op다 —\n태우면 **조용히 성공한 것처럼 보인다**. 비네이티브의 정본은 `\"./web\"`의\n`createFetchBinaryTransport`뿐이다."
        },
        {
          "name": "createFileHasher",
          "slug": "create-file-hasher",
          "kind": "function",
          "declaration": "/**\n * `FileSystemAdapter` 위에 base64 창 스트리밍 해시를 조립한다. 기본 `HashAdapter` 구현이다.\n *\n * 해시는 **원본 바이트**에 대해 계산되므로 같은 기기 사진이면 실행 간에도 값이 안정적이다\n * (전신 주석 보존) — 그것이 dedup 키로 쓸 수 있는 이유다.\n */\ndeclare function createFileHasher(input: {\n    readonly files: FileSystemAdapter;\n}): HashAdapter;",
          "sourceDocumentation": "`FileSystemAdapter` 위에 base64 창 스트리밍 해시를 조립한다. 기본 `HashAdapter` 구현이다.\n\n해시는 **원본 바이트**에 대해 계산되므로 같은 기기 사진이면 실행 간에도 값이 안정적이다\n(전신 주석 보존) — 그것이 dedup 키로 쓸 수 있는 이유다."
        },
        {
          "name": "createLocalMediaSaver",
          "slug": "create-local-media-saver",
          "kind": "function",
          "declaration": "/**\n * Create a saver for files that are already owned by the application.\n *\n * Unlike the remote `createMediaSaver`, this never downloads or deletes source\n * files. It asks the OS for write permission at most once per non-empty batch,\n * and only after confirming that at least one non-empty `file://` source is\n * available.\n */\ndeclare function createLocalMediaSaver(input: {\n    readonly files: LocalMediaSaveFileAdapter;\n    readonly library: MediaLibrarySaveAdapter;\n    readonly strings?: MediaStrings | undefined;\n    readonly telemetry?: MediaTelemetry | undefined;\n}): LocalMediaSaver;",
          "sourceDocumentation": "Create a saver for files that are already owned by the application.\n\nUnlike the remote `createMediaSaver`, this never downloads or deletes source\nfiles. It asks the OS for write permission at most once per non-empty batch,\nand only after confirming that at least one non-empty `file://` source is\navailable."
        },
        {
          "name": "createLocalUploads",
          "slug": "create-local-uploads",
          "kind": "function",
          "declaration": "/** 보통의 presign → PUT → 서버 등록 흐름. */\ndeclare function createLocalUploads<TAsset, TCollectionId extends string = string>(config: MediaUploadConfig<TAsset, TCollectionId> & LocalUploadAdapters): LocalUploads<TAsset, TCollectionId>;",
          "sourceDocumentation": "보통의 presign → PUT → 서버 등록 흐름."
        },
        {
          "name": "createMediaDebugLogger",
          "slug": "create-media-debug-logger",
          "kind": "function",
          "declaration": "/**\n * 게이트: `platform.isDev && platform.os !== 'web'` (전신 `debugEnabled()` 보존).\n *\n * 전신의 세 번째 조건이던 \"테스트 환경 제외\"는 `PlatformAdapter.isDev`가 흡수한다\n * (기본 어댑터가 `__DEV__ && NODE_ENV !== 'test'`로 채운다 — §3.3). 코어에는 그 전역이 없다.\n * `options.enabled`는 호스트의 명시적 스위치이며, 생략하면 전신과 동일하게 플랫폼 게이트만 남는다.\n *\n * ⚠ 게이트가 닫혀 있으면 두 메서드 모두 **완전 no-op**이다 — details를 만드는 비용도 치르지 않는다.\n */\ndeclare function createMediaDebugLogger(input: {\n    readonly platform: PlatformAdapter;\n    readonly options?: MediaDebugOptions | undefined;\n}): MediaDebugLogger;",
          "sourceDocumentation": "게이트: `platform.isDev && platform.os !== 'web'` (전신 `debugEnabled()` 보존).\n\n전신의 세 번째 조건이던 \"테스트 환경 제외\"는 `PlatformAdapter.isDev`가 흡수한다\n(기본 어댑터가 `__DEV__ && NODE_ENV !== 'test'`로 채운다 — §3.3). 코어에는 그 전역이 없다.\n`options.enabled`는 호스트의 명시적 스위치이며, 생략하면 전신과 동일하게 플랫폼 게이트만 남는다.\n\n⚠ 게이트가 닫혀 있으면 두 메서드 모두 **완전 no-op**이다 — details를 만드는 비용도 치르지 않는다."
        },
        {
          "name": "createMediaKit",
          "slug": "create-media-kit",
          "kind": "function",
          "declaration": "declare function createMediaKit<TAsset, TCollectionId extends string = string>(config: MediaKitConfig<TAsset, TCollectionId>): MediaKit<TAsset, TCollectionId>;"
        },
        {
          "name": "createMediaPickerActions",
          "slug": "create-media-picker-actions",
          "kind": "function",
          "declaration": "/**\n * Build an upload-independent camera and library picker flow.\n *\n * This is the right boundary for app-owned workflows such as crop, image analysis, and OCR:\n * it owns OS permissions and untrusted picker-result normalization, while the app owns what it\n * does with the returned local URI.\n */\ndeclare function createMediaPickerActions(input: {\n    readonly picker: PickerAdapter;\n    readonly strings?: MediaStrings | undefined;\n}): MediaPickerActions;",
          "sourceDocumentation": "Build an upload-independent camera and library picker flow.\n\nThis is the right boundary for app-owned workflows such as crop, image analysis, and OCR:\nit owns OS permissions and untrusted picker-result normalization, while the app owns what it\ndoes with the returned local URI."
        },
        {
          "name": "createMediaSaver",
          "slug": "create-media-saver",
          "kind": "function",
          "declaration": "declare function createMediaSaver(input: {\n    readonly target: SaveTarget;\n    readonly fileNamePrefix?: string | undefined;\n    readonly strings?: MediaStrings | undefined;\n    readonly telemetry?: MediaTelemetry | undefined;\n}): MediaSaver;"
        },
        {
          "name": "createPendingSelection",
          "slug": "create-pending-selection",
          "kind": "function",
          "declaration": "/**\n * Build a pure staged-selection model: dedup by stable identity, a hard cap with per-item\n * rejection reasons, exactly-once preview release, HEIC/HEIF preview gating, and capture-time\n * extraction. State is an immutable ordered list owned by the host (React state, a store, a\n * plain variable) — the selection holds no items itself.\n *\n * The revoke-once registry is shared through a global symbol, so the guarantee holds per item\n * object across selection instances and across the package's entry copies (`.` and `./core`,\n * ESM and CJS) within one realm.\n *\n * @throws MediaError `config-invalid` when `max` is not a positive integer.\n */\ndeclare function createPendingSelection(options: PendingSelectionOptions): PendingSelection;",
          "sourceDocumentation": "Build a pure staged-selection model: dedup by stable identity, a hard cap with per-item\nrejection reasons, exactly-once preview release, HEIC/HEIF preview gating, and capture-time\nextraction. State is an immutable ordered list owned by the host (React state, a store, a\nplain variable) — the selection holds no items itself.\n\nThe revoke-once registry is shared through a global symbol, so the guarantee holds per item\nobject across selection instances and across the package's entry copies (`.` and `./core`,\nESM and CJS) within one realm."
        },
        {
          "name": "createPickerFlows",
          "slug": "create-picker-flows",
          "kind": "function",
          "declaration": "declare function createPickerFlows<TAsset, TCollectionId extends string = string>(input: {\n    readonly picker: PickerAdapter;\n    readonly uploads: LocalUploads<TAsset, TCollectionId>;\n    readonly platform: PlatformAdapter;\n    readonly strings?: MediaStrings | undefined;\n    /**\n     * 웹 경로에서 `asset.fileName`이 없을 때 생성되는 폴백 파일명의 접두사.\n     * 전신은 uploader의 `fileNamePrefix`를 썼다(uploader.ts:701) — 생략하면 `mediaFileName`\n     * 기본값(`'media'`)이 되어 호스트 접두사가 웹 경로에서만 달라진다.\n     * 네이티브 경로는 `uploads`가 이미 자기 접두사를 갖고 있으므로 영향받지 않는다.\n     */\n    readonly fileNamePrefix?: string | undefined;\n    /**\n     * ⚠ `platform.os === 'web'`에서 피커 자산을 업로드하려면 **필수**. 없으면\n     * `MediaError('platform-unsupported')`. 조건부 타입 0 — 런타임 분기다(§3.1).\n     *\n     * 실측 근거(§5.7.4/V9-d): memorylog2 `app/profile-edit.tsx:137`의 `pickAndUploadPhoto()`에는\n     * **플랫폼 게이트가 없다**. 웹에서 그 호출은 전신 uploader.ts:692-710으로 들어가\n     * `fetch(asset.uri)` → Blob → `uploadImageBlob({ fallbackExif: asset.exif })`를 탔다.\n     * 그 경로를 잃지 않으려면 변환 담당자(`loader`)와 업로더(`uploads`)가 여기 주입돼야 한다.\n     */\n    readonly web?: {\n        readonly uploads: BinaryUploads<TAsset, TCollectionId>;\n        readonly loader: BinarySourceLoader;\n    } | undefined;\n}): PickerFlows<TAsset, TCollectionId>;"
        },
        {
          "name": "createSha256",
          "slug": "create-sha256",
          "kind": "function",
          "declaration": "declare function createSha256(): Sha256Hasher;"
        },
        {
          "name": "createStagingCache",
          "slug": "create-staging-cache",
          "kind": "function",
          "declaration": "declare function createStagingCache(input: {\n    /** `/^[a-z0-9][a-z0-9-]{1,30}$/` — 위반 시 `MediaError('config-invalid')`. 부팅 시 즉사. */\n    readonly namespace: string;\n    readonly files: FileSystemAdapter;\n}): StagingCache;"
        },
        {
          "name": "DeferredLocalUpload",
          "slug": "deferred-local-upload",
          "kind": "type",
          "declaration": "/**\n * PUT에 성공한 뒤, 나중의 앱 트랜잭션에 연결할 수 있도록 돌려주는 오브젝트 메타데이터.\n * `MediaUploadCompletion`과 같은 형태를 써서 앱이 별도 변환 없이 자기 API에 전달할 수 있다.\n */\ntype DeferredLocalUpload<TCollectionId extends string = string> = MediaUploadCompletion<TCollectionId>;",
          "sourceDocumentation": "PUT에 성공한 뒤, 나중의 앱 트랜잭션에 연결할 수 있도록 돌려주는 오브젝트 메타데이터.\n`MediaUploadCompletion`과 같은 형태를 써서 앱이 별도 변환 없이 자기 API에 전달할 수 있다."
        },
        {
          "name": "DeferredLocalUploadConfig",
          "slug": "deferred-local-upload-config",
          "kind": "type",
          "declaration": "/**\n * `completeUpload` 없이 presign → 네이티브 PUT까지만 수행하는 설정.\n *\n * 반환된 `DeferredLocalUpload`은 서버 등록 요청처럼 보이게 만든 가짜 값이 아니다. 호출자가\n * 자기 도메인 트랜잭션에서 `objectName`과 메타데이터를 연결할 수 있는 완료 페이로드다.\n */\ntype DeferredLocalUploadConfig<TCollectionId extends string = string> = MediaUploadBaseConfig<TCollectionId> & LocalStreamingUploadAdapters & {\n    readonly api: MediaUploadIntentApi<TCollectionId>;\n};",
          "sourceDocumentation": "`completeUpload` 없이 presign → 네이티브 PUT까지만 수행하는 설정.\n\n반환된 `DeferredLocalUpload`은 서버 등록 요청처럼 보이게 만든 가짜 값이 아니다. 호출자가\n자기 도메인 트랜잭션에서 `objectName`과 메타데이터를 연결할 수 있는 완료 페이로드다."
        },
        {
          "name": "DeferredLocalUploads",
          "slug": "deferred-local-uploads",
          "kind": "interface",
          "declaration": "/** `createDeferredLocalUploads`의 최소 능력. 피커 흐름이나 앱 자산 등록은 의도적으로 포함하지 않는다. */\ninterface DeferredLocalUploads<TCollectionId extends string = string> {\n    uploadLocalFile(input: LocalUploadInput<TCollectionId>): Promise<DeferredLocalUpload<TCollectionId>>;\n}",
          "sourceDocumentation": "`createDeferredLocalUploads`의 최소 능력. 피커 흐름이나 앱 자산 등록은 의도적으로 포함하지 않는다."
        },
        {
          "name": "detectImageContentType",
          "slug": "detect-image-content-type",
          "kind": "function",
          "declaration": "/** 엄격 감지의 이미지 전용 판. 근거는 `detectMediaContentType`와 동일하다. */\ndeclare function detectImageContentType(mime?: string | null, nameOrUri?: string | null): ImageContentType | null;",
          "sourceDocumentation": "엄격 감지의 이미지 전용 판. 근거는 `detectMediaContentType`와 동일하다."
        },
        {
          "name": "detectMediaContentType",
          "slug": "detect-media-content-type",
          "kind": "function",
          "declaration": "/**\n * 엄격 감지 — MIME도 확장자도 지원 형식을 지목하지 못하면 `null`.\n *\n * 전신 주석의 계약 원문:\n *   \"잘못 라벨링하면 데이터가 손상되는 곳(예: 백그라운드 동기화 스캐너)에는 이 엄격 감지를 쓰고,\n *    `infer*` 폴백은 **OS가 종류를 이미 보장하는 곳**(카메라·피커 출력)에만 쓴다.\"\n */\ndeclare function detectMediaContentType(mime?: string | null, nameOrUri?: string | null): MediaContentType | null;",
          "sourceDocumentation": "엄격 감지 — MIME도 확장자도 지원 형식을 지목하지 못하면 `null`.\n\n전신 주석의 계약 원문:\n  \"잘못 라벨링하면 데이터가 손상되는 곳(예: 백그라운드 동기화 스캐너)에는 이 엄격 감지를 쓰고,\n   `infer*` 폴백은 **OS가 종류를 이미 보장하는 곳**(카메라·피커 출력)에만 쓴다.\""
        },
        {
          "name": "DeviceAlbum",
          "slug": "device-album",
          "kind": "type",
          "declaration": "type DeviceAlbum = {\n    readonly id: string;\n    readonly title: string;\n    readonly count: number;\n};"
        },
        {
          "name": "DeviceAsset",
          "slug": "device-asset",
          "kind": "type",
          "declaration": "type DeviceAsset = DeviceAssetRef & {\n    readonly uri: string;\n    readonly width: number;\n    readonly height: number;\n    readonly mediaType: MediaKind;\n    readonly creationTime?: number | undefined;\n};"
        },
        {
          "name": "deviceAssetCapturedAt",
          "slug": "device-asset-captured-at",
          "kind": "function",
          "declaration": "/**\n * 촬영 시각(ms 에폭) → ISO 문자열. 전신 `deviceAssetCapturedAt`(devicePhotoLibrary.ts:372-378).\n *\n * peer 0인 순수 함수이므로 `\"./device\"`가 아니라 `\"./core\"`에 둔다(§5.7.5).\n * ⚠ `creationTime`이 0이면 null이다 — 전신의 falsy 판정을 그대로 보존한다. 에폭 0(1970-01-01)은\n *   기기가 시각을 모른다는 뜻이지 실제 촬영 시각이 아니며, 이것을 ISO로 흘리면 서버의\n *   타임라인 정렬이 1970년으로 끌려간다.\n */\ndeclare function deviceAssetCapturedAt(asset: {\n    readonly creationTime?: number | undefined;\n}): string | null;",
          "sourceDocumentation": "촬영 시각(ms 에폭) → ISO 문자열. 전신 `deviceAssetCapturedAt`(devicePhotoLibrary.ts:372-378).\n\npeer 0인 순수 함수이므로 `\"./device\"`가 아니라 `\"./core\"`에 둔다(§5.7.5).\n⚠ `creationTime`이 0이면 null이다 — 전신의 falsy 판정을 그대로 보존한다. 에폭 0(1970-01-01)은\n  기기가 시각을 모른다는 뜻이지 실제 촬영 시각이 아니며, 이것을 ISO로 흘리면 서버의\n  타임라인 정렬이 1970년으로 끌려간다."
        },
        {
          "name": "DeviceAssetInfo",
          "slug": "device-asset-info",
          "kind": "type",
          "declaration": "type DeviceAssetInfo = {\n    /** 앱 컨테이너 밖(iOS Photos 컨테이너)일 수 있다 — core가 캐시로 실체화한다(§7 하드닝 2). */\n    readonly localUri?: string | undefined;\n    readonly uri?: string | undefined;\n    readonly exif?: Readonly<Record<string, unknown>> | undefined;\n    /** true면 원본이 iCloud에만 있다. core 기본 정책은 여기서 중단(§7 하드닝 6). */\n    readonly isNetworkAsset: boolean;\n};"
        },
        {
          "name": "DeviceAssetPage",
          "slug": "device-asset-page",
          "kind": "type",
          "declaration": "type DeviceAssetPage = {\n    readonly assets: readonly DeviceAsset[];\n    /**\n     * §1-7 규약(`?: T | undefined`)을 따른다 — 초안은 필수-undefined였다(G20-14).\n     * 필수로 두면 3자 어댑터 구현자가 값 없는 마지막 페이지에서도 키를 명시해야 한다.\n     */\n    readonly endCursor?: string | undefined;\n    readonly hasNextPage: boolean;\n    readonly totalCount: number;\n};"
        },
        {
          "name": "DeviceAssetRef",
          "slug": "device-asset-ref",
          "kind": "type",
          "declaration": "/**\n * 하드닝된 업로드 resolve가 필요로 하는 최소 정체성. 전체 `MediaLibrary.Asset`이 이것을\n * 구조적으로 만족하며, 동기화 큐는 이 두 필드만 저장해 두고 넘길 수 있다\n * (전신 devicePhotoLibrary.ts:21-24 주석 계승).\n */\ntype DeviceAssetRef = {\n    readonly id: string;\n    readonly filename: string;\n};",
          "sourceDocumentation": "하드닝된 업로드 resolve가 필요로 하는 최소 정체성. 전체 `MediaLibrary.Asset`이 이것을\n구조적으로 만족하며, 동기화 큐는 이 두 필드만 저장해 두고 넘길 수 있다\n(전신 devicePhotoLibrary.ts:21-24 주석 계승)."
        },
        {
          "name": "DeviceKit",
          "slug": "device-kit",
          "kind": "interface",
          "declaration": "/** `DeviceLibrary` + `DeviceUploads`를 한 객체로 — staging·files는 `MediaKit`이 공급한다(§5.5). */\ninterface DeviceKit<TAsset, TCollectionId extends string = string> extends DeviceLibrary, DeviceUploads<TAsset, TCollectionId> {\n}",
          "sourceDocumentation": "`DeviceLibrary` + `DeviceUploads`를 한 객체로 — staging·files는 `MediaKit`이 공급한다(§5.5)."
        },
        {
          "name": "DeviceLibrary",
          "slug": "device-library",
          "kind": "interface",
          "declaration": "interface DeviceLibrary {\n    /** 순수 조회. 요청하지 않는다. */\n    getPermission(): Promise<MediaPermission>;\n    /**\n     * 권한 합성 규칙의 **유일한 거처**(§5.4-④(c), G17):\n     * 조회 → `!granted && canAskAgain`일 때만 요청 → `accessPrivileges === 'limited'` 매핑\n     * (마지막 매핑은 어댑터 몫 — §3.3).\n     *\n     * ⚠ raw `requestPermission()`은 **공개하지 않는다**. `canAskAgain`을 무시하는 재요청이\n     * 정확히 iOS UI 데드록(재요청해도 아무 일도 일어나지 않아 화면이 영원히 기다린다)의 원인이며,\n     * 골든패스에 그 문을 두면 반드시 누군가 그 문으로 들어간다. 필요한 소비자는\n     * `DeviceLibraryAdapter.requestPermission()`을 직접 쓴다.\n     */\n    ensurePermission(): Promise<MediaPermission>;\n    fetchPage(input?: {\n        readonly albumId?: string | null | undefined;\n        readonly after?: string | undefined;\n        readonly pageSize?: number | undefined;\n        readonly kinds?: readonly MediaKind[] | undefined;\n    } | undefined): Promise<DeviceAssetPage>;\n    /** ⚠ core가 `count > 0` 필터 + count 내림차순 정렬을 **수행한다**(§5.4-④(d)). */\n    fetchAlbums(): Promise<readonly DeviceAlbum[]>;\n    /**\n     * 하드닝된 자산정보 조회 — 그리드/스캐너/업로드가 공유하는 단일 관문(§5.7.5, G9 승격).\n     * 기본값은 iCloud 다운로드 없음 + 15s 데드라인, 옵트인 시 60s(§7 하드닝 6).\n     * 데드라인 초과는 `MediaError('device-timeout')`이며, 어댑터 실패는 URL·원본 예외를\n     * 신뢰하지 않고 새 `MediaError('device-library-failed')`로 정규화한다.\n     *\n     * ⚠ 이 메서드를 공개하지 않으면 동기화 스캐너가 자체 조회를 짜게 되고, 그 순간 15초 데드라인과\n     * `downloadFromNetwork: false` 기본값이 **스캔 경로에서만** 사라진다 — 하드닝 6의 조용한 절반 소멸.\n     */\n    getAssetInfo(assetId: string, options?: {\n        readonly downloadFromICloud?: boolean | undefined;\n        readonly infoTimeoutMs?: number | undefined;\n        readonly downloadTimeoutMs?: number | undefined;\n    } | undefined): Promise<DeviceAssetInfo>;\n    /**\n     * 원본 바이트 위치만 필요한 경로(동기화 엔진)용. 전신 `resolveDeviceAssetSourceForUpload`.\n     * iCloud 가드 · 이중 타임아웃 · iOS 캐시 실체화(§7 하드닝 2·6).\n     */\n    resolveForUpload(asset: DeviceAssetRef, options?: DeviceResolveOptions | undefined): Promise<ResolvedDeviceAsset>;\n    /**\n     * 화면 경로용. 전신 `resolveDeviceAssetForUpload`(G4).\n     *\n     * ⚠ 후보 목록에 `asset.uri`를 **자동으로 덧붙인다**(전신 devicePhotoLibrary.ts:355-359).\n     *   최종 순서: `[info.localUri, info.uri, asset.uri, ...options.extraCandidates]`\n     *   이 자동 후보가 §7.1 「정보 조회 실패 시 폴백 후보 생존」 규칙을 실제로 발화시키는 값이다 —\n     *   전신 주석 \"the picker keeps the original asset.uri\"(286-288)가 가리키는 정확한 경로다.\n     *   저장된 `asset.uri`는 업로드 파이프라인이 읽을 수 없는 `ph://` 참조일 수 있으므로 원본\n     *   바이트와 EXIF를 다시 해석한 뒤 피커 형태로 넘긴다(전신 341-343 주석).\n     */\n    resolvePickedAsset(asset: DeviceAsset, options?: DeviceResolveOptions | undefined): Promise<ResolvedPickedAsset>;\n}"
        },
        {
          "name": "DeviceLibraryAdapter",
          "slug": "device-library-adapter",
          "kind": "interface",
          "declaration": "interface DeviceLibraryAdapter {\n    /**\n     * ⚠ Android 13+에서 granular 목록(`['photo','video']`)을 **반드시** 지정할 것. 생략하면\n     * 매니페스트의 모든 권한이 대상이 되어, 거부된 `READ_MEDIA_AUDIO`가 유효한 사진·동영상 허용을\n     * 거부처럼 보이게 만든다(Android의 \"선택한 사진\" 접근 모드 포함) — §7 하드닝 5.\n     *\n     * ⚠ **순수 위임이다.** \"언제 요청할 것인가\"(현재 권한 조회 → `!granted && canAskAgain`일 때만\n     * 요청)는 어댑터가 아니라 core가 소유한다 — `DeviceLibrary.ensurePermission()`(§5.4-④(c), G17).\n     * 어댑터에 두면 (i) 3자 어댑터마다 규칙이 갈리고, (ii) iOS에서 `canAskAgain=false`인데 재요청해\n     * 아무 일도 일어나지 않는 **UI 데드록**이 재발한다.\n     * 어댑터의 몫은 네이티브 응답을 `MediaPermission`으로 매핑하는 것까지다\n     * (`accessPrivileges === 'limited'` → `limited: true` 포함).\n     */\n    requestPermission(): Promise<MediaPermission>;\n    /** 순수 조회 — 요청하지 않는다. 합성 규칙은 core가 갖는다(위 참조). */\n    getPermission(): Promise<MediaPermission>;\n    /**\n     * ⚠ **정렬 계약: creationTime 내림차순(최신 우선).** 전신 `devicePhotoLibrary.ts:220`\n     *   `sortBy: [[SortBy.creationTime, false]]`.\n     * core는 재정렬하지 않는다 — 페이지 단위 재정렬은 전역 순서를 보장하지 못하면서\n     * (다음 페이지가 이전 페이지보다 최신일 수 있다) `endCursor`는 여전히 어댑터 순서를 따라가\n     * **커서와 표시 순서가 어긋난다**(§5.4-④(d)). 이 계약을 어기면 그리드 순서와 무한스크롤\n     * 커서가 함께 깨지며, **타입도 가드도 그것을 잡지 못한다**.\n     *\n     * ⚠ 자산별 `getAssetInfo` 호출 금지 — 60개 원본을 직렬 해석하면 페이지당 ~20초다.\n     * 그리드는 raw uri(iOS `ph://`)를 그대로 그린다. 네이티브 이미지 로더가 PHImageManager에\n     * 뷰 크기 썸네일을 요청하므로 시스템 사진 그리드와 같은 방식이 된다. 원본 바이트는\n     * 업로드 시점의 resolve에서만 해석한다(§7.1).\n     */\n    listAssets(input: {\n        readonly albumId?: string | null | undefined;\n        readonly after?: string | undefined;\n        readonly pageSize: number;\n        readonly kinds: readonly MediaKind[];\n    }): Promise<DeviceAssetPage>;\n    /**\n     * 원본 목록을 **그대로** 반환한다 — 필터·정렬 금지.\n     * `count > 0` 필터와 count 내림차순 정렬은 core가 수행하므로\n     * (전신 `devicePhotoLibrary.ts:243-250`의 정책을 core로 승격) 어댑터가 중복 수행할 이유가 없다.\n     * 전량을 한 번에 반환하는 in-memory 목록이라 core가 전역 순서를 보장할 수 있다는 점이\n     * `listAssets`와의 차이다(§5.4-④(d)).\n     */\n    listAlbums(): Promise<readonly DeviceAlbum[]>;\n    /**\n     * ⚠ `downloadFromNetwork`는 **필수 인자**다(§6.1-④). 옵셔널로 두면 어댑터 구현자가\n     * 플랫폼 기본값(Expo legacy API는 `true`)을 흘려 iCloud 원본을 무단으로 셀룰러 다운로드한다 —\n     * 전신이 실제로 겪은 사고다.\n     * 타임아웃(15s / 60s)도 core가 건다 — 어댑터는 순수 위임(§7 하드닝 6).\n     */\n    getAssetInfo(assetId: string, input: {\n        readonly downloadFromNetwork: boolean;\n    }): Promise<DeviceAssetInfo>;\n}"
        },
        {
          "name": "DeviceResolveOptions",
          "slug": "device-resolve-options",
          "kind": "type",
          "declaration": "/**\n * 기기 자산 해석 옵션(§5.4-④).\n *\n * ⚠ `downloadFromICloud` 기본 false — 전신 주석(devicePhotoLibrary.ts:48-50) 그대로다:\n * \"사용자가 직접 시작한 수동 업로드만 iCloud 전용 원본 가져오기에 옵트인할 수 있다.\n *  백그라운드 동기화는 예기치 않은 셀룰러 전송을 절대 시작하지 않도록 기본값 false를 유지한다.\"\n */\ntype DeviceResolveOptions = {\n    readonly downloadFromICloud?: boolean | undefined;\n    readonly onICloudDownload?: ((downloading: boolean) => void) | undefined;\n    readonly extraCandidates?: readonly (string | null | undefined)[] | undefined;\n    readonly infoTimeoutMs?: number | undefined;\n    readonly downloadTimeoutMs?: number | undefined;\n};",
          "sourceDocumentation": "기기 자산 해석 옵션(§5.4-④).\n\n⚠ `downloadFromICloud` 기본 false — 전신 주석(devicePhotoLibrary.ts:48-50) 그대로다:\n\"사용자가 직접 시작한 수동 업로드만 iCloud 전용 원본 가져오기에 옵트인할 수 있다.\n 백그라운드 동기화는 예기치 않은 셀룰러 전송을 절대 시작하지 않도록 기본값 false를 유지한다.\""
        },
        {
          "name": "DeviceUploads",
          "slug": "device-uploads",
          "kind": "interface",
          "declaration": "interface DeviceUploads<TAsset, TCollectionId extends string = string> {\n    /**\n     * ⚠ 순차 실행 고정(위 참조). 업로드 성공·실패와 무관하게 finally에서 `staging.cleanup`을 호출한다 —\n     * 누락하면 업로드한 **모든** 사진의 원본 사본이 앱 컨테이너에 영구 축적된다(§7 하드닝 7).\n     */\n    uploadDeviceAssets(assets: readonly DeviceAssetRef[], options?: (DeviceResolveOptions & {\n        readonly collectionId?: TCollectionId | null | undefined;\n        readonly max?: number | undefined;\n    }) | undefined): Promise<readonly UploadResult<TAsset>[]>;\n}"
        },
        {
          "name": "DURABLE_FILE_ERROR_CODES",
          "slug": "durable-file-error-codes",
          "kind": "constant",
          "declaration": "DURABLE_FILE_ERROR_CODES: readonly [\n    \"durable-file-invalid-path\",\n    \"durable-file-root-unavailable\",\n    \"durable-file-source-required\",\n    \"durable-file-source-not-found\",\n    \"durable-file-destination-exists\",\n    \"durable-file-copy-empty\",\n    \"durable-file-copy-size-mismatch\",\n    \"durable-file-copy-failed\",\n    \"durable-file-unsupported-asset\"\n]",
          "sourceDocumentation": "Durable-file failures are deliberately independent from `MediaError`.\n\nA durable attachment store has no user-facing upload policy, and its host\nadapter can receive local URIs that must never reach an error boundary or a\nlog. Every code below is therefore actionable without exposing a source or\ndestination URI."
        },
        {
          "name": "DurableFile",
          "slug": "durable-file",
          "kind": "type",
          "declaration": "type DurableFile = {\n    readonly uri: string;\n    readonly sizeBytes: number;\n};"
        },
        {
          "name": "DurableFileError",
          "slug": "durable-file-error",
          "kind": "class",
          "declaration": "/** A URI-safe error emitted by durable attachment storage. */\ndeclare class DurableFileError extends Error {\n    readonly code: DurableFileErrorCode;\n    constructor(code: DurableFileErrorCode);\n}",
          "sourceDocumentation": "A URI-safe error emitted by durable attachment storage."
        },
        {
          "name": "DurableFileErrorCode",
          "slug": "durable-file-error-code",
          "kind": "type",
          "declaration": "type DurableFileErrorCode = (typeof DURABLE_FILE_ERROR_CODES)[number];"
        },
        {
          "name": "DurableFileStore",
          "slug": "durable-file-store",
          "kind": "interface",
          "declaration": "interface DurableFileStore {\n    /** True only for a safe, non-empty file path rooted in this store. */\n    owns(uri: string | null | undefined): boolean;\n    /** Atomically-enough copy: a failed, empty, or size-mismatched destination is removed before rejection. */\n    copy(input: DurableFileStoreCopyInput): Promise<DurableFile>;\n    /** Copy a picker result while retaining a supported, validated media extension. */\n    copyPickedAsset(input: DurablePickedAssetCopyInput): Promise<DurablePickedAssetFile>;\n    /** Best-effort no-op for paths outside this store. */\n    remove(uri: string | null | undefined): Promise<void>;\n}"
        },
        {
          "name": "DurableFileStoreAdapter",
          "slug": "durable-file-store-adapter",
          "kind": "interface",
          "declaration": "/**\n * App-owned persistent-file storage seam.\n *\n * This is deliberately separate from `FileSystemAdapter`: upload code needs a\n * cache directory plus base64 reads, whereas an application attachment store\n * needs a durable root plus directory creation. Keeping the contracts apart\n * avoids making every upload-only adapter pretend it can persist user files.\n */\ninterface DurableFileStoreAdapter {\n    /** App-owned durable root URI. It must identify a directory, or return null when unavailable. */\n    rootDirectory(): string | null;\n    /** Creates a directory and all missing parents. Repeating the call must be safe. */\n    ensureDirectory(uri: string): Promise<void>;\n    /** Same discriminated stat contract as the upload file-system seam. */\n    stat(uri: string): Promise<FileStat>;\n    copy(input: {\n        readonly from: string;\n        readonly to: string;\n    }): Promise<void>;\n    /** Idempotent best-effort removal. */\n    remove(uri: string): Promise<void>;\n}",
          "sourceDocumentation": "App-owned persistent-file storage seam.\n\nThis is deliberately separate from `FileSystemAdapter`: upload code needs a\ncache directory plus base64 reads, whereas an application attachment store\nneeds a durable root plus directory creation. Keeping the contracts apart\navoids making every upload-only adapter pretend it can persist user files."
        },
        {
          "name": "DurableFileStoreCopyInput",
          "slug": "durable-file-store-copy-input",
          "kind": "type",
          "declaration": "type DurableFileStoreCopyInput = {\n    readonly sourceUri: string;\n    /** Nested folders below the configured root. Every segment is validated independently. */\n    readonly directory?: readonly string[] | undefined;\n    /** The final file name, including any extension the host chose. */\n    readonly fileName: string;\n};"
        },
        {
          "name": "DurablePickedAssetCopyInput",
          "slug": "durable-picked-asset-copy-input",
          "kind": "type",
          "declaration": "/**\n * Copy a picker result with a caller-owned stable filename stem.\n *\n * The source filename is metadata, not a safe destination path. The store\n * validates `fileNameStem` as a path segment and appends an extension from the\n * selected asset's supported content type. This prevents an app from silently\n * relabeling every picked asset as `.jpg` while retaining its own stable ID.\n */\ntype DurablePickedAssetCopyInput = {\n    readonly asset: PickedAsset;\n    /** Nested folders below the configured root. Every segment is validated independently. */\n    readonly directory?: readonly string[] | undefined;\n    /** Safe, caller-owned destination filename stem; the store adds the validated extension. */\n    readonly fileNameStem: string;\n};",
          "sourceDocumentation": "Copy a picker result with a caller-owned stable filename stem.\n\nThe source filename is metadata, not a safe destination path. The store\nvalidates `fileNameStem` as a path segment and appends an extension from the\nselected asset's supported content type. This prevents an app from silently\nrelabeling every picked asset as `.jpg` while retaining its own stable ID."
        },
        {
          "name": "DurablePickedAssetFile",
          "slug": "durable-picked-asset-file",
          "kind": "type",
          "declaration": "type DurablePickedAssetFile = DurableFile & {\n    readonly fileName: string;\n    readonly contentType: MediaContentType;\n};"
        },
        {
          "name": "enMediaStrings",
          "slug": "en-media-strings",
          "kind": "constant",
          "declaration": "enMediaStrings: MediaStrings",
          "sourceDocumentation": "기본값. 팩토리에 `strings`를 주지 않은 소비자가 받는 문구다."
        },
        {
          "name": "ExifCapturedAtOptions",
          "slug": "exif-captured-at-options",
          "kind": "type",
          "declaration": "type ExifCapturedAtOptions = {\n    /**\n     * Offset saved with the event in minutes east of UTC (KST = 540). When this\n     * is omitted, the historical device-local behaviour of\n     * `mediaMetadataFromExif` is preserved.\n     */\n    readonly timeZoneOffsetMinutes?: number | undefined;\n};"
        },
        {
          "name": "ExifWallClock",
          "slug": "exif-wall-clock",
          "kind": "type",
          "declaration": "/**\n * A timezone-less EXIF wall clock. EXIF itself carries no offset, so this is\n * deliberately not a `Date`: callers must choose the policy for mapping it to\n * an instant.\n */\ntype ExifWallClock = {\n    readonly year: number;\n    readonly month: number;\n    readonly day: number;\n    readonly hour: number;\n    readonly minute: number;\n    readonly second: number;\n    readonly millisecond: number;\n};",
          "sourceDocumentation": "A timezone-less EXIF wall clock. EXIF itself carries no offset, so this is\ndeliberately not a `Date`: callers must choose the policy for mapping it to\nan instant."
        },
        {
          "name": "expoPlatform",
          "slug": "expo-platform",
          "kind": "function",
          "declaration": "declare function expoPlatform(): PlatformAdapter;"
        },
        {
          "name": "extensionForContentType",
          "slug": "extension-for-content-type",
          "kind": "function",
          "declaration": "/** 전신 `fileExtensionForContentType`. 정규 확장자를 **점 없이** 반환한다. */\ndeclare function extensionForContentType(contentType: MediaContentType): string;",
          "sourceDocumentation": "전신 `fileExtensionForContentType`. 정규 확장자를 **점 없이** 반환한다."
        },
        {
          "name": "FileDownloadAdapter",
          "slug": "file-download-adapter",
          "kind": "interface",
          "declaration": "/** 저장 플로우 전용 — 업로드만 하는 소비자는 구현할 필요가 없다. */\ninterface FileDownloadAdapter {\n    download(input: {\n        readonly url: string;\n        readonly to: string;\n    }): Promise<{\n        readonly uri: string;\n        readonly status: number;\n    }>;\n}",
          "sourceDocumentation": "저장 플로우 전용 — 업로드만 하는 소비자는 구현할 필요가 없다."
        },
        {
          "name": "FileStat",
          "slug": "file-stat",
          "kind": "type",
          "declaration": "/**\n * 전신의 `{exists, isDirectory, size}`를 판별 유니언으로 교체.\n * `info.exists && !info.isDirectory ? info.size : 0` 패턴 5중복이 좁히기 한 번으로 소멸한다\n * (§7 하드닝 3).\n */\ntype FileStat = {\n    readonly kind: 'file';\n    readonly sizeBytes: number;\n} | {\n    readonly kind: 'directory';\n} | {\n    readonly kind: 'missing';\n};",
          "sourceDocumentation": "전신의 `{exists, isDirectory, size}`를 판별 유니언으로 교체.\n`info.exists && !info.isDirectory ? info.size : 0` 패턴 5중복이 좁히기 한 번으로 소멸한다\n(§7 하드닝 3)."
        },
        {
          "name": "FileSystemAdapter",
          "slug": "file-system-adapter",
          "kind": "interface",
          "declaration": "interface FileSystemAdapter {\n    /** 앱 소유 캐시 디렉토리 URI(끝에 '/'). 없으면 documentDirectory, 그것도 없으면 null. */\n    cacheDirectory(): string | null;\n    /** ⚠ throw 금지 — 코어가 후보 URI를 순회한다. 없거나 디렉토리면 그 kind를 반환. */\n    stat(uri: string): Promise<FileStat>;\n    copy(input: {\n        readonly from: string;\n        readonly to: string;\n    }): Promise<void>;\n    /** 멱등 삭제. 실패해도 throw 금지 — 스테이징 누수는 디스크 비용일 뿐이다. */\n    remove(uri: string): Promise<void>;\n    /**\n     * `[position, position+length)` 구간을 base64로 반환.\n     * ⚠ 코어는 length를 **항상 3의 배수**로 준다(§7 하드닝 9 — 3바이트가 base64 4문자에 대응하므로\n     * 3의 배수가 아니면 윈도우 경계에 패딩이 끼어 **해시가 조용히 틀린다**).\n     * 어댑터가 범위를 재정렬·병합·확장하면 그 하드닝이 무력화된다.\n     */\n    readBase64(uri: string, range: ChunkRange): Promise<string>;\n}"
        },
        {
          "name": "GeoPoint",
          "slug": "geo-point",
          "kind": "type",
          "declaration": "/** 전신 `PhotoGeoPoint`. */\ntype GeoPoint = {\n    readonly latitude: number;\n    readonly longitude: number;\n};",
          "sourceDocumentation": "전신 `PhotoGeoPoint`."
        },
        {
          "name": "HASH_CHUNK_BYTES",
          "slug": "hash-chunk-bytes",
          "kind": "constant",
          "declaration": "HASH_CHUNK_BYTES: number",
          "sourceDocumentation": "읽기 창 크기(바이트).\n\n⚠ **반드시 3의 배수여야 한다**(§7 하드닝 9). 3바이트가 base64 4문자에 대응하므로, 3의 배수가\n아니면 창 경계에 패딩(`=`)이 끼어 디코딩된 바이트열이 원본과 어긋난다 — **해시가 조용히 틀린다**.\n서버와 클라이언트의 dedup 키가 달라질 뿐 어떤 예외도 나지 않으므로 붙잡을 방법이 없다.\n`hardening-guard`가 `HASH_CHUNK_BYTES % 3 === 0`을 정적으로 못 박는다."
        },
        {
          "name": "HashAdapter",
          "slug": "hash-adapter",
          "kind": "interface",
          "declaration": "/**\n * 기본 구현은 core 내장 순수 TS 증분 SHA-256(§9) — `js-sha256` 제거, 런타임 의존성 0.\n * 네이티브 가속이 필요한 호스트만 교체한다.\n *\n * ⚠ 해시는 dedup **최적화**일 뿐이므로 실패가 업로드를 막지 않는다(§7.1). 그 정책은 코어의\n * `hashSafely()`가 갖는다 — 어댑터는 그냥 throw해도 된다.\n */\ninterface HashAdapter {\n    hashLocalFile(uri: string): Promise<string>;\n    hashBinary(source: BinarySource): Promise<string>;\n}",
          "sourceDocumentation": "기본 구현은 core 내장 순수 TS 증분 SHA-256(§9) — `js-sha256` 제거, 런타임 의존성 0.\n네이티브 가속이 필요한 호스트만 교체한다.\n\n⚠ 해시는 dedup **최적화**일 뿐이므로 실패가 업로드를 막지 않는다(§7.1). 그 정책은 코어의\n`hashSafely()`가 갖는다 — 어댑터는 그냥 throw해도 된다."
        },
        {
          "name": "ImageContentType",
          "slug": "image-content-type",
          "kind": "type",
          "declaration": "type ImageContentType = Extract<MediaContentType, `image/${string}`>;"
        },
        {
          "name": "inferImageContentType",
          "slug": "infer-image-content-type",
          "kind": "function",
          "declaration": "/**\n * 전신 `inferContentType`. 폴백은 `image/jpeg`.\n *\n * ⚠ **반환값에 분기가 걸린다** — 호스트의 HEIC/HEIF 프리뷰 분기가 이 값을 읽는다\n * (`pendingPhotos.ts:56,131,143`). 폴백을 바꾸면 프리뷰 경로가 조용히 달라진다.\n */\ndeclare function inferImageContentType(mime?: string | null, nameOrUri?: string | null): ImageContentType;",
          "sourceDocumentation": "전신 `inferContentType`. 폴백은 `image/jpeg`.\n\n⚠ **반환값에 분기가 걸린다** — 호스트의 HEIC/HEIF 프리뷰 분기가 이 값을 읽는다\n(`pendingPhotos.ts:56,131,143`). 폴백을 바꾸면 프리뷰 경로가 조용히 달라진다."
        },
        {
          "name": "inferMediaContentType",
          "slug": "infer-media-content-type",
          "kind": "function",
          "declaration": "/** 관대 추론 — 항상 값을 준다. mime이 미디어를 지목하면 그것을, 아니면 이미지 폴백. */\ndeclare function inferMediaContentType(mime?: string | null, nameOrUri?: string | null): MediaContentType;",
          "sourceDocumentation": "관대 추론 — 항상 값을 준다. mime이 미디어를 지목하면 그것을, 아니면 이미지 폴백."
        },
        {
          "name": "isDurableFileError",
          "slug": "is-durable-file-error",
          "kind": "function",
          "declaration": "/** Cross-entry-safe `DurableFileError` guard. */\ndeclare function isDurableFileError(error: unknown): error is DurableFileError;",
          "sourceDocumentation": "Cross-entry-safe `DurableFileError` guard."
        },
        {
          "name": "isMediaError",
          "slug": "is-media-error",
          "kind": "function",
          "declaration": "/**\n * `instanceof` 대신 이것을 쓴다(§5.2). 엔트리마다 복제된 코어 사본이 만든 에러도 인식한다.\n */\ndeclare function isMediaError(error: unknown): error is MediaError;",
          "sourceDocumentation": "`instanceof` 대신 이것을 쓴다(§5.2). 엔트리마다 복제된 코어 사본이 만든 에러도 인식한다."
        },
        {
          "name": "isPhotoKitUri",
          "slug": "is-photo-kit-uri",
          "kind": "function",
          "declaration": "/** iOS 사진 보관함 자산 URI인가. 하드닝 2의 `ph://` 후보 스킵 술어가 이것을 쓴다. */\ndeclare function isPhotoKitUri(uri?: string | null): boolean;",
          "sourceDocumentation": "iOS 사진 보관함 자산 URI인가. 하드닝 2의 `ph://` 후보 스킵 술어가 이것을 쓴다."
        },
        {
          "name": "isSupportedImageFile",
          "slug": "is-supported-image-file",
          "kind": "function",
          "declaration": "declare function isSupportedImageFile(file: NamedFileLike): boolean;"
        },
        {
          "name": "isSupportedMediaFile",
          "slug": "is-supported-media-file",
          "kind": "function",
          "declaration": "declare function isSupportedMediaFile(file: NamedFileLike): boolean;"
        },
        {
          "name": "isSupportedVideoFile",
          "slug": "is-supported-video-file",
          "kind": "function",
          "declaration": "declare function isSupportedVideoFile(file: NamedFileLike): boolean;"
        },
        {
          "name": "koMediaStrings",
          "slug": "ko-media-strings",
          "kind": "constant",
          "declaration": "koMediaStrings: MediaStrings",
          "sourceDocumentation": "전신 문구를 **원문 그대로** 이식 — memorylog2 이관 시 UI 회귀 0(§11).\n한 글자라도 바꾸면 사용자가 보던 문구가 바뀐다. 개선은 호스트가 스프레드로 한다."
        },
        {
          "name": "LocalFileTransport",
          "slug": "local-file-transport",
          "kind": "interface",
          "declaration": "/**\n * ⚠ **계약: 파일 바이트를 JS 힙으로 읽지 말 것.** 네이티브 스트리밍 업로드여야 한다.\n *\n * 근거(§7 하드닝 1): `FileSystem.uploadAsync`(레거시 URLSession 브리지)가 iOS 26에서 파일 기반\n * 업로드를 **시작하는 중 프로세스를 종료시킨다** — promise가 reject될 기회조차 없으므로\n * 재시도 로직도 에러 보고도 발화하지 않는다. 크래시 리포트에도 앱 코드 프레임이 남지 않아\n * 원인 추적에만 수일이 걸렸다.\n *\n * expo 기본 어댑터는 `new File(uri).upload(url, { sessionType: 'foreground',\n * uploadType: BINARY_CONTENT })`를 쓴다. `hardening-guard`가 `uploadAsync` 문자열의\n * 재등장을 `src/**` 전역에서 정적 차단한다.\n */\ninterface LocalFileTransport {\n    putLocalFile(input: PutRequest & {\n        readonly uri: string;\n    }): Promise<{\n        readonly status: number;\n    }>;\n}",
          "sourceDocumentation": "⚠ **계약: 파일 바이트를 JS 힙으로 읽지 말 것.** 네이티브 스트리밍 업로드여야 한다.\n\n근거(§7 하드닝 1): `FileSystem.uploadAsync`(레거시 URLSession 브리지)가 iOS 26에서 파일 기반\n업로드를 **시작하는 중 프로세스를 종료시킨다** — promise가 reject될 기회조차 없으므로\n재시도 로직도 에러 보고도 발화하지 않는다. 크래시 리포트에도 앱 코드 프레임이 남지 않아\n원인 추적에만 수일이 걸렸다.\n\nexpo 기본 어댑터는 `new File(uri).upload(url, { sessionType: 'foreground',\nuploadType: BINARY_CONTENT })`를 쓴다. `hardening-guard`가 `uploadAsync` 문자열의\n재등장을 `src/**` 전역에서 정적 차단한다."
        },
        {
          "name": "LocalMediaSaveFailureCode",
          "slug": "local-media-save-failure-code",
          "kind": "type",
          "declaration": "/**\n * The only per-item failures this local saver exposes. These are batch\n * outcomes, rather than thrown `MediaError`s: one bad file must not stop later\n * files from being saved. Adapter errors and URIs never escape.\n */\ntype LocalMediaSaveFailureCode = 'save-local-file-unavailable' | 'save-local-file-failed';",
          "sourceDocumentation": "The only per-item failures this local saver exposes. These are batch\noutcomes, rather than thrown `MediaError`s: one bad file must not stop later\nfiles from being saved. Adapter errors and URIs never escape."
        },
        {
          "name": "LocalMediaSaveFileAdapter",
          "slug": "local-media-save-file-adapter",
          "kind": "interface",
          "declaration": "/**\n * The local save path only needs an existence/size check. Requiring the full\n * upload `FileSystemAdapter` would make a save-only consumer implement unused\n * cache, copy, base64-read, and download capabilities.\n */\ninterface LocalMediaSaveFileAdapter {\n    stat(uri: string): Promise<FileStat>;\n}",
          "sourceDocumentation": "The local save path only needs an existence/size check. Requiring the full\nupload `FileSystemAdapter` would make a save-only consumer implement unused\ncache, copy, base64-read, and download capabilities."
        },
        {
          "name": "LocalMediaSaveItemResult",
          "slug": "local-media-save-item-result",
          "kind": "type",
          "declaration": "type LocalMediaSaveItemResult = {\n    readonly index: number;\n    readonly id?: string | undefined;\n    readonly status: 'saved';\n} | {\n    readonly index: number;\n    readonly id?: string | undefined;\n    readonly status: 'unavailable';\n    readonly errorCode: 'save-local-file-unavailable';\n} | {\n    readonly index: number;\n    readonly id?: string | undefined;\n    readonly status: 'failed';\n    readonly errorCode: 'save-local-file-failed';\n};"
        },
        {
          "name": "LocalMediaSaver",
          "slug": "local-media-saver",
          "kind": "interface",
          "declaration": "interface LocalMediaSaver {\n    /**\n     * Saves app-owned local files in input order. A missing/empty source and an\n     * OS save failure are reported per item so one bad item never prevents later\n     * files from being attempted. A write-permission failure rejects once with a\n     * user-safe `MediaError('save-permission-denied')` before any save attempt.\n     */\n    saveLocalToDevice(media: readonly LocalSaveableMedia[]): Promise<LocalMediaSaveResult>;\n}"
        },
        {
          "name": "LocalMediaSaveResult",
          "slug": "local-media-save-result",
          "kind": "type",
          "declaration": "type LocalMediaSaveResult = {\n    /** Input order is preserved, including unavailable sources. */\n    readonly items: readonly LocalMediaSaveItemResult[];\n    readonly savedCount: number;\n    readonly unavailableCount: number;\n    readonly failedCount: number;\n};"
        },
        {
          "name": "LocalPosterAdapter",
          "slug": "local-poster-adapter",
          "kind": "interface",
          "declaration": "/**\n * 로컬 URI → 포스터 프레임. 기본 구현은 `\"./video\"`(expo-video-thumbnails).\n * `BinaryPosterAdapter`와 입력 타입이 다르므로 자리를 바꿔 끼우면 컴파일 에러가 난다 —\n * 브랜드·phantom 각인 없이 오배치가 차단된다.\n */\ninterface LocalPosterAdapter {\n    /** ⚠ 실패는 `null`. 포스터 실패가 동영상 업로드를 막지 않는다(§7.1). */\n    posterFromLocalFile(input: {\n        readonly uri: string;\n        readonly atMs: number;\n    }): Promise<{\n        readonly uri: string;\n    } | null>;\n}",
          "sourceDocumentation": "로컬 URI → 포스터 프레임. 기본 구현은 `\"./video\"`(expo-video-thumbnails).\n`BinaryPosterAdapter`와 입력 타입이 다르므로 자리를 바꿔 끼우면 컴파일 에러가 난다 —\n브랜드·phantom 각인 없이 오배치가 차단된다."
        },
        {
          "name": "LocalSaveableMedia",
          "slug": "local-saveable-media",
          "kind": "type",
          "declaration": "/** A host-owned local artifact. `id` is only returned for caller-side correlation. */\ntype LocalSaveableMedia = {\n    readonly uri: string;\n    readonly id?: string | undefined;\n};",
          "sourceDocumentation": "A host-owned local artifact. `id` is only returned for caller-side correlation."
        },
        {
          "name": "LocalUploadInput",
          "slug": "local-upload-input",
          "kind": "type",
          "declaration": "type LocalUploadInput<TCollectionId extends string = string> = {\n    readonly uri: string;\n    readonly fileName?: string | undefined;\n    readonly contentType?: MediaContentType | undefined;\n    readonly sizeBytes?: number | undefined;\n    /**\n     * ⚠ **주어지면 hasher를 호출하지 않는다**(§7.1 신설 행). 전신 `uploadLocalUriToIntent`는 해시를\n     * 계산하지 않고 호출자 값을 그대로 전달했다(uploader.ts:57-69의 필드 정의, 440의 전달).\n     * 동기화 큐가 재시도 간 해시를 캐시하기 때문이다:\n     *   `src/sync/uploadAsset.ts:45-46` — \"Reuse the cached hash across retries; only compute\n     *   on first attempt\" / `item.contentHash ?? (await hash(source.uri))`.\n     * 이 필드가 없으면 재시도마다 15MB 파일을 다시 해시한다(순수 TS SHA-256 위에서는 §12-3의\n     * Hermes 성능 리스크와 곱해진다).\n     *\n     * ⚠ 바로 아래 `hashSafely`의 \"해시 실패는 업로드를 막지 않는다\"와 **나란히** 읽어야 한다 —\n     *    한쪽만 보면 정반대 구현이 나온다(§7.1의 경고 그대로).\n     */\n    readonly contentHash?: string | undefined;\n    readonly collectionId?: TCollectionId | null | undefined;\n    readonly photo?: MediaMetadata | undefined;\n    readonly durationMs?: number | undefined;\n    readonly width?: number | undefined;\n    readonly height?: number | undefined;\n};"
        },
        {
          "name": "LocalUploads",
          "slug": "local-uploads",
          "kind": "interface",
          "declaration": "interface LocalUploads<TAsset, TCollectionId extends string = string> {\n    uploadLocalFile(input: LocalUploadInput<TCollectionId>): Promise<UploadResult<TAsset>>;\n    /**\n     * 피커 자산 1건 — `PickerFlows`·`DeviceUploads`가 위임한다.\n     * ⚠ **네이티브 전용이다.** `platform.os === 'web'`이면 `MediaError('platform-unsupported')`\n     * — 로컬 파일 스트리밍은 웹에 존재하지 않는다. 웹 피커 자산의 정본 경로는 §5.7.4다.\n     */\n    uploadPickedAsset(asset: PickedAsset, options?: {\n        readonly collectionId?: TCollectionId | null | undefined;\n    } | undefined): Promise<UploadResult<TAsset>>;\n}"
        },
        {
          "name": "MEDIA_CONTENT_TYPES",
          "slug": "media-content-types",
          "kind": "constant",
          "declaration": "MEDIA_CONTENT_TYPES: readonly MediaContentType[]",
          "sourceDocumentation": "지원 콘텐츠 타입 전량.\n\n§5.7.2-③이 내부화한 `IMAGE_CONTENT_TYPES`/`VIDEO_CONTENT_TYPES`의 **동치 공개 대체**가\n`MEDIA_CONTENT_TYPES.filter((t) => mediaKindOf(t) === 'image')`이므로, 그 판정이 참이 되려면\n이 배열이 공개돼 있어야 한다."
        },
        {
          "name": "MEDIA_ERROR_CODES",
          "slug": "media-error-codes",
          "kind": "constant",
          "declaration": "MEDIA_ERROR_CODES: readonly [\n    \"device-timeout\",\n    \"device-icloud-only\",\n    \"device-not-found\",\n    \"device-library-failed\",\n    \"picker-failed\",\n    \"image-processing-failed\",\n    \"unsupported-file-type\",\n    \"file-too-large\",\n    \"upload-failed\",\n    \"save-permission-denied\",\n    \"save-download-failed\",\n    \"permission-denied\",\n    \"poster-upload-failed\",\n    \"no-media-selected\",\n    \"picked-asset-invalid\",\n    \"config-invalid\",\n    \"platform-unsupported\"\n]",
          "sourceDocumentation": "⚠ **순서·문자열이 계약이다.** 소비자가 `Set<MediaErrorCode>`(예: memorylog2의\n`ACTIONABLE_ERROR_CODES`)를 리터럴로 만들어 두고 분기하므로 rename = 파괴적 변경이다.\n유니언을 넓히는 것(추가)은 비파괴이므로 minor로 가능하다."
        },
        {
          "name": "MEDIA_FILE_EXTENSIONS",
          "slug": "media-file-extensions",
          "kind": "constant",
          "declaration": "MEDIA_FILE_EXTENSIONS: Readonly<Record<MediaContentType, readonly string[]>>",
          "sourceDocumentation": "확장자↔MIME 단일 테이블.\n\n공개하는 이유(§5.7.2-③): 호스트가 \"지원 형식\" 안내 문구나 파일 입력의 `accept` 속성을 그리려면\n이 테이블이 필요하다. 숨기면 3자 소비자가 재구현하고, 재구현본은 라이브러리가 형식을 추가할 때\n조용히 어긋난다.\n\n⚠ 각 값의 **첫 원소가 정규 확장자**다(`extensionForContentType`). 순서를 바꾸면 저장 파일명과\n스테이징 사본 이름이 함께 바뀐다."
        },
        {
          "name": "MEDIA_OPERATIONS",
          "slug": "media-operations",
          "kind": "constant",
          "declaration": "MEDIA_OPERATIONS: readonly [\n    \"media.upload.native\",\n    \"media.upload.web-image\",\n    \"media.upload.web-video\",\n    \"media.upload.poster.native\",\n    \"media.upload.poster.web\",\n    \"media.save-to-device\"\n]",
          "sourceDocumentation": "안정적 dotted operation 이름.\n\n⚠ **값 변경 = 소비자 대시보드·알림 규칙 파손**이므로 하드닝과 동급으로 보존한다(§7.2).\n§7.2의 unit 1번이 이 배열을 **인라인 리터럴로 단언**한다 — 스냅샷은 `-u`로 조용히 갱신되므로\n스냅샷을 쓰지 않는다."
        },
        {
          "name": "MediaActivity",
          "slug": "media-activity",
          "kind": "interface",
          "declaration": "/**\n * 하나의 스팬. 정확히 한 번만 종료되어야 한다(라이브러리 내부 규율 — 소비자 검증 대상 아님).\n *\n * `cancel`은 \"실패가 아닌 중단\"이다: 빈 포스터(전신 uploader.ts:268-271)처럼 사용자에게\n * 오류로 보고하면 안 되지만 성공으로 세어서도 안 되는 **3번째 종료 상태**다.\n * 이 상태가 없으면 포스터 추출이 빈 결과를 낸 경우가 성공률 지표에 섞여 들어간다.\n */\ninterface MediaActivity {\n    succeed(finish?: MediaActivityFinish | undefined): void;\n    fail(error: unknown, finish?: MediaActivityFinish | undefined): void;\n    cancel(finish?: MediaActivityFinish | undefined): void;\n}",
          "sourceDocumentation": "하나의 스팬. 정확히 한 번만 종료되어야 한다(라이브러리 내부 규율 — 소비자 검증 대상 아님).\n\n`cancel`은 \"실패가 아닌 중단\"이다: 빈 포스터(전신 uploader.ts:268-271)처럼 사용자에게\n오류로 보고하면 안 되지만 성공으로 세어서도 안 되는 **3번째 종료 상태**다.\n이 상태가 없으면 포스터 추출이 빈 결과를 낸 경우가 성공률 지표에 섞여 들어간다."
        },
        {
          "name": "MediaActivityFinish",
          "slug": "media-activity-finish",
          "kind": "type",
          "declaration": "type MediaActivityFinish = {\n    readonly extra?: Readonly<Record<string, unknown>> | undefined;\n};"
        },
        {
          "name": "MediaContentType",
          "slug": "media-content-type",
          "kind": "type",
          "declaration": "/**\n * ⚠ **8종 고정 — 전신 `MEDIA_FILE_EXTENSIONS`(mediaTypes.ts:7-16)와 정확히 동일하다.**\n * 초안이 신설했던 `image/gif`는 **제거**했다(G15 확정, §5.1):\n *   ① 전신 확장자 테이블에 gif 항목이 없다 → §7의 \"전신 168줄 그대로\" 문구가 참이 된다.\n *   ② 호스트(memorylog2)의 `SUPPORTED_MEDIA_CONTENT_TYPES`에도 없고 서버 zod가 그 목록으로\n *      정규식 검증을 한다 → gif를 통과시키면 **presign 단계에서 서버가 거절**하는\n *      클라이언트/서버 불일치가 생긴다. 클라이언트가 서버보다 넓은 유니언을 갖는 것은 순손실이다.\n * 형식을 넓히려면 서버 유니언을 먼저 넓히고 minor로 추가한다(유니언 확장은 소비자에게 비파괴).\n */\ntype MediaContentType = 'image/jpeg' | 'image/png' | 'image/webp' | 'image/heic' | 'image/heif' | 'video/mp4' | 'video/quicktime' | 'video/webm';",
          "sourceDocumentation": "⚠ **8종 고정 — 전신 `MEDIA_FILE_EXTENSIONS`(mediaTypes.ts:7-16)와 정확히 동일하다.**\n초안이 신설했던 `image/gif`는 **제거**했다(G15 확정, §5.1):\n  ① 전신 확장자 테이블에 gif 항목이 없다 → §7의 \"전신 168줄 그대로\" 문구가 참이 된다.\n  ② 호스트(memorylog2)의 `SUPPORTED_MEDIA_CONTENT_TYPES`에도 없고 서버 zod가 그 목록으로\n     정규식 검증을 한다 → gif를 통과시키면 **presign 단계에서 서버가 거절**하는\n     클라이언트/서버 불일치가 생긴다. 클라이언트가 서버보다 넓은 유니언을 갖는 것은 순손실이다.\n형식을 넓히려면 서버 유니언을 먼저 넓히고 minor로 추가한다(유니언 확장은 소비자에게 비파괴)."
        },
        {
          "name": "MediaDebugLogger",
          "slug": "media-debug-logger",
          "kind": "interface",
          "declaration": "/**\n * 전신 `PhotoDebugLogger`(debug.ts:39-47)를 그대로 계승한다. 초안은 반환 타입만 있고 멤버가 없어\n * 구현자가 임의로 정할 수 있었다 — 그러면 하드닝 8의 새니타이즈 지점이 구현마다 달라진다(G14).\n */\ninterface MediaDebugLogger {\n    log(event: string, details?: Readonly<Record<string, unknown>> | undefined): void;\n    /** `errorName` + **새니타이즈된** `errorMessage`를 details에 병합해 기록한다(전신 동작 보존). */\n    error(event: string, error: unknown, details?: Readonly<Record<string, unknown>> | undefined): void;\n}",
          "sourceDocumentation": "전신 `PhotoDebugLogger`(debug.ts:39-47)를 그대로 계승한다. 초안은 반환 타입만 있고 멤버가 없어\n구현자가 임의로 정할 수 있었다 — 그러면 하드닝 8의 새니타이즈 지점이 구현마다 달라진다(G14)."
        },
        {
          "name": "MediaDebugOptions",
          "slug": "media-debug-options",
          "kind": "type",
          "declaration": "/**\n * 디버그 로거 설정. 전신 `PhotoUploaderConfig`의 `debugTag`/`debugContext`에 1:1 대응한다.\n * ⚠ core에는 `__DEV__`가 없으므로 기본값은 **비활성**이다. 실제 게이트는\n * `createMediaDebugLogger`가 `platform.isDev && platform.os !== 'web'`로 건다(§7 하드닝 8).\n */\ntype MediaDebugOptions = {\n    readonly enabled: boolean;\n    readonly tag?: string | undefined;\n    readonly context?: (() => Record<string, unknown>) | undefined;\n};",
          "sourceDocumentation": "디버그 로거 설정. 전신 `PhotoUploaderConfig`의 `debugTag`/`debugContext`에 1:1 대응한다.\n⚠ core에는 `__DEV__`가 없으므로 기본값은 **비활성**이다. 실제 게이트는\n`createMediaDebugLogger`가 `platform.isDev && platform.os !== 'web'`로 건다(§7 하드닝 8)."
        },
        {
          "name": "mediaDownloadFileName",
          "slug": "media-download-file-name",
          "kind": "function",
          "declaration": "/**\n * 저장 파일명 = `` `${prefix}-${id || index + 1}.${ext}` `` (전신 saveImages.ts:71).\n *\n * 확장자 우선순위 — **저장된 fileName → contentType → URL 경로 → `'jpg'`**.\n * 전신 주석(saveImages.ts:56-58) 그대로의 근거: \"저장된 원본 이름을 우선한다 — 다운로드 URL은\n * 흔히 경로에 확장자가 없는 토큰화된 프록시라, URL을 스니핑하면 모든 PNG/HEIC/동영상\n * 다운로드가 `<prefix>-<id>.jpg`가 되어버린다.\"\n *\n * ⚠ `index`는 **0-base 필수 인자**다(G8). `id`가 없거나 빈 문자열일 때 `index + 1`이\n * 유일한 구분자이며, 이것이 없으면 여러 장을 저장할 때 파일명이 전부 같아진다 —\n * 브라우저는 `(1)` 접미사로 얼버무리고, MediaLibrary 경로는 캐시 파일을 덮어쓴다.\n */\ndeclare function mediaDownloadFileName(input: {\n    readonly url: string;\n    readonly index: number;\n    readonly id?: string | undefined;\n    readonly fileName?: string | undefined;\n    readonly contentType?: MediaContentType | undefined;\n    readonly prefix?: string | undefined;\n}): string;",
          "sourceDocumentation": "저장 파일명 = `` `${prefix}-${id || index + 1}.${ext}` `` (전신 saveImages.ts:71).\n\n확장자 우선순위 — **저장된 fileName → contentType → URL 경로 → `'jpg'`**.\n전신 주석(saveImages.ts:56-58) 그대로의 근거: \"저장된 원본 이름을 우선한다 — 다운로드 URL은\n흔히 경로에 확장자가 없는 토큰화된 프록시라, URL을 스니핑하면 모든 PNG/HEIC/동영상\n다운로드가 `<prefix>-<id>.jpg`가 되어버린다.\"\n\n⚠ `index`는 **0-base 필수 인자**다(G8). `id`가 없거나 빈 문자열일 때 `index + 1`이\n유일한 구분자이며, 이것이 없으면 여러 장을 저장할 때 파일명이 전부 같아진다 —\n브라우저는 `(1)` 접미사로 얼버무리고, MediaLibrary 경로는 캐시 파일을 덮어쓴다."
        },
        {
          "name": "MediaError",
          "slug": "media-error",
          "kind": "class",
          "declaration": "declare class MediaError extends Error {\n    readonly code: MediaErrorCode;\n    constructor(code: MediaErrorCode, message: string);\n}"
        },
        {
          "name": "mediaErrorCode",
          "slug": "media-error-code",
          "kind": "function",
          "declaration": "/** 전신 `photoErrorCode`. 소비자는 이 값으로만 분기한다 — 문구 매칭 금지. */\ndeclare function mediaErrorCode(error: unknown): MediaErrorCode | null;",
          "sourceDocumentation": "전신 `photoErrorCode`. 소비자는 이 값으로만 분기한다 — 문구 매칭 금지."
        },
        {
          "name": "MediaErrorCode",
          "slug": "media-error-code--type",
          "kind": "type",
          "declaration": "type MediaErrorCode = (typeof MEDIA_ERROR_CODES)[number];"
        },
        {
          "name": "mediaErrorUserMessage",
          "slug": "media-error-user-message",
          "kind": "function",
          "declaration": "/**\n * 전신 `photoErrorUserMessage`.\n * `MediaError`의 message는 이미 사용자 노출 가능 문구다(`MediaStrings` 주입 결과 — §4).\n * 화면은 일반 실패 문구 대신 이 값을 그대로 표시해도 된다.\n */\ndeclare function mediaErrorUserMessage(error: unknown): string | null;",
          "sourceDocumentation": "전신 `photoErrorUserMessage`.\n`MediaError`의 message는 이미 사용자 노출 가능 문구다(`MediaStrings` 주입 결과 — §4).\n화면은 일반 실패 문구 대신 이 값을 그대로 표시해도 된다."
        },
        {
          "name": "mediaFileName",
          "slug": "media-file-name",
          "kind": "function",
          "declaration": "/**\n * 전신의 `defaultMediaFileName`·`inferFileName`·`inferWebFileName` **3종을 하나로 통합**(§5.7.2-③).\n *\n * `fileName`이 있으면 그대로 쓰고, 없으면 `${prefix}-${now}.${ext}`(전신 규칙 보존).\n * ⚠ `now`는 결정론적 테스트를 위한 주입구다 — 생략 시 `Date.now()`로 전신과 동일하게 동작한다\n * (§5.4.1-15). 전신은 `Date.now()`를 직접 호출해 파일명 규칙에 테스트가 없었다.\n * ⚠ `prefix` 기본값은 `'media'`다 — 전신 `'photo'`에서 바꿨다(§5.4.1-13). 호스트는 항상 자기\n * 프리픽스를 주입하므로 이관 영향이 0이며, 라이브러리 이름과의 일관성을 택했다.\n */\ndeclare function mediaFileName(input: {\n    readonly fileName?: string | null | undefined;\n    readonly contentType: MediaContentType;\n    readonly prefix?: string | undefined;\n    readonly now?: number | undefined;\n}): string;",
          "sourceDocumentation": "전신의 `defaultMediaFileName`·`inferFileName`·`inferWebFileName` **3종을 하나로 통합**(§5.7.2-③).\n\n`fileName`이 있으면 그대로 쓰고, 없으면 `${prefix}-${now}.${ext}`(전신 규칙 보존).\n⚠ `now`는 결정론적 테스트를 위한 주입구다 — 생략 시 `Date.now()`로 전신과 동일하게 동작한다\n(§5.4.1-15). 전신은 `Date.now()`를 직접 호출해 파일명 규칙에 테스트가 없었다.\n⚠ `prefix` 기본값은 `'media'`다 — 전신 `'photo'`에서 바꿨다(§5.4.1-13). 호스트는 항상 자기\n프리픽스를 주입하므로 이관 영향이 0이며, 라이브러리 이름과의 일관성을 택했다."
        },
        {
          "name": "MediaKind",
          "slug": "media-kind",
          "kind": "type",
          "declaration": "type MediaKind = 'image' | 'video';"
        },
        {
          "name": "mediaKindOf",
          "slug": "media-kind-of",
          "kind": "function",
          "declaration": "/**\n * 전신 `isVideoContentType`의 자리를 잇는다(그쪽은 §5.7.2-③에서 내부화 —\n * 동치 대체가 `mediaKindOf(ct) === 'video'`다).\n */\ndeclare function mediaKindOf(contentType: MediaContentType): MediaKind;",
          "sourceDocumentation": "전신 `isVideoContentType`의 자리를 잇는다(그쪽은 §5.7.2-③에서 내부화 —\n동치 대체가 `mediaKindOf(ct) === 'video'`다)."
        },
        {
          "name": "MediaKit",
          "slug": "media-kit",
          "kind": "interface",
          "declaration": "/**\n * 골든패스 킷. expo 기본 어댑터(platform·files·localTransport·binaryTransport·hasher·staging)가\n * 이미 채워져 있다.\n *\n * ⚠ 선택 능력은 **구체 킷을 반환하는 `with*`**로 부착한다(§3.1). `with*`는 자기 자신을 넓히지\n * 않고 새 객체를 반환하므로 조건부 타입이 0이다 — V3가 실측한 \"타입 애노테이션 한 번에 전 기능이\n * 소멸하는\" 붕괴가 **표현 불가능**하다.\n */\ninterface MediaKit<TAsset, TCollectionId extends string = string> extends LocalUploads<TAsset, TCollectionId>, BinaryUploads<TAsset, TCollectionId> {\n    readonly platform: PlatformAdapter;\n    readonly files: FileSystemAdapter;\n    readonly staging: StagingCache;\n    /**\n     * ⚠ **공개 필드다**(V9 요구). memorylog2 `src/sync/hashFile.ts`가 `hashLocalFile`을 재export하고\n     * `src/sync/uploadAsset.ts:14,22`가 그것을 쓴다. 이 필드가 없으면 앱 어댑터가 위임할 대상이\n     * 없어 동기화 엔진이 해시 함수를 잃는다(§5.7.2-⑦).\n     */\n    readonly hasher: HashAdapter;\n    withPicker(picker: PickerAdapter, \n    /**\n     * 웹에서 피커 자산을 업로드하는 앱은 `loader`만 주면 된다 — `uploads`는 킷 자신\n     * (`BinaryUploads`)이 채운다. 생략 시 웹 피커 업로드는 `platform-unsupported`다(§5.7.4).\n     */\n    options?: {\n        readonly web?: {\n            readonly loader: BinarySourceLoader;\n        } | undefined;\n    } | undefined): PickerFlows<TAsset, TCollectionId>;\n    withDeviceLibrary(adapter: DeviceLibraryAdapter): DeviceKit<TAsset, TCollectionId>;\n    withDeviceSave(adapter: MediaLibrarySaveAdapter): MediaSaver;\n}",
          "sourceDocumentation": "골든패스 킷. expo 기본 어댑터(platform·files·localTransport·binaryTransport·hasher·staging)가\n이미 채워져 있다.\n\n⚠ 선택 능력은 **구체 킷을 반환하는 `with*`**로 부착한다(§3.1). `with*`는 자기 자신을 넓히지\n않고 새 객체를 반환하므로 조건부 타입이 0이다 — V3가 실측한 \"타입 애노테이션 한 번에 전 기능이\n소멸하는\" 붕괴가 **표현 불가능**하다."
        },
        {
          "name": "MediaKitConfig",
          "slug": "media-kit-config",
          "kind": "type",
          "declaration": "type MediaKitConfig<TAsset, TCollectionId extends string = string> = Omit<MediaUploadConfig<TAsset, TCollectionId>, 'platform'> & {\n    /**\n     * expo 기본 어댑터 위의 **선택 오버라이드**. 각 필드는 개별 교체이며 부분 객체가 아니다 —\n     * `platform`만 갈아끼우고 나머지는 기본값을 그대로 쓰는 조합이 정상이다.\n     */\n    readonly platform?: PlatformAdapter | undefined;\n    readonly files?: FileSystemAdapter | undefined;\n    readonly localTransport?: LocalFileTransport | undefined;\n    readonly binaryTransport?: BinaryTransport | undefined;\n    readonly hasher?: HashAdapter | undefined;\n    /** 네이티브 경로(로컬 URI → 포스터). 보통 `expoVideoPoster()`(`\"./video\"`). */\n    readonly poster?: LocalPosterAdapter | undefined;\n    /**\n     * 바이너리 경로(Blob → 포스터). 보통 `webCanvasVideoPoster()`(`\"./web\"`).\n     *\n     * ⚠ `poster`와 **입력 타입이 달라 자리를 바꿔 끼우면 컴파일 에러**가 나는 것이 설계다(§6.1-⑥).\n     *   그래서 필드도 둘로 나뉜다 — 하나로 합치면 그 검출력이 사라진다.\n     * ⚠ 주지 않으면 **웹 동영상에 포스터가 붙지 않는다.** 전신 uploader.ts:566-569는\n     *   `posterBlob === undefined`일 때 `createWebVideoPosterBlob(blob)`으로 자동 추출했으므로,\n     *   웹 업로드 경로가 있는 앱이 이 필드를 비우면 그 동작이 조용히 사라진다.\n     *   (호출자가 `uploadBinary({ poster })`로 직접 주는 경로는 이 필드와 무관하게 항상 동작한다.)\n     */\n    readonly binaryPoster?: BinaryPosterAdapter | undefined;\n    readonly posterAtMs?: number | undefined;\n    /** 스테이징 네임스페이스. 기본 `'gj-media'`. `/^[a-z0-9][a-z0-9-]{1,30}$/`(§5.3). */\n    readonly namespace?: string | undefined;\n};"
        },
        {
          "name": "MediaLibrarySaveAdapter",
          "slug": "media-library-save-adapter",
          "kind": "interface",
          "declaration": "interface MediaLibrarySaveAdapter {\n    requestWritePermission(): Promise<MediaPermission>;\n    saveToLibrary(uri: string): Promise<void>;\n    /**\n     * Android Expo Go는 사진 권한 요청 자체가 불가 — 그 판정을 어댑터가 정적으로 노출한다\n     * (전신 saveImages.ts:96). `expo-constants` 의존을 라이브러리에서 완전히 제거하는 지점(§0.2):\n     * `Constants.appOwnership === 'expo'` 판정은 호스트가 하고 값만 넘긴다.\n     */\n    readonly skipPermissionRequest: boolean;\n}"
        },
        {
          "name": "MediaMetadata",
          "slug": "media-metadata",
          "kind": "type",
          "declaration": "/**\n * EXIF에서 유도한 촬영 메타데이터. `capturedAt`은 ISO 타임스탬프이며, EXIF 벽시계는\n * **기기 로컬 타임존**으로 해석한다(§7 하드닝 11 — 근거는 `metadata.ts`에 원문 그대로 있다).\n *\n * ⚠ **필드명은 전신 그대로다.** 초안의 `geoPoint → location` 리네임은 **철회**했다(G5 확정).\n * 이 객체는 `api.completeUpload({ photo })`로 **호스트 백엔드에 그대로 전달**된다\n * (전신 uploader.ts:442). 리네임의 파손은 타입에 잡히지 않는다:\n *   · 호스트 대입 지점 `kit.ts:40-44`가 `completeUpload({...input})`을 zod 파생 타입에 넘긴다.\n *   · 타깃은 `packages/shared/src/index.ts:105-108` `photoMetadataSchema = { capturedAt?, geoPoint? }`.\n *   · `{capturedAt?, location?}` → `{capturedAt?, geoPoint?}` 대입은 공통 프로퍼티 `capturedAt`이\n *     있어 **weak type 검사를 통과**하고, 변수 전달이라 초과 프로퍼티 검사도 걸리지 않는다.\n *   · 런타임에서는 서버 zod가 non-strict라 미지의 `location` 키를 **조용히 스트립**한다.\n * 즉 컴파일도 테스트도 통과하고 위치정보만 사라지는 §6.1의 전형적 \"조용히 깨지는\" 클래스이며,\n * 얻는 것은 명명 취향뿐이었다.\n *\n * ⚠ 초안의 `width`/`height`는 **삭제**했다. 전신 EXIF 파서는 `PixelXDimension`/`PixelYDimension`을\n * 읽지 않으므로(photoMetadata.ts 전문에 `Pixel`·`width`·`height` 0건 — grep 실측) 영구 `undefined`인\n * 죽은 필드였고, `MediaUploadCompletion`에 이미 최상위 `width`/`height`(피커 자산 치수,\n * uploader.ts:749-756)가 있어 의미도 중복이다.\n */\ntype MediaMetadata = {\n    readonly capturedAt?: string | undefined;\n    readonly geoPoint?: GeoPoint | undefined;\n};",
          "sourceDocumentation": "EXIF에서 유도한 촬영 메타데이터. `capturedAt`은 ISO 타임스탬프이며, EXIF 벽시계는\n**기기 로컬 타임존**으로 해석한다(§7 하드닝 11 — 근거는 `metadata.ts`에 원문 그대로 있다).\n\n⚠ **필드명은 전신 그대로다.** 초안의 `geoPoint → location` 리네임은 **철회**했다(G5 확정).\n이 객체는 `api.completeUpload({ photo })`로 **호스트 백엔드에 그대로 전달**된다\n(전신 uploader.ts:442). 리네임의 파손은 타입에 잡히지 않는다:\n  · 호스트 대입 지점 `kit.ts:40-44`가 `completeUpload({...input})`을 zod 파생 타입에 넘긴다.\n  · 타깃은 `packages/shared/src/index.ts:105-108` `photoMetadataSchema = { capturedAt?, geoPoint? }`.\n  · `{capturedAt?, location?}` → `{capturedAt?, geoPoint?}` 대입은 공통 프로퍼티 `capturedAt`이\n    있어 **weak type 검사를 통과**하고, 변수 전달이라 초과 프로퍼티 검사도 걸리지 않는다.\n  · 런타임에서는 서버 zod가 non-strict라 미지의 `location` 키를 **조용히 스트립**한다.\n즉 컴파일도 테스트도 통과하고 위치정보만 사라지는 §6.1의 전형적 \"조용히 깨지는\" 클래스이며,\n얻는 것은 명명 취향뿐이었다.\n\n⚠ 초안의 `width`/`height`는 **삭제**했다. 전신 EXIF 파서는 `PixelXDimension`/`PixelYDimension`을\n읽지 않으므로(photoMetadata.ts 전문에 `Pixel`·`width`·`height` 0건 — grep 실측) 영구 `undefined`인\n죽은 필드였고, `MediaUploadCompletion`에 이미 최상위 `width`/`height`(피커 자산 치수,\nuploader.ts:749-756)가 있어 의미도 중복이다."
        },
        {
          "name": "mediaMetadataFromExif",
          "slug": "media-metadata-from-exif",
          "kind": "function",
          "declaration": "/**\n * 전신 `extractPhotoMetadata`.\n *\n * ⚠ **유효값이 하나도 없으면 `undefined`를 반환한다 — 빈 객체 금지**(photoMetadata.ts:130 규칙).\n * truthy한 빈 객체를 주면 호출자가 \"EXIF가 있었다\"고 믿게 되고, 그 오해는 업로드 완료 페이로드의\n * `photo` 필드까지 그대로 흘러간다.\n */\ndeclare function mediaMetadataFromExif(exif?: ReadonlyExifRecord | null): MediaMetadata | undefined;",
          "sourceDocumentation": "전신 `extractPhotoMetadata`.\n\n⚠ **유효값이 하나도 없으면 `undefined`를 반환한다 — 빈 객체 금지**(photoMetadata.ts:130 규칙).\ntruthy한 빈 객체를 주면 호출자가 \"EXIF가 있었다\"고 믿게 되고, 그 오해는 업로드 완료 페이로드의\n`photo` 필드까지 그대로 흘러간다."
        },
        {
          "name": "mediaMetadataFromJpeg",
          "slug": "media-metadata-from-jpeg",
          "kind": "function",
          "declaration": "/**\n * 전신 `extractPhotoMetadataFromBlob(blob, fallbackExif, contentType)`(photoMetadata.ts:265-290).\n * 초안이 지웠던 인자 2개를 `options`로 **복원**한다(G6 · §7.1). **4규칙이 계약이다**:\n *   ① 비-JPEG 스킵 — `(contentType ?? source.type)`이 'jpeg'/'jpg'를 포함하지 않으면 파싱하지 않고\n *      fallback을 그대로 반환한다.\n *   ② **필드 단위 병합** — `parsed?.X ?? fallback?.X`. 객체 단위 폴백이 아니다.\n *      capturedAt만 파싱되고 geoPoint는 fallback에서 오는 조합이 **정상 결과**다.\n *   ③ 파싱 예외 시 fallback 반환 — throw 금지. 메타데이터 때문에 업로드가 죽어선 안 된다.\n *   ④ ①②③ 이후에도 유효값이 없으면 `undefined`(빈 객체 금지 — `mediaMetadataFromExif`와 동일).\n *\n * ⚠ 인자를 지우면 웹 피커 경로(`fallbackExif: asset.exif`)에서 JPEG 파싱이 실패했을 때 피커가 준\n * EXIF가 통째로 버려져 촬영시각·위치가 유실된다. 그 유실은 컴파일도 테스트도 통과한다.\n */\ndeclare function mediaMetadataFromJpeg(source: BinarySource, options?: {\n    readonly fallbackExif?: ReadonlyExifRecord | null | undefined;\n    readonly contentType?: MediaContentType | string | null | undefined;\n} | undefined): Promise<MediaMetadata | undefined>;",
          "sourceDocumentation": "전신 `extractPhotoMetadataFromBlob(blob, fallbackExif, contentType)`(photoMetadata.ts:265-290).\n초안이 지웠던 인자 2개를 `options`로 **복원**한다(G6 · §7.1). **4규칙이 계약이다**:\n  ① 비-JPEG 스킵 — `(contentType ?? source.type)`이 'jpeg'/'jpg'를 포함하지 않으면 파싱하지 않고\n     fallback을 그대로 반환한다.\n  ② **필드 단위 병합** — `parsed?.X ?? fallback?.X`. 객체 단위 폴백이 아니다.\n     capturedAt만 파싱되고 geoPoint는 fallback에서 오는 조합이 **정상 결과**다.\n  ③ 파싱 예외 시 fallback 반환 — throw 금지. 메타데이터 때문에 업로드가 죽어선 안 된다.\n  ④ ①②③ 이후에도 유효값이 없으면 `undefined`(빈 객체 금지 — `mediaMetadataFromExif`와 동일).\n\n⚠ 인자를 지우면 웹 피커 경로(`fallbackExif: asset.exif`)에서 JPEG 파싱이 실패했을 때 피커가 준\nEXIF가 통째로 버려져 촬영시각·위치가 유실된다. 그 유실은 컴파일도 테스트도 통과한다."
        },
        {
          "name": "MediaOperation",
          "slug": "media-operation",
          "kind": "type",
          "declaration": "/**\n * 라이브러리가 방출하는 operation의 닫힌 목록.\n *\n * 리터럴 유니언으로 좁힌 이유(§6.2 기각표의 반대 항목을 철회한 근거): 이 인터페이스는\n * **라이브러리가 방출하는** operation만 다루고 그 목록은 6종으로 닫혀 있다. 호스트가 자기\n * operation을 보고하는 것은 이 인터페이스의 일이 아니다 — memorylog2도 기기 라이브러리 진단\n * 4종을 앱 리포터로 직접 보낸다(`devicePhotoLibraryTelemetry.ts`). 좁히면 오타가 컴파일 에러가 된다.\n *\n * `MediaOperation`은 문자열 리터럴 유니언이므로 호스트의 `operation: string` 슬롯에\n * 그대로 대입된다(가변성 문제 없음).\n */\ntype MediaOperation = (typeof MEDIA_OPERATIONS)[number];",
          "sourceDocumentation": "라이브러리가 방출하는 operation의 닫힌 목록.\n\n리터럴 유니언으로 좁힌 이유(§6.2 기각표의 반대 항목을 철회한 근거): 이 인터페이스는\n**라이브러리가 방출하는** operation만 다루고 그 목록은 6종으로 닫혀 있다. 호스트가 자기\noperation을 보고하는 것은 이 인터페이스의 일이 아니다 — memorylog2도 기기 라이브러리 진단\n4종을 앱 리포터로 직접 보낸다(`devicePhotoLibraryTelemetry.ts`). 좁히면 오타가 컴파일 에러가 된다.\n\n`MediaOperation`은 문자열 리터럴 유니언이므로 호스트의 `operation: string` 슬롯에\n그대로 대입된다(가변성 문제 없음)."
        },
        {
          "name": "MediaOrphanedUpload",
          "slug": "media-orphaned-upload",
          "kind": "type",
          "declaration": "/**\n * 실패 시 정리 후보인 스토리지 오브젝트.\n *\n * `uploaded`는 2xx 응답까지 확인한 PUT, `possibly-uploaded`는 응답 유실·전송 예외처럼\n * 서버에는 도달했을 수도 있는 PUT이다. 후자도 cleanup endpoint가 멱등으로 처리해야 한다.\n */\ntype MediaOrphanedUpload = MediaUploadObject & {\n    readonly storageState: 'uploaded' | 'possibly-uploaded';\n};",
          "sourceDocumentation": "실패 시 정리 후보인 스토리지 오브젝트.\n\n`uploaded`는 2xx 응답까지 확인한 PUT, `possibly-uploaded`는 응답 유실·전송 예외처럼\n서버에는 도달했을 수도 있는 PUT이다. 후자도 cleanup endpoint가 멱등으로 처리해야 한다."
        },
        {
          "name": "MediaPermission",
          "slug": "media-permission",
          "kind": "type",
          "declaration": "type MediaPermission = {\n    readonly granted: boolean;\n    readonly canAskAgain: boolean;\n    /** iOS \"선택된 사진\" — 일부만 보인다. */\n    readonly limited: boolean;\n};"
        },
        {
          "name": "MediaPickerActions",
          "slug": "media-picker-actions",
          "kind": "interface",
          "declaration": "interface MediaPickerActions {\n    /** Request library permission and return at most `max` normalized assets. */\n    pick(options?: MediaPickOptions | undefined): Promise<readonly PickedAsset[]>;\n    /** Request camera permission and return at most one normalized asset. */\n    capture(options?: {\n        readonly kind?: MediaKind | undefined;\n    } | undefined): Promise<readonly PickedAsset[]>;\n}"
        },
        {
          "name": "MediaPickOptions",
          "slug": "media-pick-options",
          "kind": "type",
          "declaration": "type MediaPickOptions = {\n    readonly max?: number | undefined;\n    readonly kinds?: readonly MediaKind[] | undefined;\n};"
        },
        {
          "name": "MediaPlatform",
          "slug": "media-platform",
          "kind": "type",
          "declaration": "type MediaPlatform = 'ios' | 'android' | 'web';"
        },
        {
          "name": "MediaSaver",
          "slug": "media-saver",
          "kind": "interface",
          "declaration": "interface MediaSaver {\n    saveToDevice(images: readonly SaveableMedia[]): Promise<SaveResult>;\n}"
        },
        {
          "name": "MediaStrings",
          "slug": "media-strings",
          "kind": "interface",
          "declaration": "interface MediaStrings {\n    /** 자산 정보 조회 15s 데드라인 초과 — `device-timeout`. */\n    readonly deviceInfoTimeout: string;\n    /** iCloud 원본 다운로드 60s 데드라인 초과 — `device-timeout`. */\n    readonly iCloudDownloadTimeout: string;\n    /** 원본이 iCloud에만 있고 다운로드 옵트인이 없음 — `device-icloud-only`. */\n    readonly iCloudOnly: string;\n    /** 로컬 파일 없음/판독 불가 — `device-not-found`. 전신은 hashFile.ts:71과 문구를 공유했다. */\n    readonly fileNotFound: string;\n    /** 기기 라이브러리 어댑터/OS 호출 실패 — 원본 예외를 공개하지 않는다. */\n    readonly deviceLibraryFailed: string;\n    /** 피커 어댑터/웹 바이너리 로더 호출 실패 — 원본 예외를 공개하지 않는다. */\n    readonly pickerFailed: string;\n    /** 이미지 decoder 또는 transform 실패 — URI와 네이티브 원문은 공개하지 않는다. */\n    readonly imageProcessingFailed: string;\n    readonly unsupportedFileType: string;\n    /** 전신 uploader.ts:625 — 드롭/선택 결과에 업로드 가능한 미디어가 없음. */\n    readonly noMediaFiles: string;\n    /** 전신 uploader.ts:678 — 피커가 준 사진 자산에 uri가 없음. */\n    readonly pickedPhotoInvalid: string;\n    /** 전신 uploader.ts:717 — 피커가 준 미디어 자산에 uri가 없음. */\n    readonly pickedMediaInvalid: string;\n    readonly imageSizeUnknown: string;\n    readonly videoSizeUnknown: string;\n    readonly imageUploadFailed: string;\n    readonly videoUploadFailed: string;\n    /** 전신 uploader.ts:237, 295 — 포스터(썸네일) PUT 실패. */\n    readonly posterUploadFailed: string;\n    readonly photoPermissionRequired: string;\n    readonly mediaPermissionRequired: string;\n    readonly cameraPermissionRequired: string;\n    readonly savePermissionDenied: string;\n    readonly saveDownloadFailed: string;\n    /**\n     * `platform-unsupported` — 비네이티브 포크(web·SSR·RSC)에서 네이티브 전용 경로를 호출했을 때.\n     * 전신에는 대응 문구가 없다(전신의 web 포크는 영어 `Error`를 던졌다 —\n     * devicePhotoLibrary.web.ts:29). 이 코드는 §5.2에서 신설됐는데 §4의 키 목록이 함께\n     * 늘지 않아, 초기 구현이 `pickedMediaInvalid`·`fileNotFound`·`saveDownloadFailed` 셋을\n     * 돌려 쓰고 있었다 — 전부 원인과 무관한 문구다(\"파일을 찾을 수 없습니다\"는 사용자를\n     * 파일 탐색으로 오도한다). 그래서 전용 키로 분리한다.\n     */\n    readonly platformUnsupported: string;\n    /** 크기 초과 — 단위 표기가 언어마다 다르므로 함수. */\n    readonly fileTooLarge: (input: {\n        readonly maxBytes: number;\n        readonly kind: MediaKind;\n    }) => string;\n}"
        },
        {
          "name": "MediaTelemetry",
          "slug": "media-telemetry",
          "kind": "interface",
          "declaration": "interface MediaTelemetry {\n    /**\n     * `run()`을 감싸 성공/예외를 자동 보고한다. 구현은 `run()`의 결과·예외를 그대로\n     * return/rethrow해야 한다. 라이브러리는 호스트 텔레메트리를 관측자로 취급하므로,\n     * 구현 자체의 예외는 실제 업로드·저장 결과를 바꾸지 않는다.\n     */\n    track<T>(operation: MediaOperation, extra: Readonly<Record<string, unknown>>, run: () => Promise<T>): Promise<T>;\n    begin(operation: MediaOperation, extra?: Readonly<Record<string, unknown>> | undefined): MediaActivity;\n}"
        },
        {
          "name": "MediaUploadApi",
          "slug": "media-upload-api",
          "kind": "interface",
          "declaration": "/**\n * 백엔드 계약: presigned 슬롯을 발급받고, 올라간 오브젝트를 등록한다.\n * `TAsset`은 호스트 API가 저장된 자산으로 반환하는 무엇이든 된다.\n */\ninterface MediaUploadApi<TAsset, TCollectionId extends string = string> extends MediaUploadIntentApi<TCollectionId> {\n    completeUpload(input: MediaUploadCompletion<TCollectionId>): Promise<UploadResult<TAsset>>;\n}",
          "sourceDocumentation": "백엔드 계약: presigned 슬롯을 발급받고, 올라간 오브젝트를 등록한다.\n`TAsset`은 호스트 API가 저장된 자산으로 반환하는 무엇이든 된다."
        },
        {
          "name": "MediaUploadCompletion",
          "slug": "media-upload-completion",
          "kind": "type",
          "declaration": "/** 바이트가 스토리지에 올라간 뒤 보내는 등록 페이로드. */\ntype MediaUploadCompletion<TCollectionId extends string = string> = {\n    readonly fileName: string;\n    readonly contentType: MediaContentType;\n    readonly sizeBytes: number;\n    readonly objectName: string;\n    readonly contentHash?: string | undefined;\n    /**\n     * 불투명 그룹 id — 킷은 해석하지 않고 전달만 한다(§6.2 기각: `CollectionId` 브랜드).\n     * memorylog2는 이것을 `albumId`로 매핑한다. 빈 문자열은 런타임 차단(§6.1-⑪) —\n     * falsy 스프레드로 조용히 탈락해 앨범 없이 저장되는 경로를 막는다.\n     */\n    readonly collectionId?: TCollectionId | undefined;\n    readonly photo?: MediaMetadata | undefined;\n    readonly durationMs?: number | undefined;\n    readonly width?: number | undefined;\n    readonly height?: number | undefined;\n    readonly poster?: UploadedPoster | undefined;\n};",
          "sourceDocumentation": "바이트가 스토리지에 올라간 뒤 보내는 등록 페이로드."
        },
        {
          "name": "MediaUploadConfig",
          "slug": "media-upload-config",
          "kind": "type",
          "declaration": "type MediaUploadConfig<TAsset, TCollectionId extends string = string> = MediaUploadBaseConfig<TCollectionId> & {\n    readonly api: MediaUploadApi<TAsset, TCollectionId>;\n};"
        },
        {
          "name": "mediaUploadFailureInfo",
          "slug": "media-upload-failure-info",
          "kind": "function",
          "declaration": "/**\n * 실패한 업로드가 남긴 정리 후보를 읽는다.\n *\n * `MediaError`와 마찬가지로 `instanceof`가 아니라 전역 심볼을 읽으므로, `.`에서 잡은\n * 에러가 `./core` 또는 다른 CJS/ESM 사본에서 만들어졌어도 동작한다. 값에는 presigned URL,\n * HTTP header, 원본 네트워크 에러가 절대 포함되지 않는다.\n */\ndeclare function mediaUploadFailureInfo(error: unknown): MediaUploadFailureInfo | null;",
          "sourceDocumentation": "실패한 업로드가 남긴 정리 후보를 읽는다.\n\n`MediaError`와 마찬가지로 `instanceof`가 아니라 전역 심볼을 읽으므로, `.`에서 잡은\n에러가 `./core` 또는 다른 CJS/ESM 사본에서 만들어졌어도 동작한다. 값에는 presigned URL,\nHTTP header, 원본 네트워크 에러가 절대 포함되지 않는다."
        },
        {
          "name": "MediaUploadFailureInfo",
          "slug": "media-upload-failure-info--type",
          "kind": "type",
          "declaration": "/**\n * `mediaUploadFailureInfo(error)`가 돌려주는, cross-entry-safe 실패 복구 정보.\n *\n * 이 정보는 에러에 전역 심볼로 비열거형 각인되므로 code splitting으로 코어 사본이 갈린\n * 엔트리에서도 검사할 수 있다. `orphanedObjects`는 attachment/등록이 끝나기 전에 남은\n * 정리 후보이며, 앱은 자신의 권한 있는 cleanup API로만 처리해야 한다.\n */\ntype MediaUploadFailureInfo = {\n    readonly stage: MediaUploadFailureStage;\n    readonly orphanedObjects: readonly MediaOrphanedUpload[];\n};",
          "sourceDocumentation": "`mediaUploadFailureInfo(error)`가 돌려주는, cross-entry-safe 실패 복구 정보.\n\n이 정보는 에러에 전역 심볼로 비열거형 각인되므로 code splitting으로 코어 사본이 갈린\n엔트리에서도 검사할 수 있다. `orphanedObjects`는 attachment/등록이 끝나기 전에 남은\n정리 후보이며, 앱은 자신의 권한 있는 cleanup API로만 처리해야 한다."
        },
        {
          "name": "MediaUploadFailureStage",
          "slug": "media-upload-failure-stage",
          "kind": "type",
          "declaration": "/** 업로드 파이프라인에서 안전하게 공개할 수 있는 실패 단계. URL·헤더·원본 예외는 포함하지 않는다. */\ntype MediaUploadFailureStage = 'intent' | 'put' | 'complete';",
          "sourceDocumentation": "업로드 파이프라인에서 안전하게 공개할 수 있는 실패 단계. URL·헤더·원본 예외는 포함하지 않는다."
        },
        {
          "name": "MediaUploadIntent",
          "slug": "media-upload-intent",
          "kind": "type",
          "declaration": "/** 백엔드가 발급한 단일 오브젝트 presigned 업로드 슬롯. */\ntype MediaUploadIntent = {\n    readonly uploadUrl: string;\n    readonly method: 'PUT';\n    readonly headers: Readonly<Record<string, string>>;\n    readonly objectName: string;\n};",
          "sourceDocumentation": "백엔드가 발급한 단일 오브젝트 presigned 업로드 슬롯."
        },
        {
          "name": "MediaUploadIntentApi",
          "slug": "media-upload-intent-api",
          "kind": "interface",
          "declaration": "/**\n * Presign-only backend seam.\n *\n * Some products deliberately attach the uploaded object in a later domain\n * transaction (for example, after creating a record). They must not pretend\n * that a registration endpoint exists just to use the local streaming path.\n * `MediaUploadApi` extends this narrower contract for the usual\n * presign → PUT → complete flow.\n */\ninterface MediaUploadIntentApi<TCollectionId extends string = string> {\n    createUploadIntent(input: MediaUploadIntentRequest<TCollectionId>): Promise<MediaUploadIntent>;\n}",
          "sourceDocumentation": "Presign-only backend seam.\n\nSome products deliberately attach the uploaded object in a later domain\ntransaction (for example, after creating a record). They must not pretend\nthat a registration endpoint exists just to use the local streaming path.\n`MediaUploadApi` extends this narrower contract for the usual\npresign → PUT → complete flow."
        },
        {
          "name": "MediaUploadIntentRequest",
          "slug": "media-upload-intent-request",
          "kind": "type",
          "declaration": "type MediaUploadIntentRequest<TCollectionId extends string = string> = {\n    readonly fileName: string;\n    /** ⚠ 전신은 `string`이었다. 닫힌 8종 유니언으로 좁혀 서버 zod와 클라이언트가 어긋나지 않게 한다. */\n    readonly contentType: MediaContentType;\n    readonly sizeBytes: number;\n    /**\n     * 업로드를 시작하기 전에 백엔드가 권한·용량을 확인해야 하는 경우의 불투명 그룹 id.\n     * 완료 단계의 `collectionId`와 같은 값이며, 킷은 해석하거나 생성하지 않고 그대로 전달한다.\n     * 선택적 필드라 기존 presign-only 소비자는 변경 없이 동작한다.\n     */\n    readonly collectionId?: TCollectionId | undefined;\n};"
        },
        {
          "name": "MediaUploadLimit",
          "slug": "media-upload-limit",
          "kind": "type",
          "declaration": "/**\n * 백엔드 정책을 반영한 클라이언트 크기 캡. 같은 문구로 **빨리** 실패시켜 사용자가 전체 업로드를\n * 마친 뒤 413을 받는 일을 없앤다.\n */\ntype MediaUploadLimit = {\n    readonly maxBytes: number;\n    readonly message?: string | undefined;\n};",
          "sourceDocumentation": "백엔드 정책을 반영한 클라이언트 크기 캡. 같은 문구로 **빨리** 실패시켜 사용자가 전체 업로드를\n마친 뒤 413을 받는 일을 없앤다."
        },
        {
          "name": "MediaUploadLimits",
          "slug": "media-upload-limits",
          "kind": "type",
          "declaration": "/**\n * ⚠ 팩토리에서 이 값은 **생략 불가**다(§6.1-③). 무제한 업로드는 명시적 결정이어야 한다.\n * 서버만 검증하는 정책도 정당하므로 팩토리 설정은 `MediaUploadLimits | 'server-enforced'`를 받는다\n * (`Number.POSITIVE_INFINITY`는 JSON 직렬화 불가라 기각 — §0.4 기각 8).\n */\ntype MediaUploadLimits = {\n    readonly image?: MediaUploadLimit | undefined;\n    readonly video?: MediaUploadLimit | undefined;\n};",
          "sourceDocumentation": "⚠ 팩토리에서 이 값은 **생략 불가**다(§6.1-③). 무제한 업로드는 명시적 결정이어야 한다.\n서버만 검증하는 정책도 정당하므로 팩토리 설정은 `MediaUploadLimits | 'server-enforced'`를 받는다\n(`Number.POSITIVE_INFINITY`는 JSON 직렬화 불가라 기각 — §0.4 기각 8)."
        },
        {
          "name": "MediaUploadObject",
          "slug": "media-upload-object",
          "kind": "type",
          "declaration": "/**\n * 스토리지에 쓸 수 있는 이름으로 발급된 오브젝트의 안전한 식별 정보.\n *\n * URL·헤더·서명은 의도적으로 없다. 앱은 이 값만 자기 cleanup API에 넘겨, 실패한\n * 업로드가 남긴 object를 best-effort로 정리할 수 있다. `objectName`의 권한 검증은\n * 언제나 서버가 다시 해야 하며, 이 타입은 클라이언트 권한 증명이 아니다. 런타임에서는\n * 1024자 이하 ASCII unreserved 경로 세그먼트(`[A-Za-z0-9._~-]`)와 `/`만 허용한다.\n * URL/query/percent-encoding/공백을 받지 않으므로, 서버는 그 문법을 발급 키에도 맞춰야 한다.\n */\ntype MediaUploadObject = {\n    readonly objectName: string;\n    readonly contentType: MediaContentType;\n    readonly sizeBytes: number;\n};",
          "sourceDocumentation": "스토리지에 쓸 수 있는 이름으로 발급된 오브젝트의 안전한 식별 정보.\n\nURL·헤더·서명은 의도적으로 없다. 앱은 이 값만 자기 cleanup API에 넘겨, 실패한\n업로드가 남긴 object를 best-effort로 정리할 수 있다. `objectName`의 권한 검증은\n언제나 서버가 다시 해야 하며, 이 타입은 클라이언트 권한 증명이 아니다. 런타임에서는\n1024자 이하 ASCII unreserved 경로 세그먼트(`[A-Za-z0-9._~-]`)와 `/`만 허용한다.\nURL/query/percent-encoding/공백을 받지 않으므로, 서버는 그 문법을 발급 키에도 맞춰야 한다."
        },
        {
          "name": "NamedBinarySource",
          "slug": "named-binary-source",
          "kind": "interface",
          "declaration": "/** 웹 File의 구조적 최소치 — DOM lib 없이 `isSupportedMediaFile`을 쓰기 위해(§7 하드닝 10). */\ninterface NamedBinarySource extends BinarySource {\n    readonly name: string;\n}",
          "sourceDocumentation": "웹 File의 구조적 최소치 — DOM lib 없이 `isSupportedMediaFile`을 쓰기 위해(§7 하드닝 10)."
        },
        {
          "name": "noopMediaTelemetry",
          "slug": "noop-media-telemetry",
          "kind": "constant",
          "declaration": "noopMediaTelemetry: MediaTelemetry",
          "sourceDocumentation": "팩토리 기본값. `track`은 run()을 그대로 실행하고 `begin`은 no-op 활동을 준다\n(전신 `NOOP_TELEMETRY` — types.ts:94-101 계승).\n텔레메트리를 주입하지 않은 소비자에게 분기(`telemetry?.track ?? …`)를 강요하지 않기 위한 값이므로,\n라이브러리 내부는 항상 이 객체를 통해 호출한다."
        },
        {
          "name": "normalizeDurationMs",
          "slug": "normalize-duration-ms",
          "kind": "function",
          "declaration": "/**\n * 피커가 준 원시 duration을 밀리초로 정규화한다.\n *\n * - `os === 'web'` → 초 단위이므로 ×1000\n * - 그 외(ios·android) → 이미 밀리초이므로 그대로\n * - 0·음수·NaN·Infinity → `undefined` (전신 `asset.duration > 0` 게이트 보존)\n *\n * 반올림 규칙(`Math.round`)도 전신 그대로다 — 서버에 소수점 밀리초를 보내지 않는다.\n */\ndeclare function normalizeDurationMs(raw: number | undefined, os: MediaPlatform): number | undefined;",
          "sourceDocumentation": "피커가 준 원시 duration을 밀리초로 정규화한다.\n\n- `os === 'web'` → 초 단위이므로 ×1000\n- 그 외(ios·android) → 이미 밀리초이므로 그대로\n- 0·음수·NaN·Infinity → `undefined` (전신 `asset.duration > 0` 게이트 보존)\n\n반올림 규칙(`Math.round`)도 전신 그대로다 — 서버에 소수점 밀리초를 보내지 않는다."
        },
        {
          "name": "parseExifWallClock",
          "slug": "parse-exif-wall-clock",
          "kind": "function",
          "declaration": "/**\n * Strictly parses the EXIF datetime representation as a wall clock.\n *\n * `Date.UTC` normally normalises invalid values (for example February 30), so\n * all fields are compared after construction before the value is exposed. This\n * helper has no device-time-zone dependency and is therefore safe for a\n * historical activity or trip whose offset was persisted at record time.\n */\ndeclare function parseExifWallClock(value: unknown): ExifWallClock | undefined;",
          "sourceDocumentation": "Strictly parses the EXIF datetime representation as a wall clock.\n\n`Date.UTC` normally normalises invalid values (for example February 30), so\nall fields are compared after construction before the value is exposed. This\nhelper has no device-time-zone dependency and is therefore safe for a\nhistorical activity or trip whose offset was persisted at record time."
        },
        {
          "name": "PendingAddResult",
          "slug": "pending-add-result",
          "kind": "type",
          "declaration": "type PendingAddResult = {\n    /** The next state. Same reference as the input when nothing was added. */\n    readonly state: PendingSelectionState;\n    /** Items that entered the selection, in input order. */\n    readonly added: readonly PendingMediaItem[];\n    /**\n     * Items that did not enter the selection, in input order, with the reason. Use it for messaging.\n     * A `duplicate` entry can be the very object that is already staged (the host re-added it), so\n     * do not release this list directly — release `releasable`.\n     */\n    readonly rejected: readonly PendingRejection[];\n    /**\n     * Rejected items that are safe to hand to `release`: every rejected item that is not the same\n     * object as an item in `state`, each object once, in input order. Their `revoke` has not been\n     * called. Picked items and binaries without `revoke` may appear here; `release` ignores them.\n     */\n    readonly releasable: readonly PendingMediaItem[];\n};"
        },
        {
          "name": "PendingBinaryItem",
          "slug": "pending-binary-item",
          "kind": "type",
          "declaration": "/**\n * A binary (web `File`, `Blob`, or any `NamedBinarySource`) staged for a later upload.\n *\n * Core never touches DOM APIs, so the preview resource is injected: `previewUri` is whatever the\n * host can render (typically an object URL) and `revoke` releases it. On web,\n * `pendingItemFromFile` from `@gj-kit/expo-media/web` builds this shape from a DOM `File`.\n */\ntype PendingBinaryItem = {\n    readonly kind: 'binary';\n    readonly source: NamedBinarySource;\n    /** Renderable preview URI (for example an object URL). Omit when none exists. */\n    readonly previewUri?: string | undefined;\n    /**\n     * Releases the preview resource. The selection calls it **at most once per item object**, when\n     * the item leaves a selection through `remove`/`clear`, or when the host passes it to `release`.\n     */\n    readonly revoke?: (() => void) | undefined;\n    /**\n     * Modification timestamp (ms epoch) folded into the dedup key, e.g. DOM `File.lastModified`.\n     * Without it two binaries with the same name and size are considered the same file.\n     */\n    readonly lastModified?: number | undefined;\n};",
          "sourceDocumentation": "A binary (web `File`, `Blob`, or any `NamedBinarySource`) staged for a later upload.\n\nCore never touches DOM APIs, so the preview resource is injected: `previewUri` is whatever the\nhost can render (typically an object URL) and `revoke` releases it. On web,\n`pendingItemFromFile` from `@gj-kit/expo-media/web` builds this shape from a DOM `File`."
        },
        {
          "name": "PendingMediaItem",
          "slug": "pending-media-item",
          "kind": "type",
          "declaration": "type PendingMediaItem = PendingPickedItem | PendingBinaryItem;"
        },
        {
          "name": "PendingPickedItem",
          "slug": "pending-picked-item",
          "kind": "type",
          "declaration": "/** A picker, camera, or device-library asset staged for a later upload. */\ntype PendingPickedItem = {\n    readonly kind: 'picked';\n    readonly asset: PickedAsset;\n};",
          "sourceDocumentation": "A picker, camera, or device-library asset staged for a later upload."
        },
        {
          "name": "PendingRejection",
          "slug": "pending-rejection",
          "kind": "type",
          "declaration": "type PendingRejection = {\n    readonly item: PendingMediaItem;\n    readonly reason: PendingRejectionReason;\n};"
        },
        {
          "name": "PendingRejectionReason",
          "slug": "pending-rejection-reason",
          "kind": "type",
          "declaration": "/**\n * Why an item did not enter the selection.\n * - `duplicate`: an item with the same `keyOf` is already staged (or appeared earlier in the batch).\n * - `over-limit`: the selection already holds `max` items.\n */\ntype PendingRejectionReason = 'duplicate' | 'over-limit';",
          "sourceDocumentation": "Why an item did not enter the selection.\n- `duplicate`: an item with the same `keyOf` is already staged (or appeared earlier in the batch).\n- `over-limit`: the selection already holds `max` items."
        },
        {
          "name": "PendingSelection",
          "slug": "pending-selection",
          "kind": "interface",
          "declaration": "interface PendingSelection {\n    /** The configured cap. */\n    readonly max: number;\n    /**\n     * Stable identity of an item: `asset:<assetId>` (falling back to the URI when the picker gave\n     * no asset id) for picked items, `binary:<name>:<size>:<lastModified>` for binaries.\n     */\n    keyOf(item: PendingMediaItem): string;\n    /**\n     * Stage items. Duplicates (by `keyOf`, including duplicates within `items`) are rejected before\n     * the cap is checked, so a re-selected photo reports `duplicate` even when the selection is full.\n     * Nothing is revoked here; pass the result's `releasable` to `release`. Neither `state` nor\n     * `items` is mutated, and the result object and its arrays are frozen.\n     */\n    add(state: PendingSelectionState, items: readonly PendingMediaItem[]): PendingAddResult;\n    /**\n     * Drop every item whose `keyOf` equals `key`, calling `revoke` on removed binaries. Returns the\n     * same reference when no item matched.\n     */\n    remove(state: PendingSelectionState, key: string): PendingSelectionState;\n    /** Drop every item, calling `revoke` on each binary that still holds a preview resource. */\n    clear(state: PendingSelectionState): PendingSelectionState;\n    /**\n     * Call `revoke` on binaries that are not staged — typically `PendingAddResult.releasable`.\n     * Idempotent per item object; picked items and binaries without `revoke` are ignored. Passing an\n     * item that is still staged kills its live preview, which is why `rejected` is not the input here.\n     */\n    release(items: readonly PendingMediaItem[]): void;\n    /** Picked assets in selection order — the input of `uploadPickedAsset` / device resolve paths. */\n    toPickedAssets(state: PendingSelectionState): readonly PickedAsset[];\n    /** Binary sources in selection order — the input of `uploadBinary` / `uploadDropped`. */\n    toBinarySources(state: PendingSelectionState): readonly NamedBinarySource[];\n    /**\n     * URI the host can render, or `null`. Picked assets always preview through their own URI. A binary\n     * previews only when it has a `previewUri` **and** is not HEIC/HEIF — browsers cannot decode those,\n     * and an `<img>` that never loads looks like a broken upload.\n     */\n    previewUriOf(item: PendingMediaItem): string | null;\n    /** `previewUriOf(item) !== null`. */\n    canPreview(item: PendingMediaItem): boolean;\n    /**\n     * Capture time as an ISO string, or `null`. Picked items read the picker EXIF dictionary; binaries\n     * parse the JPEG APP1 segment from bytes (the same parsers the upload path uses). Never throws.\n     */\n    capturedAtOf(item: PendingMediaItem): Promise<string | null>;\n}"
        },
        {
          "name": "PendingSelectionOptions",
          "slug": "pending-selection-options",
          "kind": "type",
          "declaration": "type PendingSelectionOptions = {\n    /** Maximum number of staged items. Must be a positive integer; otherwise `MediaError('config-invalid')`. */\n    readonly max: number;\n};"
        },
        {
          "name": "PendingSelectionState",
          "slug": "pending-selection-state",
          "kind": "type",
          "declaration": "/** Ordered, immutable selection. Every operation returns a new array or the same reference. */\ntype PendingSelectionState = readonly PendingMediaItem[];",
          "sourceDocumentation": "Ordered, immutable selection. Every operation returns a new array or the same reference."
        },
        {
          "name": "PickedAsset",
          "slug": "picked-asset",
          "kind": "type",
          "declaration": "type PickedAsset = {\n    readonly uri: string;\n    /**\n     * 기기 라이브러리 원본 식별자(iOS PhotoKit localIdentifier / Android MediaStore id).\n     * 없을 수 있다(웹 File 드롭, 카메라 캡처).\n     *\n     * ⚠ **왜 필요한가**: 소비자의 dedup 1차 키다 —\n     *   `apps/mobile/src/photos/pendingPhotos.ts:43`\n     *   ``pickerAssetDedupKey = `asset:${asset.assetId ?? asset.uri}` ``\n     * uri 폴백만 남으면 스테이징 사본 uri가 resolve마다 재생성되므로(§7 하드닝 2) 같은 사진을\n     * 두 번 선택했을 때 dedup이 통과해버린다. 전신은 `resolveDeviceAssetForUpload`가\n     * `assetId: asset.id`를 채워 device 자산과 picker 자산의 동일성을 유지했다\n     *   — devicePhotoLibrary.ts:364. 새 설계에서는 `toPickedAsset`이 그 역할을 한다(§5.4-④).\n     */\n    readonly assetId?: string | undefined;\n    readonly fileName?: string | undefined;\n    readonly mimeType?: string | undefined;\n    readonly width?: number | undefined;\n    readonly height?: number | undefined;\n    /**\n     * ⚠ 어댑터는 **원시값 그대로** 넘긴다. 네이티브는 ms, 웹은 s이며 정규화는 core가 한다\n     * (§7 하드닝 4 — 정규화 지점을 하나로 고정. 어댑터가 변환하면 이중 변환이 된다).\n     *\n     * 전신 사고: expo-image-picker 웹이 `HTMLVideoElement.duration`(초)을 그대로 전달해\n     * 20분 영상이 1200ms로 저장됐고, 어떤 길이 캡도 그것을 통과시켰다.\n     */\n    readonly durationRaw?: number | undefined;\n    readonly exif?: Readonly<Record<string, unknown>> | undefined;\n    /**\n     * 어댑터가 \"실제로 스트리밍될 파일\"을 stat해 확인한 크기. 있으면 최우선.\n     * 전신의 뒷문 프로퍼티 `__photoKitVerifiedSizeBytes`를 정식 필드로 승격(§7 하드닝 3).\n     */\n    readonly verifiedSizeBytes?: number | undefined;\n    /**\n     * ⚠ 신뢰 금지 — 최후 폴백이다(§7 하드닝 3).\n     * Android가 `quality < 1`로 재인코딩할 때 `asset.fileSize`는 **원본** 크기를 보고하므로\n     * 스토리지가 실제로 받은 바이트와 어긋나 서버가 업로드를 거절한다.\n     * 필드명이 곧 신뢰도다: `verifiedSizeBytes`(실측) > file-system stat > `reportedSizeBytes`(자칭).\n     */\n    readonly reportedSizeBytes?: number | undefined;\n};"
        },
        {
          "name": "PickerAdapter",
          "slug": "picker-adapter",
          "kind": "interface",
          "declaration": "interface PickerAdapter {\n    requestLibraryPermission(kinds: readonly MediaKind[]): Promise<MediaPermission>;\n    requestCameraPermission(): Promise<MediaPermission>;\n    /**\n     * ⚠ iOS 원본 fast path 고정 조합(§7.1): `quality: 1` · `exif: true` ·\n     * `allowsEditing: false` · `preferredAssetRepresentationMode: Current`.\n     * **단일선택/다중선택이 달라지면 안 된다** — 한쪽만 재인코딩 경로로 빠지면 같은 사진이\n     * 선택 방식에 따라 다른 바이트로 업로드된다.\n     */\n    pickFromLibrary(input: {\n        readonly kinds: readonly MediaKind[];\n        readonly max: number;\n    }): Promise<readonly PickedAsset[]>;\n    /**\n     * 카메라 캡처.\n     * ⚠ 상위 플로우(`captureAndUpload`)는 **항상 최대 1건**으로 자른다\n     * (전신 uploader.ts:1008 `result.assets.slice(0, 1)`).\n     */\n    capture(input: {\n        readonly kind: MediaKind;\n    }): Promise<readonly PickedAsset[]>;\n}"
        },
        {
          "name": "PickerFlows",
          "slug": "picker-flows",
          "kind": "interface",
          "declaration": "interface PickerFlows<TAsset, TCollectionId extends string = string> {\n    pick(options?: {\n        readonly max?: number | undefined;\n        readonly kinds?: readonly MediaKind[] | undefined;\n    } | undefined): Promise<readonly PickedAsset[]>;\n    pickAndUpload(options?: PickUploadOptions<TCollectionId> | undefined): Promise<readonly UploadResult<TAsset>[]>;\n    /**\n     * ⚠ 항상 **최대 1건**이다(전신 uploader.ts:1008 `result.assets.slice(0, 1)`).\n     * 무시되는 옵션은 그 자체로 함정이므로 옵션 타입에서 `max`를 Omit한다(§5.4.1-12).\n     */\n    captureAndUpload(options?: (Omit<PickUploadOptions<TCollectionId>, 'max'> & {\n        readonly kind?: MediaKind | undefined;\n    }) | undefined): Promise<readonly UploadResult<TAsset>[]>;\n}"
        },
        {
          "name": "PickUploadOptions",
          "slug": "pick-upload-options",
          "kind": "type",
          "declaration": "type PickUploadOptions<TCollectionId extends string = string> = {\n    readonly collectionId?: TCollectionId | null | undefined;\n    readonly max?: number | undefined;\n    readonly kinds?: readonly MediaKind[] | undefined;\n};"
        },
        {
          "name": "PlatformAdapter",
          "slug": "platform-adapter",
          "kind": "interface",
          "declaration": "/**\n * core에서 `react-native` import를 제거하는 유일한 이유. 필드 2개뿐인 것이 정상이다.\n *\n * 전신 `debug.ts`는 `Platform`을 직접 import했고, 그 한 줄 때문에 서명 URL 새니타이저\n * (§7 하드닝 8)를 순수 유닛으로 검증할 수 없었다. 주입으로 바꾸면서 debug 모듈이 core로 하강했다.\n */\ninterface PlatformAdapter {\n    readonly os: MediaPlatform;\n    /** `__DEV__` 상당. 디버그 로거 게이트(§7 하드닝 8). expo 기본값: `__DEV__ && NODE_ENV !== 'test'`. */\n    readonly isDev: boolean;\n}",
          "sourceDocumentation": "core에서 `react-native` import를 제거하는 유일한 이유. 필드 2개뿐인 것이 정상이다.\n\n전신 `debug.ts`는 `Platform`을 직접 import했고, 그 한 줄 때문에 서명 URL 새니타이저\n(§7 하드닝 8)를 순수 유닛으로 검증할 수 없었다. 주입으로 바꾸면서 debug 모듈이 core로 하강했다."
        },
        {
          "name": "POSTER_CONTENT_TYPE",
          "slug": "poster-content-type",
          "kind": "constant",
          "declaration": "POSTER_CONTENT_TYPE: 'image/jpeg'",
          "sourceDocumentation": "포스터 contentType. 전신 `videoPoster.ts:4` `VIDEO_POSTER_CONTENT_TYPE`의 공개 계승(§5.4.1-7).\npresign 요청의 contentType과 서버 검증이 맞물리므로 **소비자가 읽을 수 있어야 한다**."
        },
        {
          "name": "PutRequest",
          "slug": "put-request",
          "kind": "type",
          "declaration": "type PutRequest = {\n    readonly url: string;\n    readonly method: 'PUT';\n    readonly headers: Readonly<Record<string, string>>;\n};"
        },
        {
          "name": "ResolvedDeviceAsset",
          "slug": "resolved-device-asset",
          "kind": "type",
          "declaration": "/**\n * `DeviceLibrary.resolveForUpload`의 결과 — 업로드 가능한 바이트의 위치(§5.4-④).\n * 전신 `DeviceAssetUploadSource`(devicePhotoLibrary.ts:255-259).\n */\ntype ResolvedDeviceAsset = {\n    readonly uri: string;\n    readonly verifiedSizeBytes?: number | undefined;\n    /** ⚠ 전신 그대로 `null` 가능이다. `PickedAsset.exif`로 옮길 때 undefined 정규화가 필요하다. */\n    readonly exif: Readonly<Record<string, unknown>> | null;\n    /** 스테이징 사본이면 true — 업로드 후 `StagingCache.cleanup` 대상(§7 하드닝 7). */\n    readonly staged: boolean;\n};",
          "sourceDocumentation": "`DeviceLibrary.resolveForUpload`의 결과 — 업로드 가능한 바이트의 위치(§5.4-④).\n전신 `DeviceAssetUploadSource`(devicePhotoLibrary.ts:255-259)."
        },
        {
          "name": "ResolvedPickedAsset",
          "slug": "resolved-picked-asset",
          "kind": "type",
          "declaration": "/** `DeviceLibrary.resolvePickedAsset` / `toPickedAsset`의 결과(§5.4-④). */\ntype ResolvedPickedAsset = PickedAsset & {\n    readonly staged: boolean;\n};",
          "sourceDocumentation": "`DeviceLibrary.resolvePickedAsset` / `toPickedAsset`의 결과(§5.4-④)."
        },
        {
          "name": "resolveUploadSize",
          "slug": "resolve-upload-size",
          "kind": "function",
          "declaration": "/**\n * 세 후보 중 가장 신뢰도 높은 것을 고른다. 전부 없으면 `null` —\n * 호출자가 `imageSizeUnknown`/`videoSizeUnknown` 문구로 실패시킨다(§6.1-⑮, 전신 유지).\n *\n * ⚠ 순서를 바꾸면 Android 재인코딩 자산이 다시 원본 크기로 presign된다.\n */\ndeclare function resolveUploadSize(input: {\n    /** 어댑터가 \"실제로 스트리밍될 파일\"을 stat해 확인한 크기(§3.3 `PickedAsset.verifiedSizeBytes`). */\n    readonly verifiedSizeBytes?: number | undefined;\n    /** 코어가 `FileSystemAdapter.stat`으로 방금 읽은 크기. */\n    readonly statSizeBytes?: number | undefined;\n    /** ⚠ 피커 자칭(`asset.fileSize`). 최후 폴백이며 단독으로는 신뢰하지 않는다. */\n    readonly reportedSizeBytes?: number | undefined;\n}): {\n    readonly sizeBytes: number;\n    readonly source: UploadSizeSource;\n} | null;",
          "sourceDocumentation": "세 후보 중 가장 신뢰도 높은 것을 고른다. 전부 없으면 `null` —\n호출자가 `imageSizeUnknown`/`videoSizeUnknown` 문구로 실패시킨다(§6.1-⑮, 전신 유지).\n\n⚠ 순서를 바꾸면 Android 재인코딩 자산이 다시 원본 크기로 presign된다."
        },
        {
          "name": "sanitizeMediaErrorMessage",
          "slug": "sanitize-media-error-message",
          "kind": "function",
          "declaration": "/**\n * 전신 `sanitizePhotoErrorMessage`.\n *\n * iOS URLSession 실패는 **서명 업로드 URL 전문을 그대로 에코**한다. 그 쿼리에는 임시 자격증명이\n * 들어 있으므로 개발자 로그에도 활동 로그에도 남겨선 안 된다. 플랫폼 에러 코드와 설명은 남기고\n * URL만 `[URL]`로 치환한 뒤 1000자에서 자른다.\n */\ndeclare function sanitizeMediaErrorMessage(message: string): string;",
          "sourceDocumentation": "전신 `sanitizePhotoErrorMessage`.\n\niOS URLSession 실패는 **서명 업로드 URL 전문을 그대로 에코**한다. 그 쿼리에는 임시 자격증명이\n들어 있으므로 개발자 로그에도 활동 로그에도 남겨선 안 된다. 플랫폼 에러 코드와 설명은 남기고\nURL만 `[URL]`로 치환한 뒤 1000자에서 자른다."
        },
        {
          "name": "SaveableMedia",
          "slug": "saveable-media",
          "kind": "type",
          "declaration": "type SaveableMedia = {\n    /**\n     * 안정 파일명의 1차 소스(G8). 없거나 빈 문자열이면 배열 인덱스+1로 폴백한다 —\n     * 전신 규칙 `${prefix}-${image.id || index + 1}.${ext}`(saveImages.ts:71) 보존.\n     */\n    readonly id?: string | undefined;\n    /**\n     * ⚠ **단일 진실이다.** `originalUrl || thumbnailUrl` 같은 폴백은 호스트 DTO 지식이므로\n     * 라이브러리가 아니라 앱이 소유한다(전신 `imageDownloadUrl` 폐지 — §5.7.3).\n     */\n    readonly url: string;\n    readonly fileName?: string | undefined;\n    readonly contentType?: MediaContentType | undefined;\n};"
        },
        {
          "name": "SaveResult",
          "slug": "save-result",
          "kind": "type",
          "declaration": "type SaveResult = {\n    readonly savedCount: number;\n    readonly mode: SaveTarget['kind'];\n};"
        },
        {
          "name": "SaveTarget",
          "slug": "save-target",
          "kind": "type",
          "declaration": "/**\n * 저장 타깃(§6.1-⑦).\n * 전신은 `platformOS:'web'` + `mediaLibrary` 동시 주입이 통과했고, 결과로 보고되는 `mode`와\n * 실제 동작이 어긋날 수 있었다. 판별 유니언이면 무효 조합이 **표현 불가능**해지고\n * `SaveResult.mode`가 `target.kind`에서 파생되므로 보고와 실동작이 어긋날 수 없다.\n */\ntype SaveTarget = {\n    readonly kind: 'media-library';\n    readonly files: FileSystemAdapter & FileDownloadAdapter;\n    readonly library: MediaLibrarySaveAdapter;\n} | {\n    readonly kind: 'browser-download';\n    readonly browser: BrowserSaveAdapter;\n};",
          "sourceDocumentation": "저장 타깃(§6.1-⑦).\n전신은 `platformOS:'web'` + `mediaLibrary` 동시 주입이 통과했고, 결과로 보고되는 `mode`와\n실제 동작이 어긋날 수 있었다. 판별 유니언이면 무효 조합이 **표현 불가능**해지고\n`SaveResult.mode`가 `target.kind`에서 파생되므로 보고와 실동작이 어긋날 수 없다."
        },
        {
          "name": "Sha256Hasher",
          "slug": "sha256-hasher",
          "kind": "interface",
          "declaration": "/**\n * 증분 해셔. `js-sha256`의 `sha256.create()`와 같은 형태라 전신 호출부가 그대로 옮겨진다.\n *\n * ⚠ `hex()`는 **종료 연산**이다(패딩을 상태에 써 넣는다). 두 번 이상 부르면 같은 값을 돌려주고,\n * 종료 후의 `update()`는 무시한다 — 부분적으로 갱신된 다이제스트를 돌려주는 것보다 낫다.\n */\ninterface Sha256Hasher {\n    update(bytes: Uint8Array): void;\n    hex(): string;\n}",
          "sourceDocumentation": "증분 해셔. `js-sha256`의 `sha256.create()`와 같은 형태라 전신 호출부가 그대로 옮겨진다.\n\n⚠ `hex()`는 **종료 연산**이다(패딩을 상태에 써 넣는다). 두 번 이상 부르면 같은 값을 돌려주고,\n종료 후의 `update()`는 무시한다 — 부분적으로 갱신된 다이제스트를 돌려주는 것보다 낫다."
        },
        {
          "name": "sha256Hex",
          "slug": "sha256-hex",
          "kind": "function",
          "declaration": "/** 단발 해시. 증분 경로와 **같은 구현**을 쓰므로 둘이 어긋날 수 없다. */\ndeclare function sha256Hex(bytes: Uint8Array): string;",
          "sourceDocumentation": "단발 해시. 증분 경로와 **같은 구현**을 쓰므로 둘이 어긋날 수 없다."
        },
        {
          "name": "StagingCache",
          "slug": "staging-cache",
          "kind": "interface",
          "declaration": "interface StagingCache extends Brand<'StagingCache'> {\n    /** `${namespace}-upload-`. */\n    readonly prefix: string;\n    /**\n     * cleanup의 유일한 판정 근거.\n     *\n     * 전신은 `uri.includes(PREFIX)` 한 줄이었다(deviceUploadCache.ts:29). 그 술어는\n     * `file:///other/dir/memorylog-upload-x.jpg`처럼 **우리가 만들지 않은 경로**도 참으로 만들고,\n     * 프리픽스가 중간에 낀 임의의 경로까지 삭제 대상에 넣는다. 3조건으로 좁힌다:\n     *   (i) 앱 캐시 디렉토리로 시작 (ii) 파일명이 prefix로 **시작** (iii) 하위 경로 없음\n     */\n    owns(uri: string | null | undefined): boolean;\n    /** 스테이징 사본이 놓일 자리. 캐시 디렉토리가 없으면 `null`. */\n    uriFor(asset: DeviceAssetRef): string | null;\n    /**\n     * 스테이징 사본 삭제. `owns()`가 false면 **no-op**이다.\n     * 실패는 삼킨다 — 누수의 대가는 디스크 비용뿐이고, 정리 실패로 업로드 결과를 뒤집을 이유가 없다.\n     */\n    cleanup(uri: string | null | undefined): Promise<void>;\n}"
        },
        {
          "name": "summarizeUri",
          "slug": "summarize-uri",
          "kind": "function",
          "declaration": "/**\n * ⚠ **원문 URI를 절대 로깅하지 않는다.** 서명 업로드 URL은 쿼리에 임시 자격증명을 담고,\n * 사진 보관함 경로는 사용자의 미디어를 식별한다. 모양(scheme·확장자·길이·종류)만 남긴다.\n *\n * `hardening-guard`가 로거 인자에 `uri`/`url` 원문을 넘기는 것을 정적으로 차단한다 —\n * 이 함수를 **경유하지 않은** 전달은 실패다(§10.3).\n */\ndeclare function summarizeUri(uri?: string | null): {\n    readonly scheme: string;\n    readonly extension: string | null;\n    readonly length: number;\n    readonly isFile: boolean;\n    readonly isContent: boolean;\n    readonly isPhotoKit: boolean;\n} | null;",
          "sourceDocumentation": "⚠ **원문 URI를 절대 로깅하지 않는다.** 서명 업로드 URL은 쿼리에 임시 자격증명을 담고,\n사진 보관함 경로는 사용자의 미디어를 식별한다. 모양(scheme·확장자·길이·종류)만 남긴다.\n\n`hardening-guard`가 로거 인자에 `uri`/`url` 원문을 넘기는 것을 정적으로 차단한다 —\n이 함수를 **경유하지 않은** 전달은 실패다(§10.3)."
        },
        {
          "name": "toPickedAsset",
          "slug": "to-picked-asset",
          "kind": "function",
          "declaration": "/**\n * `DeviceAsset`의 정체성(id·filename·치수)과 resolve 결과(바이트 위치·실측 크기·EXIF)를 합쳐\n * 피커 업로드 경로가 받는 형태를 만든다.\n *\n * ⚠ **`exif: resolved.exif ?? undefined` — null → undefined 정규화가 필수다.**\n * EOP(`exactOptionalPropertyTypes`) 하에서 `ResolvedDeviceAsset.exif`는\n * `Readonly<Record<string, unknown>> | null`이고 `PickedAsset.exif`는\n * `?: Readonly<Record<string, unknown>> | undefined`라 **null을 그대로 대입하면 TS2322**다\n * (설계 문서 §5.4-④의 실측 확인). 전신은 `exif: null`을 그대로 흘렸다.\n *\n * ⚠ **`reportedSizeBytes`는 채우지 않는다.** 기기 경로에서 크기의 진실은 `verifiedSizeBytes`\n * (= 실제로 스트리밍될 캐시 사본을 stat한 값)뿐이다. 자칭 크기를 여기서 끼워 넣으면\n * §7 하드닝 3이 세운 신뢰도 서열(verified > file-system stat > reported)이 무너지고,\n * Android 재인코딩 자산에서 서버가 업로드를 거절하던 그 사고가 그대로 재발한다.\n *\n * ⚠ **`assetId`를 반드시 채운다.** 소비자의 dedup 1차 키이기 때문이다(§3.3-⑥):\n * `assetId`가 없으면 dedup이 스테이징 사본 uri로 폴백하는데, 그 uri는 resolve마다 재생성되므로\n * 같은 사진을 두 번 선택해도 dedup이 통과해버린다.\n */\ndeclare function toPickedAsset(asset: DeviceAsset, resolved: ResolvedDeviceAsset): ResolvedPickedAsset;",
          "sourceDocumentation": "`DeviceAsset`의 정체성(id·filename·치수)과 resolve 결과(바이트 위치·실측 크기·EXIF)를 합쳐\n피커 업로드 경로가 받는 형태를 만든다.\n\n⚠ **`exif: resolved.exif ?? undefined` — null → undefined 정규화가 필수다.**\nEOP(`exactOptionalPropertyTypes`) 하에서 `ResolvedDeviceAsset.exif`는\n`Readonly<Record<string, unknown>> | null`이고 `PickedAsset.exif`는\n`?: Readonly<Record<string, unknown>> | undefined`라 **null을 그대로 대입하면 TS2322**다\n(설계 문서 §5.4-④의 실측 확인). 전신은 `exif: null`을 그대로 흘렸다.\n\n⚠ **`reportedSizeBytes`는 채우지 않는다.** 기기 경로에서 크기의 진실은 `verifiedSizeBytes`\n(= 실제로 스트리밍될 캐시 사본을 stat한 값)뿐이다. 자칭 크기를 여기서 끼워 넣으면\n§7 하드닝 3이 세운 신뢰도 서열(verified > file-system stat > reported)이 무너지고,\nAndroid 재인코딩 자산에서 서버가 업로드를 거절하던 그 사고가 그대로 재발한다.\n\n⚠ **`assetId`를 반드시 채운다.** 소비자의 dedup 1차 키이기 때문이다(§3.3-⑥):\n`assetId`가 없으면 dedup이 스테이징 사본 uri로 폴백하는데, 그 uri는 resolve마다 재생성되므로\n같은 사진을 두 번 선택해도 dedup이 통과해버린다."
        },
        {
          "name": "UploadedPoster",
          "slug": "uploaded-poster",
          "kind": "type",
          "declaration": "/**\n * 구 `posterObjectName` / `posterSizeBytes` 2필드를 쌍 객체로 통합(§6.1-②).\n * 한쪽만 채워 보내면 서버가 반쪽 메타로 등록해 **썸네일이 영구 누락**된다 — 그 상태를\n * 표현 불가능하게 만든다.\n */\ntype UploadedPoster = {\n    readonly objectName: string;\n    readonly sizeBytes: number;\n};",
          "sourceDocumentation": "구 `posterObjectName` / `posterSizeBytes` 2필드를 쌍 객체로 통합(§6.1-②).\n한쪽만 채워 보내면 서버가 반쪽 메타로 등록해 **썸네일이 영구 누락**된다 — 그 상태를\n표현 불가능하게 만든다."
        },
        {
          "name": "UploadResult",
          "slug": "upload-result",
          "kind": "type",
          "declaration": "/**\n * 전신 `CompletedPhotoUpload<TAsset>`.\n * ⚠ `duplicate`는 **필수**다(§6.1-⑯). 옵셔널이면 호스트가 중복 판정을 반환하지 않을 때\n * 킷이 \"새로 만들어졌다\"로 오독하고, 중복 취소 경로가 **사용자의 예전 사진을 지운다**.\n */\ntype UploadResult<TAsset> = {\n    readonly asset: TAsset;\n    readonly duplicate: boolean;\n};",
          "sourceDocumentation": "전신 `CompletedPhotoUpload<TAsset>`.\n⚠ `duplicate`는 **필수**다(§6.1-⑯). 옵셔널이면 호스트가 중복 판정을 반환하지 않을 때\n킷이 \"새로 만들어졌다\"로 오독하고, 중복 취소 경로가 **사용자의 예전 사진을 지운다**."
        },
        {
          "name": "UploadSizeSource",
          "slug": "upload-size-source",
          "kind": "type",
          "declaration": "/** 크기를 어느 근거로 정했는지. 텔레메트리·디버그 로그의 `sizeSource`에 그대로 쓰인다. */\ntype UploadSizeSource = 'verified' | 'file-system' | 'reported';",
          "sourceDocumentation": "크기를 어느 근거로 정했는지. 텔레메트리·디버그 로그의 `sizeSource`에 그대로 쓰인다."
        },
        {
          "name": "VideoContentType",
          "slug": "video-content-type",
          "kind": "type",
          "declaration": "type VideoContentType = Extract<MediaContentType, `video/${string}`>;"
        }
      ]
    },
    {
      "subpath": "./core",
      "id": "core",
      "declarationTarget": "./dist/core.d.cts",
      "symbols": [
        {
          "name": "assertNeverMediaError",
          "slug": "assert-never-media-error",
          "kind": "function",
          "declaration": "/**\n * switch의 `default`에서 호출하면, 라이브러리가 코드를 추가할 때 소비자에게 컴파일 에러가 난다.\n * ⚠ 강제하지 않는다 — 제공만 한다(§6.1-⑫). 코드 추가는 비파괴 변경이어야 하므로\n * 라이브러리가 소비자에게 exhaustive 분기를 요구할 수는 없다.\n */\ndeclare function assertNeverMediaError(code: never): never;",
          "sourceDocumentation": "switch의 `default`에서 호출하면, 라이브러리가 코드를 추가할 때 소비자에게 컴파일 에러가 난다.\n⚠ 강제하지 않는다 — 제공만 한다(§6.1-⑫). 코드 추가는 비파괴 변경이어야 하므로\n라이브러리가 소비자에게 exhaustive 분기를 요구할 수는 없다."
        },
        {
          "name": "BinaryPosterAdapter",
          "slug": "binary-poster-adapter",
          "kind": "interface",
          "declaration": "/** 바이너리 → 포스터 바이너리. 기본 구현은 `\"./web\"`의 canvas 포스터. */\ninterface BinaryPosterAdapter {\n    /** ⚠ 실패는 `null`. 근거는 `LocalPosterAdapter`와 동일. */\n    posterFromBinary(input: {\n        readonly source: BinarySource;\n        readonly atMs: number;\n    }): Promise<BinarySource | null>;\n}",
          "sourceDocumentation": "바이너리 → 포스터 바이너리. 기본 구현은 `\"./web\"`의 canvas 포스터."
        },
        {
          "name": "BinarySource",
          "slug": "binary-source",
          "kind": "interface",
          "declaration": "/**\n * DOM lib 없이 Blob/File을 받기 위한 구조적 최소 타입.\n * 브라우저 Blob·RN Blob·Node Blob이 전부 구조적으로 만족하며,\n * vitest에서 plain object로 전 경로를 도는 것을 가능하게 한다(§10.1).\n */\ninterface BinarySource {\n    readonly size: number;\n    readonly type?: string | undefined;\n    arrayBuffer(): Promise<ArrayBuffer>;\n}",
          "sourceDocumentation": "DOM lib 없이 Blob/File을 받기 위한 구조적 최소 타입.\n브라우저 Blob·RN Blob·Node Blob이 전부 구조적으로 만족하며,\nvitest에서 plain object로 전 경로를 도는 것을 가능하게 한다(§10.1)."
        },
        {
          "name": "BinarySourceLoader",
          "slug": "binary-source-loader",
          "kind": "interface",
          "declaration": "/**\n * uri → 바이너리. 웹 피커가 주는 `blob:`/`data:` URI를 업로드 가능한 소스로 바꾼다.\n * 기본 구현은 `\"./web\"`의 `createFetchBinarySourceLoader`(peer 0). 경로 전문은 §5.7.4.\n *\n * 이 seam이 없으면 웹에서 피커 자산을 업로드하는 유일한 방법이 로컬 파일 스트리밍인데,\n * 그 경로는 웹에 존재하지 않는다 — `LocalUploads.uploadPickedAsset`이\n * `MediaError('platform-unsupported')`를 던지는 이유다.\n */\ninterface BinarySourceLoader {\n    fromUri(input: {\n        readonly uri: string;\n        readonly fileName: string;\n    }): Promise<NamedBinarySource>;\n}",
          "sourceDocumentation": "uri → 바이너리. 웹 피커가 주는 `blob:`/`data:` URI를 업로드 가능한 소스로 바꾼다.\n기본 구현은 `\"./web\"`의 `createFetchBinarySourceLoader`(peer 0). 경로 전문은 §5.7.4.\n\n이 seam이 없으면 웹에서 피커 자산을 업로드하는 유일한 방법이 로컬 파일 스트리밍인데,\n그 경로는 웹에 존재하지 않는다 — `LocalUploads.uploadPickedAsset`이\n`MediaError('platform-unsupported')`를 던지는 이유다."
        },
        {
          "name": "BinaryTransport",
          "slug": "binary-transport",
          "kind": "interface",
          "declaration": "/** 웹 Blob PUT · 포스터 PUT. fetch 기반 기본 구현은 `\"./web\"`이 제공한다. */\ninterface BinaryTransport {\n    putBinary(input: PutRequest & {\n        readonly body: BinarySource;\n    }): Promise<{\n        readonly status: number;\n    }>;\n}",
          "sourceDocumentation": "웹 Blob PUT · 포스터 PUT. fetch 기반 기본 구현은 `\"./web\"`이 제공한다."
        },
        {
          "name": "BinaryUploads",
          "slug": "binary-uploads",
          "kind": "interface",
          "declaration": "interface BinaryUploads<TAsset, TCollectionId extends string = string> {\n    uploadBinary(input: {\n        readonly source: NamedBinarySource;\n        readonly collectionId?: TCollectionId | null | undefined;\n        /**\n         * JPEG APP1 파싱이 실패했을 때 **필드 단위로** 병합될 EXIF(§5.3 `mediaMetadataFromJpeg` 규칙 ②).\n         * 웹 피커 경로가 `asset.exif`를 여기로 넘긴다(전신 uploader.ts:709 · §5.7.4).\n         * 없으면 촬영 시각과 위치가 조용히 유실된다 — 이 인자가 복원된 이유다(G6).\n         */\n        readonly fallbackExif?: Readonly<Record<string, unknown>> | undefined;\n        /**\n         * 동영상 포스터. **3상태를 보존한다**(전신 `BlobVideoUploadInput.posterBlob`, uploader.ts:566-569):\n         *   `undefined` = 어댑터로 자동 추출 / `null` = 포스터 없음(추출 시도 금지) / 값 = 주어진 포스터.\n         * 이 3상태를 2상태로 접으면 \"포스터를 일부러 안 만든다\"를 표현할 방법이 사라진다.\n         */\n        readonly poster?: BinarySource | null | undefined;\n        /**\n         * 동영상 재생시간(**밀리초**). 이미지에는 무시된다.\n         * ⚠ 초 단위를 넣지 마라 — 웹의 `HTMLVideoElement.duration`은 **초**다. 호출자가\n         *   `normalizeDurationMs`(§7 하드닝 4)를 거쳐 넘겨야 한다. 20분 동영상이 1200ms로\n         *   저장되면 어떤 duration 상한도 통과한다. `createPickerFlows`의 웹 경로는 이미 거친다.\n         */\n        readonly durationMs?: number | null | undefined;\n        /**\n         * 동영상 픽셀 치수. 이미지에는 무시된다(이미지 치수는 서버가 바이트에서 읽는다).\n         * ⚠ `BinarySource`만으로는 DOM 없이 복원할 수 없으므로 **호출자가 주지 않으면 영구 유실**이다 —\n         *   전신 `BlobVideoUploadInput.width/height`(uploader.ts:86-87)가 완료 페이로드로 가던 값이다.\n         */\n        readonly dimensions?: {\n            readonly width?: number | null | undefined;\n            readonly height?: number | null | undefined;\n        } | undefined;\n    }): Promise<UploadResult<TAsset>>;\n    /**\n     * 웹 드롭 다건.\n     * ⚠ 첫 presign **이전에** 배치 전체를 검증한다 — 혼합 드롭 부분 업로드 방지(§7 하드닝 10).\n     * ⚠ 검증은 `maxFiles` slice **이후**에 수행한다(전신 uploader.ts:623→630 순서 보존).\n     * 규칙의 거처는 `webBatch.ts`다.\n     */\n    uploadDropped(files: readonly NamedBinarySource[], options?: {\n        readonly collectionId?: TCollectionId | null | undefined;\n        readonly maxFiles?: number | undefined;\n    } | undefined): Promise<readonly UploadResult<TAsset>[]>;\n}"
        },
        {
          "name": "BrowserSaveAdapter",
          "slug": "browser-save-adapter",
          "kind": "interface",
          "declaration": "interface BrowserSaveAdapter {\n    /**\n     * DOM 접근을 어댑터 안에 가둔다. `document`/`fetch`는 `\"./web\"`에서 **필수 주입**(§6.1-⑬) —\n     * 전신은 미주입 시 `globalThis.document`로 폴백해 네이티브에서도 조용히 생성됐다.\n     */\n    saveByDownload(input: {\n        readonly url: string;\n        readonly fileName: string;\n    }): Promise<void>;\n}"
        },
        {
          "name": "capturedAtFromExif",
          "slug": "captured-at-from-exif",
          "kind": "function",
          "declaration": "/**\n * Resolves the preferred EXIF datetime field to an ISO instant.\n *\n * Normal `mediaMetadataFromExif` callers should retain its device-local\n * default. Consumers that know an immutable historical event offset can pass\n * it here and avoid silently interpreting a travel photo in the device's\n * *current* time zone. A datetime which already carries an explicit ISO offset\n * retains that offset; malformed timezone-less EXIF values are rejected.\n */\ndeclare function capturedAtFromExif(exif?: Readonly<Record<string, unknown>> | null, options?: ExifCapturedAtOptions | undefined): string | undefined;",
          "sourceDocumentation": "Resolves the preferred EXIF datetime field to an ISO instant.\n\nNormal `mediaMetadataFromExif` callers should retain its device-local\ndefault. Consumers that know an immutable historical event offset can pass\nit here and avoid silently interpreting a travel photo in the device's\n*current* time zone. A datetime which already carries an explicit ISO offset\nretains that offset; malformed timezone-less EXIF values are rejected."
        },
        {
          "name": "ChunkRange",
          "slug": "chunk-range",
          "kind": "type",
          "declaration": "type ChunkRange = {\n    readonly position: number;\n    readonly length: number;\n};"
        },
        {
          "name": "computeChunkRanges",
          "slug": "compute-chunk-ranges",
          "kind": "function",
          "declaration": "/**\n * `size` 바이트 파일을 순차 `[position, length)` 창으로 나눈다. 순수 함수.\n *\n * ⚠ **`chunkBytes` 인자를 공개하지 않는다**(§6.1-⑩). 전신 `hashFile.ts:18`은 기본 인자로 이것을\n * 열어 두었고, \"3의 배수\"라는 제약은 타입으로 표현할 수 없으므로 그 인자가 곧 하드닝 9의\n * 회귀 통로였다. 실제로 전신 테스트가 `computeChunkRanges(100, 1000)`처럼 3의 배수가 아닌 값을\n * 넘기고 있었다.\n */\ndeclare function computeChunkRanges(size: number): readonly ChunkRange[];",
          "sourceDocumentation": "`size` 바이트 파일을 순차 `[position, length)` 창으로 나눈다. 순수 함수.\n\n⚠ **`chunkBytes` 인자를 공개하지 않는다**(§6.1-⑩). 전신 `hashFile.ts:18`은 기본 인자로 이것을\n열어 두었고, \"3의 배수\"라는 제약은 타입으로 표현할 수 없으므로 그 인자가 곧 하드닝 9의\n회귀 통로였다. 실제로 전신 테스트가 `computeChunkRanges(100, 1000)`처럼 3의 배수가 아닌 값을\n넘기고 있었다."
        },
        {
          "name": "createBinaryUploads",
          "slug": "create-binary-uploads",
          "kind": "function",
          "declaration": "declare function createBinaryUploads<TAsset, TCollectionId extends string = string>(config: MediaUploadConfig<TAsset, TCollectionId> & {\n    readonly transport: BinaryTransport;\n    /** 생략 = core 내장 순수 TS SHA-256(§9). `FileSystemAdapter`가 없으므로 바이너리 경로만 쓴다. */\n    readonly hasher?: HashAdapter | undefined;\n    /** 생략 = 자동 포스터 추출 없음. 호출자가 `poster` 값을 직접 줄 수는 있다. */\n    readonly poster?: BinaryPosterAdapter | undefined;\n    readonly posterAtMs?: number | undefined;\n}): BinaryUploads<TAsset, TCollectionId>;"
        },
        {
          "name": "createDeferredLocalUploads",
          "slug": "create-deferred-local-uploads",
          "kind": "function",
          "declaration": "/**\n * Presign → 네이티브 스트리밍 PUT까지만 수행하고, 나중의 도메인 트랜잭션에 연결할 attachment를 반환한다.\n *\n * `api`에는 `createUploadIntent`만 있으면 된다. 이 함수는 `completeUpload`를 흉내 내거나 호출하지 않는다.\n */\ndeclare function createDeferredLocalUploads<TCollectionId extends string = string>(config: DeferredLocalUploadConfig<TCollectionId>): DeferredLocalUploads<TCollectionId>;",
          "sourceDocumentation": "Presign → 네이티브 스트리밍 PUT까지만 수행하고, 나중의 도메인 트랜잭션에 연결할 attachment를 반환한다.\n\n`api`에는 `createUploadIntent`만 있으면 된다. 이 함수는 `completeUpload`를 흉내 내거나 호출하지 않는다."
        },
        {
          "name": "createDeviceLibrary",
          "slug": "create-device-library",
          "kind": "function",
          "declaration": "declare function createDeviceLibrary(input: {\n    readonly adapter: DeviceLibraryAdapter;\n    readonly files: FileSystemAdapter;\n    /**\n     * ⚠ **필수 인자다**(§3.1 · §7 하드닝 7). 스테이징 사본을 만드는 주체가 지우는 주체를 반드시\n     * 갖게 하려는 것이다. 옵셔널로 두면 \"카피는 하는데 지우는 사람이 없는\" 조립이 컴파일을 통과하고,\n     * 업로드한 모든 사진의 원본 사본이 앱 컨테이너에 영구 축적된다 — 타입도 테스트도 못 잡는다.\n     */\n    readonly staging: StagingCache;\n    readonly platform: PlatformAdapter;\n    readonly strings?: MediaStrings | undefined;\n    readonly debug?: MediaDebugOptions | undefined;\n}): DeviceLibrary;"
        },
        {
          "name": "createDeviceUploads",
          "slug": "create-device-uploads",
          "kind": "function",
          "declaration": "declare function createDeviceUploads<TAsset, TCollectionId extends string = string>(input: {\n    readonly device: DeviceLibrary;\n    readonly uploads: LocalUploads<TAsset, TCollectionId>;\n    readonly staging: StagingCache;\n}): DeviceUploads<TAsset, TCollectionId>;"
        },
        {
          "name": "createDurableFileStore",
          "slug": "create-durable-file-store",
          "kind": "function",
          "declaration": "/**\n * Create a durable, app-owned file store from a host storage adapter.\n *\n * The store never trusts caller-supplied paths as complete URIs. It constructs\n * them from individually validated segments, so a cleanup request cannot turn\n * into deletion of a sibling app file through `..`, encoded separators, or a\n * prefix collision.\n */\ndeclare function createDurableFileStore(input: {\n    readonly root: string;\n    readonly files: DurableFileStoreAdapter;\n}): DurableFileStore;",
          "sourceDocumentation": "Create a durable, app-owned file store from a host storage adapter.\n\nThe store never trusts caller-supplied paths as complete URIs. It constructs\nthem from individually validated segments, so a cleanup request cannot turn\ninto deletion of a sibling app file through `..`, encoded separators, or a\nprefix collision."
        },
        {
          "name": "createFileHasher",
          "slug": "create-file-hasher",
          "kind": "function",
          "declaration": "/**\n * `FileSystemAdapter` 위에 base64 창 스트리밍 해시를 조립한다. 기본 `HashAdapter` 구현이다.\n *\n * 해시는 **원본 바이트**에 대해 계산되므로 같은 기기 사진이면 실행 간에도 값이 안정적이다\n * (전신 주석 보존) — 그것이 dedup 키로 쓸 수 있는 이유다.\n */\ndeclare function createFileHasher(input: {\n    readonly files: FileSystemAdapter;\n}): HashAdapter;",
          "sourceDocumentation": "`FileSystemAdapter` 위에 base64 창 스트리밍 해시를 조립한다. 기본 `HashAdapter` 구현이다.\n\n해시는 **원본 바이트**에 대해 계산되므로 같은 기기 사진이면 실행 간에도 값이 안정적이다\n(전신 주석 보존) — 그것이 dedup 키로 쓸 수 있는 이유다."
        },
        {
          "name": "createLocalMediaSaver",
          "slug": "create-local-media-saver",
          "kind": "function",
          "declaration": "/**\n * Create a saver for files that are already owned by the application.\n *\n * Unlike the remote `createMediaSaver`, this never downloads or deletes source\n * files. It asks the OS for write permission at most once per non-empty batch,\n * and only after confirming that at least one non-empty `file://` source is\n * available.\n */\ndeclare function createLocalMediaSaver(input: {\n    readonly files: LocalMediaSaveFileAdapter;\n    readonly library: MediaLibrarySaveAdapter;\n    readonly strings?: MediaStrings | undefined;\n    readonly telemetry?: MediaTelemetry | undefined;\n}): LocalMediaSaver;",
          "sourceDocumentation": "Create a saver for files that are already owned by the application.\n\nUnlike the remote `createMediaSaver`, this never downloads or deletes source\nfiles. It asks the OS for write permission at most once per non-empty batch,\nand only after confirming that at least one non-empty `file://` source is\navailable."
        },
        {
          "name": "createLocalUploads",
          "slug": "create-local-uploads",
          "kind": "function",
          "declaration": "/** 보통의 presign → PUT → 서버 등록 흐름. */\ndeclare function createLocalUploads<TAsset, TCollectionId extends string = string>(config: MediaUploadConfig<TAsset, TCollectionId> & LocalUploadAdapters): LocalUploads<TAsset, TCollectionId>;",
          "sourceDocumentation": "보통의 presign → PUT → 서버 등록 흐름."
        },
        {
          "name": "createMediaDebugLogger",
          "slug": "create-media-debug-logger",
          "kind": "function",
          "declaration": "/**\n * 게이트: `platform.isDev && platform.os !== 'web'` (전신 `debugEnabled()` 보존).\n *\n * 전신의 세 번째 조건이던 \"테스트 환경 제외\"는 `PlatformAdapter.isDev`가 흡수한다\n * (기본 어댑터가 `__DEV__ && NODE_ENV !== 'test'`로 채운다 — §3.3). 코어에는 그 전역이 없다.\n * `options.enabled`는 호스트의 명시적 스위치이며, 생략하면 전신과 동일하게 플랫폼 게이트만 남는다.\n *\n * ⚠ 게이트가 닫혀 있으면 두 메서드 모두 **완전 no-op**이다 — details를 만드는 비용도 치르지 않는다.\n */\ndeclare function createMediaDebugLogger(input: {\n    readonly platform: PlatformAdapter;\n    readonly options?: MediaDebugOptions | undefined;\n}): MediaDebugLogger;",
          "sourceDocumentation": "게이트: `platform.isDev && platform.os !== 'web'` (전신 `debugEnabled()` 보존).\n\n전신의 세 번째 조건이던 \"테스트 환경 제외\"는 `PlatformAdapter.isDev`가 흡수한다\n(기본 어댑터가 `__DEV__ && NODE_ENV !== 'test'`로 채운다 — §3.3). 코어에는 그 전역이 없다.\n`options.enabled`는 호스트의 명시적 스위치이며, 생략하면 전신과 동일하게 플랫폼 게이트만 남는다.\n\n⚠ 게이트가 닫혀 있으면 두 메서드 모두 **완전 no-op**이다 — details를 만드는 비용도 치르지 않는다."
        },
        {
          "name": "createMediaPickerActions",
          "slug": "create-media-picker-actions",
          "kind": "function",
          "declaration": "/**\n * Build an upload-independent camera and library picker flow.\n *\n * This is the right boundary for app-owned workflows such as crop, image analysis, and OCR:\n * it owns OS permissions and untrusted picker-result normalization, while the app owns what it\n * does with the returned local URI.\n */\ndeclare function createMediaPickerActions(input: {\n    readonly picker: PickerAdapter;\n    readonly strings?: MediaStrings | undefined;\n}): MediaPickerActions;",
          "sourceDocumentation": "Build an upload-independent camera and library picker flow.\n\nThis is the right boundary for app-owned workflows such as crop, image analysis, and OCR:\nit owns OS permissions and untrusted picker-result normalization, while the app owns what it\ndoes with the returned local URI."
        },
        {
          "name": "createMediaSaver",
          "slug": "create-media-saver",
          "kind": "function",
          "declaration": "declare function createMediaSaver(input: {\n    readonly target: SaveTarget;\n    readonly fileNamePrefix?: string | undefined;\n    readonly strings?: MediaStrings | undefined;\n    readonly telemetry?: MediaTelemetry | undefined;\n}): MediaSaver;"
        },
        {
          "name": "createPendingSelection",
          "slug": "create-pending-selection",
          "kind": "function",
          "declaration": "/**\n * Build a pure staged-selection model: dedup by stable identity, a hard cap with per-item\n * rejection reasons, exactly-once preview release, HEIC/HEIF preview gating, and capture-time\n * extraction. State is an immutable ordered list owned by the host (React state, a store, a\n * plain variable) — the selection holds no items itself.\n *\n * The revoke-once registry is shared through a global symbol, so the guarantee holds per item\n * object across selection instances and across the package's entry copies (`.` and `./core`,\n * ESM and CJS) within one realm.\n *\n * @throws MediaError `config-invalid` when `max` is not a positive integer.\n */\ndeclare function createPendingSelection(options: PendingSelectionOptions): PendingSelection;",
          "sourceDocumentation": "Build a pure staged-selection model: dedup by stable identity, a hard cap with per-item\nrejection reasons, exactly-once preview release, HEIC/HEIF preview gating, and capture-time\nextraction. State is an immutable ordered list owned by the host (React state, a store, a\nplain variable) — the selection holds no items itself.\n\nThe revoke-once registry is shared through a global symbol, so the guarantee holds per item\nobject across selection instances and across the package's entry copies (`.` and `./core`,\nESM and CJS) within one realm."
        },
        {
          "name": "createPickerFlows",
          "slug": "create-picker-flows",
          "kind": "function",
          "declaration": "declare function createPickerFlows<TAsset, TCollectionId extends string = string>(input: {\n    readonly picker: PickerAdapter;\n    readonly uploads: LocalUploads<TAsset, TCollectionId>;\n    readonly platform: PlatformAdapter;\n    readonly strings?: MediaStrings | undefined;\n    /**\n     * 웹 경로에서 `asset.fileName`이 없을 때 생성되는 폴백 파일명의 접두사.\n     * 전신은 uploader의 `fileNamePrefix`를 썼다(uploader.ts:701) — 생략하면 `mediaFileName`\n     * 기본값(`'media'`)이 되어 호스트 접두사가 웹 경로에서만 달라진다.\n     * 네이티브 경로는 `uploads`가 이미 자기 접두사를 갖고 있으므로 영향받지 않는다.\n     */\n    readonly fileNamePrefix?: string | undefined;\n    /**\n     * ⚠ `platform.os === 'web'`에서 피커 자산을 업로드하려면 **필수**. 없으면\n     * `MediaError('platform-unsupported')`. 조건부 타입 0 — 런타임 분기다(§3.1).\n     *\n     * 실측 근거(§5.7.4/V9-d): memorylog2 `app/profile-edit.tsx:137`의 `pickAndUploadPhoto()`에는\n     * **플랫폼 게이트가 없다**. 웹에서 그 호출은 전신 uploader.ts:692-710으로 들어가\n     * `fetch(asset.uri)` → Blob → `uploadImageBlob({ fallbackExif: asset.exif })`를 탔다.\n     * 그 경로를 잃지 않으려면 변환 담당자(`loader`)와 업로더(`uploads`)가 여기 주입돼야 한다.\n     */\n    readonly web?: {\n        readonly uploads: BinaryUploads<TAsset, TCollectionId>;\n        readonly loader: BinarySourceLoader;\n    } | undefined;\n}): PickerFlows<TAsset, TCollectionId>;"
        },
        {
          "name": "createSha256",
          "slug": "create-sha256",
          "kind": "function",
          "declaration": "declare function createSha256(): Sha256Hasher;"
        },
        {
          "name": "createStagingCache",
          "slug": "create-staging-cache",
          "kind": "function",
          "declaration": "declare function createStagingCache(input: {\n    /** `/^[a-z0-9][a-z0-9-]{1,30}$/` — 위반 시 `MediaError('config-invalid')`. 부팅 시 즉사. */\n    readonly namespace: string;\n    readonly files: FileSystemAdapter;\n}): StagingCache;"
        },
        {
          "name": "DeferredLocalUpload",
          "slug": "deferred-local-upload",
          "kind": "type",
          "declaration": "/**\n * PUT에 성공한 뒤, 나중의 앱 트랜잭션에 연결할 수 있도록 돌려주는 오브젝트 메타데이터.\n * `MediaUploadCompletion`과 같은 형태를 써서 앱이 별도 변환 없이 자기 API에 전달할 수 있다.\n */\ntype DeferredLocalUpload<TCollectionId extends string = string> = MediaUploadCompletion<TCollectionId>;",
          "sourceDocumentation": "PUT에 성공한 뒤, 나중의 앱 트랜잭션에 연결할 수 있도록 돌려주는 오브젝트 메타데이터.\n`MediaUploadCompletion`과 같은 형태를 써서 앱이 별도 변환 없이 자기 API에 전달할 수 있다."
        },
        {
          "name": "DeferredLocalUploadConfig",
          "slug": "deferred-local-upload-config",
          "kind": "type",
          "declaration": "/**\n * `completeUpload` 없이 presign → 네이티브 PUT까지만 수행하는 설정.\n *\n * 반환된 `DeferredLocalUpload`은 서버 등록 요청처럼 보이게 만든 가짜 값이 아니다. 호출자가\n * 자기 도메인 트랜잭션에서 `objectName`과 메타데이터를 연결할 수 있는 완료 페이로드다.\n */\ntype DeferredLocalUploadConfig<TCollectionId extends string = string> = MediaUploadBaseConfig<TCollectionId> & LocalStreamingUploadAdapters & {\n    readonly api: MediaUploadIntentApi<TCollectionId>;\n};",
          "sourceDocumentation": "`completeUpload` 없이 presign → 네이티브 PUT까지만 수행하는 설정.\n\n반환된 `DeferredLocalUpload`은 서버 등록 요청처럼 보이게 만든 가짜 값이 아니다. 호출자가\n자기 도메인 트랜잭션에서 `objectName`과 메타데이터를 연결할 수 있는 완료 페이로드다."
        },
        {
          "name": "DeferredLocalUploads",
          "slug": "deferred-local-uploads",
          "kind": "interface",
          "declaration": "/** `createDeferredLocalUploads`의 최소 능력. 피커 흐름이나 앱 자산 등록은 의도적으로 포함하지 않는다. */\ninterface DeferredLocalUploads<TCollectionId extends string = string> {\n    uploadLocalFile(input: LocalUploadInput<TCollectionId>): Promise<DeferredLocalUpload<TCollectionId>>;\n}",
          "sourceDocumentation": "`createDeferredLocalUploads`의 최소 능력. 피커 흐름이나 앱 자산 등록은 의도적으로 포함하지 않는다."
        },
        {
          "name": "detectImageContentType",
          "slug": "detect-image-content-type",
          "kind": "function",
          "declaration": "/** 엄격 감지의 이미지 전용 판. 근거는 `detectMediaContentType`와 동일하다. */\ndeclare function detectImageContentType(mime?: string | null, nameOrUri?: string | null): ImageContentType | null;",
          "sourceDocumentation": "엄격 감지의 이미지 전용 판. 근거는 `detectMediaContentType`와 동일하다."
        },
        {
          "name": "detectMediaContentType",
          "slug": "detect-media-content-type",
          "kind": "function",
          "declaration": "/**\n * 엄격 감지 — MIME도 확장자도 지원 형식을 지목하지 못하면 `null`.\n *\n * 전신 주석의 계약 원문:\n *   \"잘못 라벨링하면 데이터가 손상되는 곳(예: 백그라운드 동기화 스캐너)에는 이 엄격 감지를 쓰고,\n *    `infer*` 폴백은 **OS가 종류를 이미 보장하는 곳**(카메라·피커 출력)에만 쓴다.\"\n */\ndeclare function detectMediaContentType(mime?: string | null, nameOrUri?: string | null): MediaContentType | null;",
          "sourceDocumentation": "엄격 감지 — MIME도 확장자도 지원 형식을 지목하지 못하면 `null`.\n\n전신 주석의 계약 원문:\n  \"잘못 라벨링하면 데이터가 손상되는 곳(예: 백그라운드 동기화 스캐너)에는 이 엄격 감지를 쓰고,\n   `infer*` 폴백은 **OS가 종류를 이미 보장하는 곳**(카메라·피커 출력)에만 쓴다.\""
        },
        {
          "name": "DeviceAlbum",
          "slug": "device-album",
          "kind": "type",
          "declaration": "type DeviceAlbum = {\n    readonly id: string;\n    readonly title: string;\n    readonly count: number;\n};"
        },
        {
          "name": "DeviceAsset",
          "slug": "device-asset",
          "kind": "type",
          "declaration": "type DeviceAsset = DeviceAssetRef & {\n    readonly uri: string;\n    readonly width: number;\n    readonly height: number;\n    readonly mediaType: MediaKind;\n    readonly creationTime?: number | undefined;\n};"
        },
        {
          "name": "deviceAssetCapturedAt",
          "slug": "device-asset-captured-at",
          "kind": "function",
          "declaration": "/**\n * 촬영 시각(ms 에폭) → ISO 문자열. 전신 `deviceAssetCapturedAt`(devicePhotoLibrary.ts:372-378).\n *\n * peer 0인 순수 함수이므로 `\"./device\"`가 아니라 `\"./core\"`에 둔다(§5.7.5).\n * ⚠ `creationTime`이 0이면 null이다 — 전신의 falsy 판정을 그대로 보존한다. 에폭 0(1970-01-01)은\n *   기기가 시각을 모른다는 뜻이지 실제 촬영 시각이 아니며, 이것을 ISO로 흘리면 서버의\n *   타임라인 정렬이 1970년으로 끌려간다.\n */\ndeclare function deviceAssetCapturedAt(asset: {\n    readonly creationTime?: number | undefined;\n}): string | null;",
          "sourceDocumentation": "촬영 시각(ms 에폭) → ISO 문자열. 전신 `deviceAssetCapturedAt`(devicePhotoLibrary.ts:372-378).\n\npeer 0인 순수 함수이므로 `\"./device\"`가 아니라 `\"./core\"`에 둔다(§5.7.5).\n⚠ `creationTime`이 0이면 null이다 — 전신의 falsy 판정을 그대로 보존한다. 에폭 0(1970-01-01)은\n  기기가 시각을 모른다는 뜻이지 실제 촬영 시각이 아니며, 이것을 ISO로 흘리면 서버의\n  타임라인 정렬이 1970년으로 끌려간다."
        },
        {
          "name": "DeviceAssetInfo",
          "slug": "device-asset-info",
          "kind": "type",
          "declaration": "type DeviceAssetInfo = {\n    /** 앱 컨테이너 밖(iOS Photos 컨테이너)일 수 있다 — core가 캐시로 실체화한다(§7 하드닝 2). */\n    readonly localUri?: string | undefined;\n    readonly uri?: string | undefined;\n    readonly exif?: Readonly<Record<string, unknown>> | undefined;\n    /** true면 원본이 iCloud에만 있다. core 기본 정책은 여기서 중단(§7 하드닝 6). */\n    readonly isNetworkAsset: boolean;\n};"
        },
        {
          "name": "DeviceAssetPage",
          "slug": "device-asset-page",
          "kind": "type",
          "declaration": "type DeviceAssetPage = {\n    readonly assets: readonly DeviceAsset[];\n    /**\n     * §1-7 규약(`?: T | undefined`)을 따른다 — 초안은 필수-undefined였다(G20-14).\n     * 필수로 두면 3자 어댑터 구현자가 값 없는 마지막 페이지에서도 키를 명시해야 한다.\n     */\n    readonly endCursor?: string | undefined;\n    readonly hasNextPage: boolean;\n    readonly totalCount: number;\n};"
        },
        {
          "name": "DeviceAssetRef",
          "slug": "device-asset-ref",
          "kind": "type",
          "declaration": "/**\n * 하드닝된 업로드 resolve가 필요로 하는 최소 정체성. 전체 `MediaLibrary.Asset`이 이것을\n * 구조적으로 만족하며, 동기화 큐는 이 두 필드만 저장해 두고 넘길 수 있다\n * (전신 devicePhotoLibrary.ts:21-24 주석 계승).\n */\ntype DeviceAssetRef = {\n    readonly id: string;\n    readonly filename: string;\n};",
          "sourceDocumentation": "하드닝된 업로드 resolve가 필요로 하는 최소 정체성. 전체 `MediaLibrary.Asset`이 이것을\n구조적으로 만족하며, 동기화 큐는 이 두 필드만 저장해 두고 넘길 수 있다\n(전신 devicePhotoLibrary.ts:21-24 주석 계승)."
        },
        {
          "name": "DeviceLibrary",
          "slug": "device-library",
          "kind": "interface",
          "declaration": "interface DeviceLibrary {\n    /** 순수 조회. 요청하지 않는다. */\n    getPermission(): Promise<MediaPermission>;\n    /**\n     * 권한 합성 규칙의 **유일한 거처**(§5.4-④(c), G17):\n     * 조회 → `!granted && canAskAgain`일 때만 요청 → `accessPrivileges === 'limited'` 매핑\n     * (마지막 매핑은 어댑터 몫 — §3.3).\n     *\n     * ⚠ raw `requestPermission()`은 **공개하지 않는다**. `canAskAgain`을 무시하는 재요청이\n     * 정확히 iOS UI 데드록(재요청해도 아무 일도 일어나지 않아 화면이 영원히 기다린다)의 원인이며,\n     * 골든패스에 그 문을 두면 반드시 누군가 그 문으로 들어간다. 필요한 소비자는\n     * `DeviceLibraryAdapter.requestPermission()`을 직접 쓴다.\n     */\n    ensurePermission(): Promise<MediaPermission>;\n    fetchPage(input?: {\n        readonly albumId?: string | null | undefined;\n        readonly after?: string | undefined;\n        readonly pageSize?: number | undefined;\n        readonly kinds?: readonly MediaKind[] | undefined;\n    } | undefined): Promise<DeviceAssetPage>;\n    /** ⚠ core가 `count > 0` 필터 + count 내림차순 정렬을 **수행한다**(§5.4-④(d)). */\n    fetchAlbums(): Promise<readonly DeviceAlbum[]>;\n    /**\n     * 하드닝된 자산정보 조회 — 그리드/스캐너/업로드가 공유하는 단일 관문(§5.7.5, G9 승격).\n     * 기본값은 iCloud 다운로드 없음 + 15s 데드라인, 옵트인 시 60s(§7 하드닝 6).\n     * 데드라인 초과는 `MediaError('device-timeout')`이며, 어댑터 실패는 URL·원본 예외를\n     * 신뢰하지 않고 새 `MediaError('device-library-failed')`로 정규화한다.\n     *\n     * ⚠ 이 메서드를 공개하지 않으면 동기화 스캐너가 자체 조회를 짜게 되고, 그 순간 15초 데드라인과\n     * `downloadFromNetwork: false` 기본값이 **스캔 경로에서만** 사라진다 — 하드닝 6의 조용한 절반 소멸.\n     */\n    getAssetInfo(assetId: string, options?: {\n        readonly downloadFromICloud?: boolean | undefined;\n        readonly infoTimeoutMs?: number | undefined;\n        readonly downloadTimeoutMs?: number | undefined;\n    } | undefined): Promise<DeviceAssetInfo>;\n    /**\n     * 원본 바이트 위치만 필요한 경로(동기화 엔진)용. 전신 `resolveDeviceAssetSourceForUpload`.\n     * iCloud 가드 · 이중 타임아웃 · iOS 캐시 실체화(§7 하드닝 2·6).\n     */\n    resolveForUpload(asset: DeviceAssetRef, options?: DeviceResolveOptions | undefined): Promise<ResolvedDeviceAsset>;\n    /**\n     * 화면 경로용. 전신 `resolveDeviceAssetForUpload`(G4).\n     *\n     * ⚠ 후보 목록에 `asset.uri`를 **자동으로 덧붙인다**(전신 devicePhotoLibrary.ts:355-359).\n     *   최종 순서: `[info.localUri, info.uri, asset.uri, ...options.extraCandidates]`\n     *   이 자동 후보가 §7.1 「정보 조회 실패 시 폴백 후보 생존」 규칙을 실제로 발화시키는 값이다 —\n     *   전신 주석 \"the picker keeps the original asset.uri\"(286-288)가 가리키는 정확한 경로다.\n     *   저장된 `asset.uri`는 업로드 파이프라인이 읽을 수 없는 `ph://` 참조일 수 있으므로 원본\n     *   바이트와 EXIF를 다시 해석한 뒤 피커 형태로 넘긴다(전신 341-343 주석).\n     */\n    resolvePickedAsset(asset: DeviceAsset, options?: DeviceResolveOptions | undefined): Promise<ResolvedPickedAsset>;\n}"
        },
        {
          "name": "DeviceLibraryAdapter",
          "slug": "device-library-adapter",
          "kind": "interface",
          "declaration": "interface DeviceLibraryAdapter {\n    /**\n     * ⚠ Android 13+에서 granular 목록(`['photo','video']`)을 **반드시** 지정할 것. 생략하면\n     * 매니페스트의 모든 권한이 대상이 되어, 거부된 `READ_MEDIA_AUDIO`가 유효한 사진·동영상 허용을\n     * 거부처럼 보이게 만든다(Android의 \"선택한 사진\" 접근 모드 포함) — §7 하드닝 5.\n     *\n     * ⚠ **순수 위임이다.** \"언제 요청할 것인가\"(현재 권한 조회 → `!granted && canAskAgain`일 때만\n     * 요청)는 어댑터가 아니라 core가 소유한다 — `DeviceLibrary.ensurePermission()`(§5.4-④(c), G17).\n     * 어댑터에 두면 (i) 3자 어댑터마다 규칙이 갈리고, (ii) iOS에서 `canAskAgain=false`인데 재요청해\n     * 아무 일도 일어나지 않는 **UI 데드록**이 재발한다.\n     * 어댑터의 몫은 네이티브 응답을 `MediaPermission`으로 매핑하는 것까지다\n     * (`accessPrivileges === 'limited'` → `limited: true` 포함).\n     */\n    requestPermission(): Promise<MediaPermission>;\n    /** 순수 조회 — 요청하지 않는다. 합성 규칙은 core가 갖는다(위 참조). */\n    getPermission(): Promise<MediaPermission>;\n    /**\n     * ⚠ **정렬 계약: creationTime 내림차순(최신 우선).** 전신 `devicePhotoLibrary.ts:220`\n     *   `sortBy: [[SortBy.creationTime, false]]`.\n     * core는 재정렬하지 않는다 — 페이지 단위 재정렬은 전역 순서를 보장하지 못하면서\n     * (다음 페이지가 이전 페이지보다 최신일 수 있다) `endCursor`는 여전히 어댑터 순서를 따라가\n     * **커서와 표시 순서가 어긋난다**(§5.4-④(d)). 이 계약을 어기면 그리드 순서와 무한스크롤\n     * 커서가 함께 깨지며, **타입도 가드도 그것을 잡지 못한다**.\n     *\n     * ⚠ 자산별 `getAssetInfo` 호출 금지 — 60개 원본을 직렬 해석하면 페이지당 ~20초다.\n     * 그리드는 raw uri(iOS `ph://`)를 그대로 그린다. 네이티브 이미지 로더가 PHImageManager에\n     * 뷰 크기 썸네일을 요청하므로 시스템 사진 그리드와 같은 방식이 된다. 원본 바이트는\n     * 업로드 시점의 resolve에서만 해석한다(§7.1).\n     */\n    listAssets(input: {\n        readonly albumId?: string | null | undefined;\n        readonly after?: string | undefined;\n        readonly pageSize: number;\n        readonly kinds: readonly MediaKind[];\n    }): Promise<DeviceAssetPage>;\n    /**\n     * 원본 목록을 **그대로** 반환한다 — 필터·정렬 금지.\n     * `count > 0` 필터와 count 내림차순 정렬은 core가 수행하므로\n     * (전신 `devicePhotoLibrary.ts:243-250`의 정책을 core로 승격) 어댑터가 중복 수행할 이유가 없다.\n     * 전량을 한 번에 반환하는 in-memory 목록이라 core가 전역 순서를 보장할 수 있다는 점이\n     * `listAssets`와의 차이다(§5.4-④(d)).\n     */\n    listAlbums(): Promise<readonly DeviceAlbum[]>;\n    /**\n     * ⚠ `downloadFromNetwork`는 **필수 인자**다(§6.1-④). 옵셔널로 두면 어댑터 구현자가\n     * 플랫폼 기본값(Expo legacy API는 `true`)을 흘려 iCloud 원본을 무단으로 셀룰러 다운로드한다 —\n     * 전신이 실제로 겪은 사고다.\n     * 타임아웃(15s / 60s)도 core가 건다 — 어댑터는 순수 위임(§7 하드닝 6).\n     */\n    getAssetInfo(assetId: string, input: {\n        readonly downloadFromNetwork: boolean;\n    }): Promise<DeviceAssetInfo>;\n}"
        },
        {
          "name": "DeviceResolveOptions",
          "slug": "device-resolve-options",
          "kind": "type",
          "declaration": "/**\n * 기기 자산 해석 옵션(§5.4-④).\n *\n * ⚠ `downloadFromICloud` 기본 false — 전신 주석(devicePhotoLibrary.ts:48-50) 그대로다:\n * \"사용자가 직접 시작한 수동 업로드만 iCloud 전용 원본 가져오기에 옵트인할 수 있다.\n *  백그라운드 동기화는 예기치 않은 셀룰러 전송을 절대 시작하지 않도록 기본값 false를 유지한다.\"\n */\ntype DeviceResolveOptions = {\n    readonly downloadFromICloud?: boolean | undefined;\n    readonly onICloudDownload?: ((downloading: boolean) => void) | undefined;\n    readonly extraCandidates?: readonly (string | null | undefined)[] | undefined;\n    readonly infoTimeoutMs?: number | undefined;\n    readonly downloadTimeoutMs?: number | undefined;\n};",
          "sourceDocumentation": "기기 자산 해석 옵션(§5.4-④).\n\n⚠ `downloadFromICloud` 기본 false — 전신 주석(devicePhotoLibrary.ts:48-50) 그대로다:\n\"사용자가 직접 시작한 수동 업로드만 iCloud 전용 원본 가져오기에 옵트인할 수 있다.\n 백그라운드 동기화는 예기치 않은 셀룰러 전송을 절대 시작하지 않도록 기본값 false를 유지한다.\""
        },
        {
          "name": "DeviceUploads",
          "slug": "device-uploads",
          "kind": "interface",
          "declaration": "interface DeviceUploads<TAsset, TCollectionId extends string = string> {\n    /**\n     * ⚠ 순차 실행 고정(위 참조). 업로드 성공·실패와 무관하게 finally에서 `staging.cleanup`을 호출한다 —\n     * 누락하면 업로드한 **모든** 사진의 원본 사본이 앱 컨테이너에 영구 축적된다(§7 하드닝 7).\n     */\n    uploadDeviceAssets(assets: readonly DeviceAssetRef[], options?: (DeviceResolveOptions & {\n        readonly collectionId?: TCollectionId | null | undefined;\n        readonly max?: number | undefined;\n    }) | undefined): Promise<readonly UploadResult<TAsset>[]>;\n}"
        },
        {
          "name": "DURABLE_FILE_ERROR_CODES",
          "slug": "durable-file-error-codes",
          "kind": "constant",
          "declaration": "DURABLE_FILE_ERROR_CODES: readonly [\n    \"durable-file-invalid-path\",\n    \"durable-file-root-unavailable\",\n    \"durable-file-source-required\",\n    \"durable-file-source-not-found\",\n    \"durable-file-destination-exists\",\n    \"durable-file-copy-empty\",\n    \"durable-file-copy-size-mismatch\",\n    \"durable-file-copy-failed\",\n    \"durable-file-unsupported-asset\"\n]",
          "sourceDocumentation": "Durable-file failures are deliberately independent from `MediaError`.\n\nA durable attachment store has no user-facing upload policy, and its host\nadapter can receive local URIs that must never reach an error boundary or a\nlog. Every code below is therefore actionable without exposing a source or\ndestination URI."
        },
        {
          "name": "DurableFile",
          "slug": "durable-file",
          "kind": "type",
          "declaration": "type DurableFile = {\n    readonly uri: string;\n    readonly sizeBytes: number;\n};"
        },
        {
          "name": "DurableFileError",
          "slug": "durable-file-error",
          "kind": "class",
          "declaration": "/** A URI-safe error emitted by durable attachment storage. */\ndeclare class DurableFileError extends Error {\n    readonly code: DurableFileErrorCode;\n    constructor(code: DurableFileErrorCode);\n}",
          "sourceDocumentation": "A URI-safe error emitted by durable attachment storage."
        },
        {
          "name": "DurableFileErrorCode",
          "slug": "durable-file-error-code",
          "kind": "type",
          "declaration": "type DurableFileErrorCode = (typeof DURABLE_FILE_ERROR_CODES)[number];"
        },
        {
          "name": "DurableFileStore",
          "slug": "durable-file-store",
          "kind": "interface",
          "declaration": "interface DurableFileStore {\n    /** True only for a safe, non-empty file path rooted in this store. */\n    owns(uri: string | null | undefined): boolean;\n    /** Atomically-enough copy: a failed, empty, or size-mismatched destination is removed before rejection. */\n    copy(input: DurableFileStoreCopyInput): Promise<DurableFile>;\n    /** Copy a picker result while retaining a supported, validated media extension. */\n    copyPickedAsset(input: DurablePickedAssetCopyInput): Promise<DurablePickedAssetFile>;\n    /** Best-effort no-op for paths outside this store. */\n    remove(uri: string | null | undefined): Promise<void>;\n}"
        },
        {
          "name": "DurableFileStoreAdapter",
          "slug": "durable-file-store-adapter",
          "kind": "interface",
          "declaration": "/**\n * App-owned persistent-file storage seam.\n *\n * This is deliberately separate from `FileSystemAdapter`: upload code needs a\n * cache directory plus base64 reads, whereas an application attachment store\n * needs a durable root plus directory creation. Keeping the contracts apart\n * avoids making every upload-only adapter pretend it can persist user files.\n */\ninterface DurableFileStoreAdapter {\n    /** App-owned durable root URI. It must identify a directory, or return null when unavailable. */\n    rootDirectory(): string | null;\n    /** Creates a directory and all missing parents. Repeating the call must be safe. */\n    ensureDirectory(uri: string): Promise<void>;\n    /** Same discriminated stat contract as the upload file-system seam. */\n    stat(uri: string): Promise<FileStat>;\n    copy(input: {\n        readonly from: string;\n        readonly to: string;\n    }): Promise<void>;\n    /** Idempotent best-effort removal. */\n    remove(uri: string): Promise<void>;\n}",
          "sourceDocumentation": "App-owned persistent-file storage seam.\n\nThis is deliberately separate from `FileSystemAdapter`: upload code needs a\ncache directory plus base64 reads, whereas an application attachment store\nneeds a durable root plus directory creation. Keeping the contracts apart\navoids making every upload-only adapter pretend it can persist user files."
        },
        {
          "name": "DurableFileStoreCopyInput",
          "slug": "durable-file-store-copy-input",
          "kind": "type",
          "declaration": "type DurableFileStoreCopyInput = {\n    readonly sourceUri: string;\n    /** Nested folders below the configured root. Every segment is validated independently. */\n    readonly directory?: readonly string[] | undefined;\n    /** The final file name, including any extension the host chose. */\n    readonly fileName: string;\n};"
        },
        {
          "name": "DurablePickedAssetCopyInput",
          "slug": "durable-picked-asset-copy-input",
          "kind": "type",
          "declaration": "/**\n * Copy a picker result with a caller-owned stable filename stem.\n *\n * The source filename is metadata, not a safe destination path. The store\n * validates `fileNameStem` as a path segment and appends an extension from the\n * selected asset's supported content type. This prevents an app from silently\n * relabeling every picked asset as `.jpg` while retaining its own stable ID.\n */\ntype DurablePickedAssetCopyInput = {\n    readonly asset: PickedAsset;\n    /** Nested folders below the configured root. Every segment is validated independently. */\n    readonly directory?: readonly string[] | undefined;\n    /** Safe, caller-owned destination filename stem; the store adds the validated extension. */\n    readonly fileNameStem: string;\n};",
          "sourceDocumentation": "Copy a picker result with a caller-owned stable filename stem.\n\nThe source filename is metadata, not a safe destination path. The store\nvalidates `fileNameStem` as a path segment and appends an extension from the\nselected asset's supported content type. This prevents an app from silently\nrelabeling every picked asset as `.jpg` while retaining its own stable ID."
        },
        {
          "name": "DurablePickedAssetFile",
          "slug": "durable-picked-asset-file",
          "kind": "type",
          "declaration": "type DurablePickedAssetFile = DurableFile & {\n    readonly fileName: string;\n    readonly contentType: MediaContentType;\n};"
        },
        {
          "name": "enMediaStrings",
          "slug": "en-media-strings",
          "kind": "constant",
          "declaration": "enMediaStrings: MediaStrings",
          "sourceDocumentation": "기본값. 팩토리에 `strings`를 주지 않은 소비자가 받는 문구다."
        },
        {
          "name": "ExifCapturedAtOptions",
          "slug": "exif-captured-at-options",
          "kind": "type",
          "declaration": "type ExifCapturedAtOptions = {\n    /**\n     * Offset saved with the event in minutes east of UTC (KST = 540). When this\n     * is omitted, the historical device-local behaviour of\n     * `mediaMetadataFromExif` is preserved.\n     */\n    readonly timeZoneOffsetMinutes?: number | undefined;\n};"
        },
        {
          "name": "ExifWallClock",
          "slug": "exif-wall-clock",
          "kind": "type",
          "declaration": "/**\n * A timezone-less EXIF wall clock. EXIF itself carries no offset, so this is\n * deliberately not a `Date`: callers must choose the policy for mapping it to\n * an instant.\n */\ntype ExifWallClock = {\n    readonly year: number;\n    readonly month: number;\n    readonly day: number;\n    readonly hour: number;\n    readonly minute: number;\n    readonly second: number;\n    readonly millisecond: number;\n};",
          "sourceDocumentation": "A timezone-less EXIF wall clock. EXIF itself carries no offset, so this is\ndeliberately not a `Date`: callers must choose the policy for mapping it to\nan instant."
        },
        {
          "name": "extensionForContentType",
          "slug": "extension-for-content-type",
          "kind": "function",
          "declaration": "/** 전신 `fileExtensionForContentType`. 정규 확장자를 **점 없이** 반환한다. */\ndeclare function extensionForContentType(contentType: MediaContentType): string;",
          "sourceDocumentation": "전신 `fileExtensionForContentType`. 정규 확장자를 **점 없이** 반환한다."
        },
        {
          "name": "FileDownloadAdapter",
          "slug": "file-download-adapter",
          "kind": "interface",
          "declaration": "/** 저장 플로우 전용 — 업로드만 하는 소비자는 구현할 필요가 없다. */\ninterface FileDownloadAdapter {\n    download(input: {\n        readonly url: string;\n        readonly to: string;\n    }): Promise<{\n        readonly uri: string;\n        readonly status: number;\n    }>;\n}",
          "sourceDocumentation": "저장 플로우 전용 — 업로드만 하는 소비자는 구현할 필요가 없다."
        },
        {
          "name": "FileStat",
          "slug": "file-stat",
          "kind": "type",
          "declaration": "/**\n * 전신의 `{exists, isDirectory, size}`를 판별 유니언으로 교체.\n * `info.exists && !info.isDirectory ? info.size : 0` 패턴 5중복이 좁히기 한 번으로 소멸한다\n * (§7 하드닝 3).\n */\ntype FileStat = {\n    readonly kind: 'file';\n    readonly sizeBytes: number;\n} | {\n    readonly kind: 'directory';\n} | {\n    readonly kind: 'missing';\n};",
          "sourceDocumentation": "전신의 `{exists, isDirectory, size}`를 판별 유니언으로 교체.\n`info.exists && !info.isDirectory ? info.size : 0` 패턴 5중복이 좁히기 한 번으로 소멸한다\n(§7 하드닝 3)."
        },
        {
          "name": "FileSystemAdapter",
          "slug": "file-system-adapter",
          "kind": "interface",
          "declaration": "interface FileSystemAdapter {\n    /** 앱 소유 캐시 디렉토리 URI(끝에 '/'). 없으면 documentDirectory, 그것도 없으면 null. */\n    cacheDirectory(): string | null;\n    /** ⚠ throw 금지 — 코어가 후보 URI를 순회한다. 없거나 디렉토리면 그 kind를 반환. */\n    stat(uri: string): Promise<FileStat>;\n    copy(input: {\n        readonly from: string;\n        readonly to: string;\n    }): Promise<void>;\n    /** 멱등 삭제. 실패해도 throw 금지 — 스테이징 누수는 디스크 비용일 뿐이다. */\n    remove(uri: string): Promise<void>;\n    /**\n     * `[position, position+length)` 구간을 base64로 반환.\n     * ⚠ 코어는 length를 **항상 3의 배수**로 준다(§7 하드닝 9 — 3바이트가 base64 4문자에 대응하므로\n     * 3의 배수가 아니면 윈도우 경계에 패딩이 끼어 **해시가 조용히 틀린다**).\n     * 어댑터가 범위를 재정렬·병합·확장하면 그 하드닝이 무력화된다.\n     */\n    readBase64(uri: string, range: ChunkRange): Promise<string>;\n}"
        },
        {
          "name": "GeoPoint",
          "slug": "geo-point",
          "kind": "type",
          "declaration": "/** 전신 `PhotoGeoPoint`. */\ntype GeoPoint = {\n    readonly latitude: number;\n    readonly longitude: number;\n};",
          "sourceDocumentation": "전신 `PhotoGeoPoint`."
        },
        {
          "name": "HASH_CHUNK_BYTES",
          "slug": "hash-chunk-bytes",
          "kind": "constant",
          "declaration": "HASH_CHUNK_BYTES: number",
          "sourceDocumentation": "읽기 창 크기(바이트).\n\n⚠ **반드시 3의 배수여야 한다**(§7 하드닝 9). 3바이트가 base64 4문자에 대응하므로, 3의 배수가\n아니면 창 경계에 패딩(`=`)이 끼어 디코딩된 바이트열이 원본과 어긋난다 — **해시가 조용히 틀린다**.\n서버와 클라이언트의 dedup 키가 달라질 뿐 어떤 예외도 나지 않으므로 붙잡을 방법이 없다.\n`hardening-guard`가 `HASH_CHUNK_BYTES % 3 === 0`을 정적으로 못 박는다."
        },
        {
          "name": "HashAdapter",
          "slug": "hash-adapter",
          "kind": "interface",
          "declaration": "/**\n * 기본 구현은 core 내장 순수 TS 증분 SHA-256(§9) — `js-sha256` 제거, 런타임 의존성 0.\n * 네이티브 가속이 필요한 호스트만 교체한다.\n *\n * ⚠ 해시는 dedup **최적화**일 뿐이므로 실패가 업로드를 막지 않는다(§7.1). 그 정책은 코어의\n * `hashSafely()`가 갖는다 — 어댑터는 그냥 throw해도 된다.\n */\ninterface HashAdapter {\n    hashLocalFile(uri: string): Promise<string>;\n    hashBinary(source: BinarySource): Promise<string>;\n}",
          "sourceDocumentation": "기본 구현은 core 내장 순수 TS 증분 SHA-256(§9) — `js-sha256` 제거, 런타임 의존성 0.\n네이티브 가속이 필요한 호스트만 교체한다.\n\n⚠ 해시는 dedup **최적화**일 뿐이므로 실패가 업로드를 막지 않는다(§7.1). 그 정책은 코어의\n`hashSafely()`가 갖는다 — 어댑터는 그냥 throw해도 된다."
        },
        {
          "name": "ImageContentType",
          "slug": "image-content-type",
          "kind": "type",
          "declaration": "type ImageContentType = Extract<MediaContentType, `image/${string}`>;"
        },
        {
          "name": "inferImageContentType",
          "slug": "infer-image-content-type",
          "kind": "function",
          "declaration": "/**\n * 전신 `inferContentType`. 폴백은 `image/jpeg`.\n *\n * ⚠ **반환값에 분기가 걸린다** — 호스트의 HEIC/HEIF 프리뷰 분기가 이 값을 읽는다\n * (`pendingPhotos.ts:56,131,143`). 폴백을 바꾸면 프리뷰 경로가 조용히 달라진다.\n */\ndeclare function inferImageContentType(mime?: string | null, nameOrUri?: string | null): ImageContentType;",
          "sourceDocumentation": "전신 `inferContentType`. 폴백은 `image/jpeg`.\n\n⚠ **반환값에 분기가 걸린다** — 호스트의 HEIC/HEIF 프리뷰 분기가 이 값을 읽는다\n(`pendingPhotos.ts:56,131,143`). 폴백을 바꾸면 프리뷰 경로가 조용히 달라진다."
        },
        {
          "name": "inferMediaContentType",
          "slug": "infer-media-content-type",
          "kind": "function",
          "declaration": "/** 관대 추론 — 항상 값을 준다. mime이 미디어를 지목하면 그것을, 아니면 이미지 폴백. */\ndeclare function inferMediaContentType(mime?: string | null, nameOrUri?: string | null): MediaContentType;",
          "sourceDocumentation": "관대 추론 — 항상 값을 준다. mime이 미디어를 지목하면 그것을, 아니면 이미지 폴백."
        },
        {
          "name": "isDurableFileError",
          "slug": "is-durable-file-error",
          "kind": "function",
          "declaration": "/** Cross-entry-safe `DurableFileError` guard. */\ndeclare function isDurableFileError(error: unknown): error is DurableFileError;",
          "sourceDocumentation": "Cross-entry-safe `DurableFileError` guard."
        },
        {
          "name": "isMediaError",
          "slug": "is-media-error",
          "kind": "function",
          "declaration": "/**\n * `instanceof` 대신 이것을 쓴다(§5.2). 엔트리마다 복제된 코어 사본이 만든 에러도 인식한다.\n */\ndeclare function isMediaError(error: unknown): error is MediaError;",
          "sourceDocumentation": "`instanceof` 대신 이것을 쓴다(§5.2). 엔트리마다 복제된 코어 사본이 만든 에러도 인식한다."
        },
        {
          "name": "isPhotoKitUri",
          "slug": "is-photo-kit-uri",
          "kind": "function",
          "declaration": "/** iOS 사진 보관함 자산 URI인가. 하드닝 2의 `ph://` 후보 스킵 술어가 이것을 쓴다. */\ndeclare function isPhotoKitUri(uri?: string | null): boolean;",
          "sourceDocumentation": "iOS 사진 보관함 자산 URI인가. 하드닝 2의 `ph://` 후보 스킵 술어가 이것을 쓴다."
        },
        {
          "name": "isSupportedImageFile",
          "slug": "is-supported-image-file",
          "kind": "function",
          "declaration": "declare function isSupportedImageFile(file: NamedFileLike): boolean;"
        },
        {
          "name": "isSupportedMediaFile",
          "slug": "is-supported-media-file",
          "kind": "function",
          "declaration": "declare function isSupportedMediaFile(file: NamedFileLike): boolean;"
        },
        {
          "name": "isSupportedVideoFile",
          "slug": "is-supported-video-file",
          "kind": "function",
          "declaration": "declare function isSupportedVideoFile(file: NamedFileLike): boolean;"
        },
        {
          "name": "koMediaStrings",
          "slug": "ko-media-strings",
          "kind": "constant",
          "declaration": "koMediaStrings: MediaStrings",
          "sourceDocumentation": "전신 문구를 **원문 그대로** 이식 — memorylog2 이관 시 UI 회귀 0(§11).\n한 글자라도 바꾸면 사용자가 보던 문구가 바뀐다. 개선은 호스트가 스프레드로 한다."
        },
        {
          "name": "LocalFileTransport",
          "slug": "local-file-transport",
          "kind": "interface",
          "declaration": "/**\n * ⚠ **계약: 파일 바이트를 JS 힙으로 읽지 말 것.** 네이티브 스트리밍 업로드여야 한다.\n *\n * 근거(§7 하드닝 1): `FileSystem.uploadAsync`(레거시 URLSession 브리지)가 iOS 26에서 파일 기반\n * 업로드를 **시작하는 중 프로세스를 종료시킨다** — promise가 reject될 기회조차 없으므로\n * 재시도 로직도 에러 보고도 발화하지 않는다. 크래시 리포트에도 앱 코드 프레임이 남지 않아\n * 원인 추적에만 수일이 걸렸다.\n *\n * expo 기본 어댑터는 `new File(uri).upload(url, { sessionType: 'foreground',\n * uploadType: BINARY_CONTENT })`를 쓴다. `hardening-guard`가 `uploadAsync` 문자열의\n * 재등장을 `src/**` 전역에서 정적 차단한다.\n */\ninterface LocalFileTransport {\n    putLocalFile(input: PutRequest & {\n        readonly uri: string;\n    }): Promise<{\n        readonly status: number;\n    }>;\n}",
          "sourceDocumentation": "⚠ **계약: 파일 바이트를 JS 힙으로 읽지 말 것.** 네이티브 스트리밍 업로드여야 한다.\n\n근거(§7 하드닝 1): `FileSystem.uploadAsync`(레거시 URLSession 브리지)가 iOS 26에서 파일 기반\n업로드를 **시작하는 중 프로세스를 종료시킨다** — promise가 reject될 기회조차 없으므로\n재시도 로직도 에러 보고도 발화하지 않는다. 크래시 리포트에도 앱 코드 프레임이 남지 않아\n원인 추적에만 수일이 걸렸다.\n\nexpo 기본 어댑터는 `new File(uri).upload(url, { sessionType: 'foreground',\nuploadType: BINARY_CONTENT })`를 쓴다. `hardening-guard`가 `uploadAsync` 문자열의\n재등장을 `src/**` 전역에서 정적 차단한다."
        },
        {
          "name": "LocalMediaSaveFailureCode",
          "slug": "local-media-save-failure-code",
          "kind": "type",
          "declaration": "/**\n * The only per-item failures this local saver exposes. These are batch\n * outcomes, rather than thrown `MediaError`s: one bad file must not stop later\n * files from being saved. Adapter errors and URIs never escape.\n */\ntype LocalMediaSaveFailureCode = 'save-local-file-unavailable' | 'save-local-file-failed';",
          "sourceDocumentation": "The only per-item failures this local saver exposes. These are batch\noutcomes, rather than thrown `MediaError`s: one bad file must not stop later\nfiles from being saved. Adapter errors and URIs never escape."
        },
        {
          "name": "LocalMediaSaveFileAdapter",
          "slug": "local-media-save-file-adapter",
          "kind": "interface",
          "declaration": "/**\n * The local save path only needs an existence/size check. Requiring the full\n * upload `FileSystemAdapter` would make a save-only consumer implement unused\n * cache, copy, base64-read, and download capabilities.\n */\ninterface LocalMediaSaveFileAdapter {\n    stat(uri: string): Promise<FileStat>;\n}",
          "sourceDocumentation": "The local save path only needs an existence/size check. Requiring the full\nupload `FileSystemAdapter` would make a save-only consumer implement unused\ncache, copy, base64-read, and download capabilities."
        },
        {
          "name": "LocalMediaSaveItemResult",
          "slug": "local-media-save-item-result",
          "kind": "type",
          "declaration": "type LocalMediaSaveItemResult = {\n    readonly index: number;\n    readonly id?: string | undefined;\n    readonly status: 'saved';\n} | {\n    readonly index: number;\n    readonly id?: string | undefined;\n    readonly status: 'unavailable';\n    readonly errorCode: 'save-local-file-unavailable';\n} | {\n    readonly index: number;\n    readonly id?: string | undefined;\n    readonly status: 'failed';\n    readonly errorCode: 'save-local-file-failed';\n};"
        },
        {
          "name": "LocalMediaSaver",
          "slug": "local-media-saver",
          "kind": "interface",
          "declaration": "interface LocalMediaSaver {\n    /**\n     * Saves app-owned local files in input order. A missing/empty source and an\n     * OS save failure are reported per item so one bad item never prevents later\n     * files from being attempted. A write-permission failure rejects once with a\n     * user-safe `MediaError('save-permission-denied')` before any save attempt.\n     */\n    saveLocalToDevice(media: readonly LocalSaveableMedia[]): Promise<LocalMediaSaveResult>;\n}"
        },
        {
          "name": "LocalMediaSaveResult",
          "slug": "local-media-save-result",
          "kind": "type",
          "declaration": "type LocalMediaSaveResult = {\n    /** Input order is preserved, including unavailable sources. */\n    readonly items: readonly LocalMediaSaveItemResult[];\n    readonly savedCount: number;\n    readonly unavailableCount: number;\n    readonly failedCount: number;\n};"
        },
        {
          "name": "LocalPosterAdapter",
          "slug": "local-poster-adapter",
          "kind": "interface",
          "declaration": "/**\n * 로컬 URI → 포스터 프레임. 기본 구현은 `\"./video\"`(expo-video-thumbnails).\n * `BinaryPosterAdapter`와 입력 타입이 다르므로 자리를 바꿔 끼우면 컴파일 에러가 난다 —\n * 브랜드·phantom 각인 없이 오배치가 차단된다.\n */\ninterface LocalPosterAdapter {\n    /** ⚠ 실패는 `null`. 포스터 실패가 동영상 업로드를 막지 않는다(§7.1). */\n    posterFromLocalFile(input: {\n        readonly uri: string;\n        readonly atMs: number;\n    }): Promise<{\n        readonly uri: string;\n    } | null>;\n}",
          "sourceDocumentation": "로컬 URI → 포스터 프레임. 기본 구현은 `\"./video\"`(expo-video-thumbnails).\n`BinaryPosterAdapter`와 입력 타입이 다르므로 자리를 바꿔 끼우면 컴파일 에러가 난다 —\n브랜드·phantom 각인 없이 오배치가 차단된다."
        },
        {
          "name": "LocalSaveableMedia",
          "slug": "local-saveable-media",
          "kind": "type",
          "declaration": "/** A host-owned local artifact. `id` is only returned for caller-side correlation. */\ntype LocalSaveableMedia = {\n    readonly uri: string;\n    readonly id?: string | undefined;\n};",
          "sourceDocumentation": "A host-owned local artifact. `id` is only returned for caller-side correlation."
        },
        {
          "name": "LocalUploadInput",
          "slug": "local-upload-input",
          "kind": "type",
          "declaration": "type LocalUploadInput<TCollectionId extends string = string> = {\n    readonly uri: string;\n    readonly fileName?: string | undefined;\n    readonly contentType?: MediaContentType | undefined;\n    readonly sizeBytes?: number | undefined;\n    /**\n     * ⚠ **주어지면 hasher를 호출하지 않는다**(§7.1 신설 행). 전신 `uploadLocalUriToIntent`는 해시를\n     * 계산하지 않고 호출자 값을 그대로 전달했다(uploader.ts:57-69의 필드 정의, 440의 전달).\n     * 동기화 큐가 재시도 간 해시를 캐시하기 때문이다:\n     *   `src/sync/uploadAsset.ts:45-46` — \"Reuse the cached hash across retries; only compute\n     *   on first attempt\" / `item.contentHash ?? (await hash(source.uri))`.\n     * 이 필드가 없으면 재시도마다 15MB 파일을 다시 해시한다(순수 TS SHA-256 위에서는 §12-3의\n     * Hermes 성능 리스크와 곱해진다).\n     *\n     * ⚠ 바로 아래 `hashSafely`의 \"해시 실패는 업로드를 막지 않는다\"와 **나란히** 읽어야 한다 —\n     *    한쪽만 보면 정반대 구현이 나온다(§7.1의 경고 그대로).\n     */\n    readonly contentHash?: string | undefined;\n    readonly collectionId?: TCollectionId | null | undefined;\n    readonly photo?: MediaMetadata | undefined;\n    readonly durationMs?: number | undefined;\n    readonly width?: number | undefined;\n    readonly height?: number | undefined;\n};"
        },
        {
          "name": "LocalUploads",
          "slug": "local-uploads",
          "kind": "interface",
          "declaration": "interface LocalUploads<TAsset, TCollectionId extends string = string> {\n    uploadLocalFile(input: LocalUploadInput<TCollectionId>): Promise<UploadResult<TAsset>>;\n    /**\n     * 피커 자산 1건 — `PickerFlows`·`DeviceUploads`가 위임한다.\n     * ⚠ **네이티브 전용이다.** `platform.os === 'web'`이면 `MediaError('platform-unsupported')`\n     * — 로컬 파일 스트리밍은 웹에 존재하지 않는다. 웹 피커 자산의 정본 경로는 §5.7.4다.\n     */\n    uploadPickedAsset(asset: PickedAsset, options?: {\n        readonly collectionId?: TCollectionId | null | undefined;\n    } | undefined): Promise<UploadResult<TAsset>>;\n}"
        },
        {
          "name": "MEDIA_CONTENT_TYPES",
          "slug": "media-content-types",
          "kind": "constant",
          "declaration": "MEDIA_CONTENT_TYPES: readonly MediaContentType[]",
          "sourceDocumentation": "지원 콘텐츠 타입 전량.\n\n§5.7.2-③이 내부화한 `IMAGE_CONTENT_TYPES`/`VIDEO_CONTENT_TYPES`의 **동치 공개 대체**가\n`MEDIA_CONTENT_TYPES.filter((t) => mediaKindOf(t) === 'image')`이므로, 그 판정이 참이 되려면\n이 배열이 공개돼 있어야 한다."
        },
        {
          "name": "MEDIA_ERROR_CODES",
          "slug": "media-error-codes",
          "kind": "constant",
          "declaration": "MEDIA_ERROR_CODES: readonly [\n    \"device-timeout\",\n    \"device-icloud-only\",\n    \"device-not-found\",\n    \"device-library-failed\",\n    \"picker-failed\",\n    \"image-processing-failed\",\n    \"unsupported-file-type\",\n    \"file-too-large\",\n    \"upload-failed\",\n    \"save-permission-denied\",\n    \"save-download-failed\",\n    \"permission-denied\",\n    \"poster-upload-failed\",\n    \"no-media-selected\",\n    \"picked-asset-invalid\",\n    \"config-invalid\",\n    \"platform-unsupported\"\n]",
          "sourceDocumentation": "⚠ **순서·문자열이 계약이다.** 소비자가 `Set<MediaErrorCode>`(예: memorylog2의\n`ACTIONABLE_ERROR_CODES`)를 리터럴로 만들어 두고 분기하므로 rename = 파괴적 변경이다.\n유니언을 넓히는 것(추가)은 비파괴이므로 minor로 가능하다."
        },
        {
          "name": "MEDIA_FILE_EXTENSIONS",
          "slug": "media-file-extensions",
          "kind": "constant",
          "declaration": "MEDIA_FILE_EXTENSIONS: Readonly<Record<MediaContentType, readonly string[]>>",
          "sourceDocumentation": "확장자↔MIME 단일 테이블.\n\n공개하는 이유(§5.7.2-③): 호스트가 \"지원 형식\" 안내 문구나 파일 입력의 `accept` 속성을 그리려면\n이 테이블이 필요하다. 숨기면 3자 소비자가 재구현하고, 재구현본은 라이브러리가 형식을 추가할 때\n조용히 어긋난다.\n\n⚠ 각 값의 **첫 원소가 정규 확장자**다(`extensionForContentType`). 순서를 바꾸면 저장 파일명과\n스테이징 사본 이름이 함께 바뀐다."
        },
        {
          "name": "MEDIA_OPERATIONS",
          "slug": "media-operations",
          "kind": "constant",
          "declaration": "MEDIA_OPERATIONS: readonly [\n    \"media.upload.native\",\n    \"media.upload.web-image\",\n    \"media.upload.web-video\",\n    \"media.upload.poster.native\",\n    \"media.upload.poster.web\",\n    \"media.save-to-device\"\n]",
          "sourceDocumentation": "안정적 dotted operation 이름.\n\n⚠ **값 변경 = 소비자 대시보드·알림 규칙 파손**이므로 하드닝과 동급으로 보존한다(§7.2).\n§7.2의 unit 1번이 이 배열을 **인라인 리터럴로 단언**한다 — 스냅샷은 `-u`로 조용히 갱신되므로\n스냅샷을 쓰지 않는다."
        },
        {
          "name": "MediaActivity",
          "slug": "media-activity",
          "kind": "interface",
          "declaration": "/**\n * 하나의 스팬. 정확히 한 번만 종료되어야 한다(라이브러리 내부 규율 — 소비자 검증 대상 아님).\n *\n * `cancel`은 \"실패가 아닌 중단\"이다: 빈 포스터(전신 uploader.ts:268-271)처럼 사용자에게\n * 오류로 보고하면 안 되지만 성공으로 세어서도 안 되는 **3번째 종료 상태**다.\n * 이 상태가 없으면 포스터 추출이 빈 결과를 낸 경우가 성공률 지표에 섞여 들어간다.\n */\ninterface MediaActivity {\n    succeed(finish?: MediaActivityFinish | undefined): void;\n    fail(error: unknown, finish?: MediaActivityFinish | undefined): void;\n    cancel(finish?: MediaActivityFinish | undefined): void;\n}",
          "sourceDocumentation": "하나의 스팬. 정확히 한 번만 종료되어야 한다(라이브러리 내부 규율 — 소비자 검증 대상 아님).\n\n`cancel`은 \"실패가 아닌 중단\"이다: 빈 포스터(전신 uploader.ts:268-271)처럼 사용자에게\n오류로 보고하면 안 되지만 성공으로 세어서도 안 되는 **3번째 종료 상태**다.\n이 상태가 없으면 포스터 추출이 빈 결과를 낸 경우가 성공률 지표에 섞여 들어간다."
        },
        {
          "name": "MediaActivityFinish",
          "slug": "media-activity-finish",
          "kind": "type",
          "declaration": "type MediaActivityFinish = {\n    readonly extra?: Readonly<Record<string, unknown>> | undefined;\n};"
        },
        {
          "name": "MediaContentType",
          "slug": "media-content-type",
          "kind": "type",
          "declaration": "/**\n * ⚠ **8종 고정 — 전신 `MEDIA_FILE_EXTENSIONS`(mediaTypes.ts:7-16)와 정확히 동일하다.**\n * 초안이 신설했던 `image/gif`는 **제거**했다(G15 확정, §5.1):\n *   ① 전신 확장자 테이블에 gif 항목이 없다 → §7의 \"전신 168줄 그대로\" 문구가 참이 된다.\n *   ② 호스트(memorylog2)의 `SUPPORTED_MEDIA_CONTENT_TYPES`에도 없고 서버 zod가 그 목록으로\n *      정규식 검증을 한다 → gif를 통과시키면 **presign 단계에서 서버가 거절**하는\n *      클라이언트/서버 불일치가 생긴다. 클라이언트가 서버보다 넓은 유니언을 갖는 것은 순손실이다.\n * 형식을 넓히려면 서버 유니언을 먼저 넓히고 minor로 추가한다(유니언 확장은 소비자에게 비파괴).\n */\ntype MediaContentType = 'image/jpeg' | 'image/png' | 'image/webp' | 'image/heic' | 'image/heif' | 'video/mp4' | 'video/quicktime' | 'video/webm';",
          "sourceDocumentation": "⚠ **8종 고정 — 전신 `MEDIA_FILE_EXTENSIONS`(mediaTypes.ts:7-16)와 정확히 동일하다.**\n초안이 신설했던 `image/gif`는 **제거**했다(G15 확정, §5.1):\n  ① 전신 확장자 테이블에 gif 항목이 없다 → §7의 \"전신 168줄 그대로\" 문구가 참이 된다.\n  ② 호스트(memorylog2)의 `SUPPORTED_MEDIA_CONTENT_TYPES`에도 없고 서버 zod가 그 목록으로\n     정규식 검증을 한다 → gif를 통과시키면 **presign 단계에서 서버가 거절**하는\n     클라이언트/서버 불일치가 생긴다. 클라이언트가 서버보다 넓은 유니언을 갖는 것은 순손실이다.\n형식을 넓히려면 서버 유니언을 먼저 넓히고 minor로 추가한다(유니언 확장은 소비자에게 비파괴)."
        },
        {
          "name": "MediaDebugLogger",
          "slug": "media-debug-logger",
          "kind": "interface",
          "declaration": "/**\n * 전신 `PhotoDebugLogger`(debug.ts:39-47)를 그대로 계승한다. 초안은 반환 타입만 있고 멤버가 없어\n * 구현자가 임의로 정할 수 있었다 — 그러면 하드닝 8의 새니타이즈 지점이 구현마다 달라진다(G14).\n */\ninterface MediaDebugLogger {\n    log(event: string, details?: Readonly<Record<string, unknown>> | undefined): void;\n    /** `errorName` + **새니타이즈된** `errorMessage`를 details에 병합해 기록한다(전신 동작 보존). */\n    error(event: string, error: unknown, details?: Readonly<Record<string, unknown>> | undefined): void;\n}",
          "sourceDocumentation": "전신 `PhotoDebugLogger`(debug.ts:39-47)를 그대로 계승한다. 초안은 반환 타입만 있고 멤버가 없어\n구현자가 임의로 정할 수 있었다 — 그러면 하드닝 8의 새니타이즈 지점이 구현마다 달라진다(G14)."
        },
        {
          "name": "MediaDebugOptions",
          "slug": "media-debug-options",
          "kind": "type",
          "declaration": "/**\n * 디버그 로거 설정. 전신 `PhotoUploaderConfig`의 `debugTag`/`debugContext`에 1:1 대응한다.\n * ⚠ core에는 `__DEV__`가 없으므로 기본값은 **비활성**이다. 실제 게이트는\n * `createMediaDebugLogger`가 `platform.isDev && platform.os !== 'web'`로 건다(§7 하드닝 8).\n */\ntype MediaDebugOptions = {\n    readonly enabled: boolean;\n    readonly tag?: string | undefined;\n    readonly context?: (() => Record<string, unknown>) | undefined;\n};",
          "sourceDocumentation": "디버그 로거 설정. 전신 `PhotoUploaderConfig`의 `debugTag`/`debugContext`에 1:1 대응한다.\n⚠ core에는 `__DEV__`가 없으므로 기본값은 **비활성**이다. 실제 게이트는\n`createMediaDebugLogger`가 `platform.isDev && platform.os !== 'web'`로 건다(§7 하드닝 8)."
        },
        {
          "name": "mediaDownloadFileName",
          "slug": "media-download-file-name",
          "kind": "function",
          "declaration": "/**\n * 저장 파일명 = `` `${prefix}-${id || index + 1}.${ext}` `` (전신 saveImages.ts:71).\n *\n * 확장자 우선순위 — **저장된 fileName → contentType → URL 경로 → `'jpg'`**.\n * 전신 주석(saveImages.ts:56-58) 그대로의 근거: \"저장된 원본 이름을 우선한다 — 다운로드 URL은\n * 흔히 경로에 확장자가 없는 토큰화된 프록시라, URL을 스니핑하면 모든 PNG/HEIC/동영상\n * 다운로드가 `<prefix>-<id>.jpg`가 되어버린다.\"\n *\n * ⚠ `index`는 **0-base 필수 인자**다(G8). `id`가 없거나 빈 문자열일 때 `index + 1`이\n * 유일한 구분자이며, 이것이 없으면 여러 장을 저장할 때 파일명이 전부 같아진다 —\n * 브라우저는 `(1)` 접미사로 얼버무리고, MediaLibrary 경로는 캐시 파일을 덮어쓴다.\n */\ndeclare function mediaDownloadFileName(input: {\n    readonly url: string;\n    readonly index: number;\n    readonly id?: string | undefined;\n    readonly fileName?: string | undefined;\n    readonly contentType?: MediaContentType | undefined;\n    readonly prefix?: string | undefined;\n}): string;",
          "sourceDocumentation": "저장 파일명 = `` `${prefix}-${id || index + 1}.${ext}` `` (전신 saveImages.ts:71).\n\n확장자 우선순위 — **저장된 fileName → contentType → URL 경로 → `'jpg'`**.\n전신 주석(saveImages.ts:56-58) 그대로의 근거: \"저장된 원본 이름을 우선한다 — 다운로드 URL은\n흔히 경로에 확장자가 없는 토큰화된 프록시라, URL을 스니핑하면 모든 PNG/HEIC/동영상\n다운로드가 `<prefix>-<id>.jpg`가 되어버린다.\"\n\n⚠ `index`는 **0-base 필수 인자**다(G8). `id`가 없거나 빈 문자열일 때 `index + 1`이\n유일한 구분자이며, 이것이 없으면 여러 장을 저장할 때 파일명이 전부 같아진다 —\n브라우저는 `(1)` 접미사로 얼버무리고, MediaLibrary 경로는 캐시 파일을 덮어쓴다."
        },
        {
          "name": "MediaError",
          "slug": "media-error",
          "kind": "class",
          "declaration": "declare class MediaError extends Error {\n    readonly code: MediaErrorCode;\n    constructor(code: MediaErrorCode, message: string);\n}"
        },
        {
          "name": "mediaErrorCode",
          "slug": "media-error-code",
          "kind": "function",
          "declaration": "/** 전신 `photoErrorCode`. 소비자는 이 값으로만 분기한다 — 문구 매칭 금지. */\ndeclare function mediaErrorCode(error: unknown): MediaErrorCode | null;",
          "sourceDocumentation": "전신 `photoErrorCode`. 소비자는 이 값으로만 분기한다 — 문구 매칭 금지."
        },
        {
          "name": "MediaErrorCode",
          "slug": "media-error-code--type",
          "kind": "type",
          "declaration": "type MediaErrorCode = (typeof MEDIA_ERROR_CODES)[number];"
        },
        {
          "name": "mediaErrorUserMessage",
          "slug": "media-error-user-message",
          "kind": "function",
          "declaration": "/**\n * 전신 `photoErrorUserMessage`.\n * `MediaError`의 message는 이미 사용자 노출 가능 문구다(`MediaStrings` 주입 결과 — §4).\n * 화면은 일반 실패 문구 대신 이 값을 그대로 표시해도 된다.\n */\ndeclare function mediaErrorUserMessage(error: unknown): string | null;",
          "sourceDocumentation": "전신 `photoErrorUserMessage`.\n`MediaError`의 message는 이미 사용자 노출 가능 문구다(`MediaStrings` 주입 결과 — §4).\n화면은 일반 실패 문구 대신 이 값을 그대로 표시해도 된다."
        },
        {
          "name": "mediaFileName",
          "slug": "media-file-name",
          "kind": "function",
          "declaration": "/**\n * 전신의 `defaultMediaFileName`·`inferFileName`·`inferWebFileName` **3종을 하나로 통합**(§5.7.2-③).\n *\n * `fileName`이 있으면 그대로 쓰고, 없으면 `${prefix}-${now}.${ext}`(전신 규칙 보존).\n * ⚠ `now`는 결정론적 테스트를 위한 주입구다 — 생략 시 `Date.now()`로 전신과 동일하게 동작한다\n * (§5.4.1-15). 전신은 `Date.now()`를 직접 호출해 파일명 규칙에 테스트가 없었다.\n * ⚠ `prefix` 기본값은 `'media'`다 — 전신 `'photo'`에서 바꿨다(§5.4.1-13). 호스트는 항상 자기\n * 프리픽스를 주입하므로 이관 영향이 0이며, 라이브러리 이름과의 일관성을 택했다.\n */\ndeclare function mediaFileName(input: {\n    readonly fileName?: string | null | undefined;\n    readonly contentType: MediaContentType;\n    readonly prefix?: string | undefined;\n    readonly now?: number | undefined;\n}): string;",
          "sourceDocumentation": "전신의 `defaultMediaFileName`·`inferFileName`·`inferWebFileName` **3종을 하나로 통합**(§5.7.2-③).\n\n`fileName`이 있으면 그대로 쓰고, 없으면 `${prefix}-${now}.${ext}`(전신 규칙 보존).\n⚠ `now`는 결정론적 테스트를 위한 주입구다 — 생략 시 `Date.now()`로 전신과 동일하게 동작한다\n(§5.4.1-15). 전신은 `Date.now()`를 직접 호출해 파일명 규칙에 테스트가 없었다.\n⚠ `prefix` 기본값은 `'media'`다 — 전신 `'photo'`에서 바꿨다(§5.4.1-13). 호스트는 항상 자기\n프리픽스를 주입하므로 이관 영향이 0이며, 라이브러리 이름과의 일관성을 택했다."
        },
        {
          "name": "MediaKind",
          "slug": "media-kind",
          "kind": "type",
          "declaration": "type MediaKind = 'image' | 'video';"
        },
        {
          "name": "mediaKindOf",
          "slug": "media-kind-of",
          "kind": "function",
          "declaration": "/**\n * 전신 `isVideoContentType`의 자리를 잇는다(그쪽은 §5.7.2-③에서 내부화 —\n * 동치 대체가 `mediaKindOf(ct) === 'video'`다).\n */\ndeclare function mediaKindOf(contentType: MediaContentType): MediaKind;",
          "sourceDocumentation": "전신 `isVideoContentType`의 자리를 잇는다(그쪽은 §5.7.2-③에서 내부화 —\n동치 대체가 `mediaKindOf(ct) === 'video'`다)."
        },
        {
          "name": "MediaLibrarySaveAdapter",
          "slug": "media-library-save-adapter",
          "kind": "interface",
          "declaration": "interface MediaLibrarySaveAdapter {\n    requestWritePermission(): Promise<MediaPermission>;\n    saveToLibrary(uri: string): Promise<void>;\n    /**\n     * Android Expo Go는 사진 권한 요청 자체가 불가 — 그 판정을 어댑터가 정적으로 노출한다\n     * (전신 saveImages.ts:96). `expo-constants` 의존을 라이브러리에서 완전히 제거하는 지점(§0.2):\n     * `Constants.appOwnership === 'expo'` 판정은 호스트가 하고 값만 넘긴다.\n     */\n    readonly skipPermissionRequest: boolean;\n}"
        },
        {
          "name": "MediaMetadata",
          "slug": "media-metadata",
          "kind": "type",
          "declaration": "/**\n * EXIF에서 유도한 촬영 메타데이터. `capturedAt`은 ISO 타임스탬프이며, EXIF 벽시계는\n * **기기 로컬 타임존**으로 해석한다(§7 하드닝 11 — 근거는 `metadata.ts`에 원문 그대로 있다).\n *\n * ⚠ **필드명은 전신 그대로다.** 초안의 `geoPoint → location` 리네임은 **철회**했다(G5 확정).\n * 이 객체는 `api.completeUpload({ photo })`로 **호스트 백엔드에 그대로 전달**된다\n * (전신 uploader.ts:442). 리네임의 파손은 타입에 잡히지 않는다:\n *   · 호스트 대입 지점 `kit.ts:40-44`가 `completeUpload({...input})`을 zod 파생 타입에 넘긴다.\n *   · 타깃은 `packages/shared/src/index.ts:105-108` `photoMetadataSchema = { capturedAt?, geoPoint? }`.\n *   · `{capturedAt?, location?}` → `{capturedAt?, geoPoint?}` 대입은 공통 프로퍼티 `capturedAt`이\n *     있어 **weak type 검사를 통과**하고, 변수 전달이라 초과 프로퍼티 검사도 걸리지 않는다.\n *   · 런타임에서는 서버 zod가 non-strict라 미지의 `location` 키를 **조용히 스트립**한다.\n * 즉 컴파일도 테스트도 통과하고 위치정보만 사라지는 §6.1의 전형적 \"조용히 깨지는\" 클래스이며,\n * 얻는 것은 명명 취향뿐이었다.\n *\n * ⚠ 초안의 `width`/`height`는 **삭제**했다. 전신 EXIF 파서는 `PixelXDimension`/`PixelYDimension`을\n * 읽지 않으므로(photoMetadata.ts 전문에 `Pixel`·`width`·`height` 0건 — grep 실측) 영구 `undefined`인\n * 죽은 필드였고, `MediaUploadCompletion`에 이미 최상위 `width`/`height`(피커 자산 치수,\n * uploader.ts:749-756)가 있어 의미도 중복이다.\n */\ntype MediaMetadata = {\n    readonly capturedAt?: string | undefined;\n    readonly geoPoint?: GeoPoint | undefined;\n};",
          "sourceDocumentation": "EXIF에서 유도한 촬영 메타데이터. `capturedAt`은 ISO 타임스탬프이며, EXIF 벽시계는\n**기기 로컬 타임존**으로 해석한다(§7 하드닝 11 — 근거는 `metadata.ts`에 원문 그대로 있다).\n\n⚠ **필드명은 전신 그대로다.** 초안의 `geoPoint → location` 리네임은 **철회**했다(G5 확정).\n이 객체는 `api.completeUpload({ photo })`로 **호스트 백엔드에 그대로 전달**된다\n(전신 uploader.ts:442). 리네임의 파손은 타입에 잡히지 않는다:\n  · 호스트 대입 지점 `kit.ts:40-44`가 `completeUpload({...input})`을 zod 파생 타입에 넘긴다.\n  · 타깃은 `packages/shared/src/index.ts:105-108` `photoMetadataSchema = { capturedAt?, geoPoint? }`.\n  · `{capturedAt?, location?}` → `{capturedAt?, geoPoint?}` 대입은 공통 프로퍼티 `capturedAt`이\n    있어 **weak type 검사를 통과**하고, 변수 전달이라 초과 프로퍼티 검사도 걸리지 않는다.\n  · 런타임에서는 서버 zod가 non-strict라 미지의 `location` 키를 **조용히 스트립**한다.\n즉 컴파일도 테스트도 통과하고 위치정보만 사라지는 §6.1의 전형적 \"조용히 깨지는\" 클래스이며,\n얻는 것은 명명 취향뿐이었다.\n\n⚠ 초안의 `width`/`height`는 **삭제**했다. 전신 EXIF 파서는 `PixelXDimension`/`PixelYDimension`을\n읽지 않으므로(photoMetadata.ts 전문에 `Pixel`·`width`·`height` 0건 — grep 실측) 영구 `undefined`인\n죽은 필드였고, `MediaUploadCompletion`에 이미 최상위 `width`/`height`(피커 자산 치수,\nuploader.ts:749-756)가 있어 의미도 중복이다."
        },
        {
          "name": "mediaMetadataFromExif",
          "slug": "media-metadata-from-exif",
          "kind": "function",
          "declaration": "/**\n * 전신 `extractPhotoMetadata`.\n *\n * ⚠ **유효값이 하나도 없으면 `undefined`를 반환한다 — 빈 객체 금지**(photoMetadata.ts:130 규칙).\n * truthy한 빈 객체를 주면 호출자가 \"EXIF가 있었다\"고 믿게 되고, 그 오해는 업로드 완료 페이로드의\n * `photo` 필드까지 그대로 흘러간다.\n */\ndeclare function mediaMetadataFromExif(exif?: ReadonlyExifRecord | null): MediaMetadata | undefined;",
          "sourceDocumentation": "전신 `extractPhotoMetadata`.\n\n⚠ **유효값이 하나도 없으면 `undefined`를 반환한다 — 빈 객체 금지**(photoMetadata.ts:130 규칙).\ntruthy한 빈 객체를 주면 호출자가 \"EXIF가 있었다\"고 믿게 되고, 그 오해는 업로드 완료 페이로드의\n`photo` 필드까지 그대로 흘러간다."
        },
        {
          "name": "mediaMetadataFromJpeg",
          "slug": "media-metadata-from-jpeg",
          "kind": "function",
          "declaration": "/**\n * 전신 `extractPhotoMetadataFromBlob(blob, fallbackExif, contentType)`(photoMetadata.ts:265-290).\n * 초안이 지웠던 인자 2개를 `options`로 **복원**한다(G6 · §7.1). **4규칙이 계약이다**:\n *   ① 비-JPEG 스킵 — `(contentType ?? source.type)`이 'jpeg'/'jpg'를 포함하지 않으면 파싱하지 않고\n *      fallback을 그대로 반환한다.\n *   ② **필드 단위 병합** — `parsed?.X ?? fallback?.X`. 객체 단위 폴백이 아니다.\n *      capturedAt만 파싱되고 geoPoint는 fallback에서 오는 조합이 **정상 결과**다.\n *   ③ 파싱 예외 시 fallback 반환 — throw 금지. 메타데이터 때문에 업로드가 죽어선 안 된다.\n *   ④ ①②③ 이후에도 유효값이 없으면 `undefined`(빈 객체 금지 — `mediaMetadataFromExif`와 동일).\n *\n * ⚠ 인자를 지우면 웹 피커 경로(`fallbackExif: asset.exif`)에서 JPEG 파싱이 실패했을 때 피커가 준\n * EXIF가 통째로 버려져 촬영시각·위치가 유실된다. 그 유실은 컴파일도 테스트도 통과한다.\n */\ndeclare function mediaMetadataFromJpeg(source: BinarySource, options?: {\n    readonly fallbackExif?: ReadonlyExifRecord | null | undefined;\n    readonly contentType?: MediaContentType | string | null | undefined;\n} | undefined): Promise<MediaMetadata | undefined>;",
          "sourceDocumentation": "전신 `extractPhotoMetadataFromBlob(blob, fallbackExif, contentType)`(photoMetadata.ts:265-290).\n초안이 지웠던 인자 2개를 `options`로 **복원**한다(G6 · §7.1). **4규칙이 계약이다**:\n  ① 비-JPEG 스킵 — `(contentType ?? source.type)`이 'jpeg'/'jpg'를 포함하지 않으면 파싱하지 않고\n     fallback을 그대로 반환한다.\n  ② **필드 단위 병합** — `parsed?.X ?? fallback?.X`. 객체 단위 폴백이 아니다.\n     capturedAt만 파싱되고 geoPoint는 fallback에서 오는 조합이 **정상 결과**다.\n  ③ 파싱 예외 시 fallback 반환 — throw 금지. 메타데이터 때문에 업로드가 죽어선 안 된다.\n  ④ ①②③ 이후에도 유효값이 없으면 `undefined`(빈 객체 금지 — `mediaMetadataFromExif`와 동일).\n\n⚠ 인자를 지우면 웹 피커 경로(`fallbackExif: asset.exif`)에서 JPEG 파싱이 실패했을 때 피커가 준\nEXIF가 통째로 버려져 촬영시각·위치가 유실된다. 그 유실은 컴파일도 테스트도 통과한다."
        },
        {
          "name": "MediaOperation",
          "slug": "media-operation",
          "kind": "type",
          "declaration": "/**\n * 라이브러리가 방출하는 operation의 닫힌 목록.\n *\n * 리터럴 유니언으로 좁힌 이유(§6.2 기각표의 반대 항목을 철회한 근거): 이 인터페이스는\n * **라이브러리가 방출하는** operation만 다루고 그 목록은 6종으로 닫혀 있다. 호스트가 자기\n * operation을 보고하는 것은 이 인터페이스의 일이 아니다 — memorylog2도 기기 라이브러리 진단\n * 4종을 앱 리포터로 직접 보낸다(`devicePhotoLibraryTelemetry.ts`). 좁히면 오타가 컴파일 에러가 된다.\n *\n * `MediaOperation`은 문자열 리터럴 유니언이므로 호스트의 `operation: string` 슬롯에\n * 그대로 대입된다(가변성 문제 없음).\n */\ntype MediaOperation = (typeof MEDIA_OPERATIONS)[number];",
          "sourceDocumentation": "라이브러리가 방출하는 operation의 닫힌 목록.\n\n리터럴 유니언으로 좁힌 이유(§6.2 기각표의 반대 항목을 철회한 근거): 이 인터페이스는\n**라이브러리가 방출하는** operation만 다루고 그 목록은 6종으로 닫혀 있다. 호스트가 자기\noperation을 보고하는 것은 이 인터페이스의 일이 아니다 — memorylog2도 기기 라이브러리 진단\n4종을 앱 리포터로 직접 보낸다(`devicePhotoLibraryTelemetry.ts`). 좁히면 오타가 컴파일 에러가 된다.\n\n`MediaOperation`은 문자열 리터럴 유니언이므로 호스트의 `operation: string` 슬롯에\n그대로 대입된다(가변성 문제 없음)."
        },
        {
          "name": "MediaOrphanedUpload",
          "slug": "media-orphaned-upload",
          "kind": "type",
          "declaration": "/**\n * 실패 시 정리 후보인 스토리지 오브젝트.\n *\n * `uploaded`는 2xx 응답까지 확인한 PUT, `possibly-uploaded`는 응답 유실·전송 예외처럼\n * 서버에는 도달했을 수도 있는 PUT이다. 후자도 cleanup endpoint가 멱등으로 처리해야 한다.\n */\ntype MediaOrphanedUpload = MediaUploadObject & {\n    readonly storageState: 'uploaded' | 'possibly-uploaded';\n};",
          "sourceDocumentation": "실패 시 정리 후보인 스토리지 오브젝트.\n\n`uploaded`는 2xx 응답까지 확인한 PUT, `possibly-uploaded`는 응답 유실·전송 예외처럼\n서버에는 도달했을 수도 있는 PUT이다. 후자도 cleanup endpoint가 멱등으로 처리해야 한다."
        },
        {
          "name": "MediaPermission",
          "slug": "media-permission",
          "kind": "type",
          "declaration": "type MediaPermission = {\n    readonly granted: boolean;\n    readonly canAskAgain: boolean;\n    /** iOS \"선택된 사진\" — 일부만 보인다. */\n    readonly limited: boolean;\n};"
        },
        {
          "name": "MediaPickerActions",
          "slug": "media-picker-actions",
          "kind": "interface",
          "declaration": "interface MediaPickerActions {\n    /** Request library permission and return at most `max` normalized assets. */\n    pick(options?: MediaPickOptions | undefined): Promise<readonly PickedAsset[]>;\n    /** Request camera permission and return at most one normalized asset. */\n    capture(options?: {\n        readonly kind?: MediaKind | undefined;\n    } | undefined): Promise<readonly PickedAsset[]>;\n}"
        },
        {
          "name": "MediaPickOptions",
          "slug": "media-pick-options",
          "kind": "type",
          "declaration": "type MediaPickOptions = {\n    readonly max?: number | undefined;\n    readonly kinds?: readonly MediaKind[] | undefined;\n};"
        },
        {
          "name": "MediaPlatform",
          "slug": "media-platform",
          "kind": "type",
          "declaration": "type MediaPlatform = 'ios' | 'android' | 'web';"
        },
        {
          "name": "MediaSaver",
          "slug": "media-saver",
          "kind": "interface",
          "declaration": "interface MediaSaver {\n    saveToDevice(images: readonly SaveableMedia[]): Promise<SaveResult>;\n}"
        },
        {
          "name": "MediaStrings",
          "slug": "media-strings",
          "kind": "interface",
          "declaration": "interface MediaStrings {\n    /** 자산 정보 조회 15s 데드라인 초과 — `device-timeout`. */\n    readonly deviceInfoTimeout: string;\n    /** iCloud 원본 다운로드 60s 데드라인 초과 — `device-timeout`. */\n    readonly iCloudDownloadTimeout: string;\n    /** 원본이 iCloud에만 있고 다운로드 옵트인이 없음 — `device-icloud-only`. */\n    readonly iCloudOnly: string;\n    /** 로컬 파일 없음/판독 불가 — `device-not-found`. 전신은 hashFile.ts:71과 문구를 공유했다. */\n    readonly fileNotFound: string;\n    /** 기기 라이브러리 어댑터/OS 호출 실패 — 원본 예외를 공개하지 않는다. */\n    readonly deviceLibraryFailed: string;\n    /** 피커 어댑터/웹 바이너리 로더 호출 실패 — 원본 예외를 공개하지 않는다. */\n    readonly pickerFailed: string;\n    /** 이미지 decoder 또는 transform 실패 — URI와 네이티브 원문은 공개하지 않는다. */\n    readonly imageProcessingFailed: string;\n    readonly unsupportedFileType: string;\n    /** 전신 uploader.ts:625 — 드롭/선택 결과에 업로드 가능한 미디어가 없음. */\n    readonly noMediaFiles: string;\n    /** 전신 uploader.ts:678 — 피커가 준 사진 자산에 uri가 없음. */\n    readonly pickedPhotoInvalid: string;\n    /** 전신 uploader.ts:717 — 피커가 준 미디어 자산에 uri가 없음. */\n    readonly pickedMediaInvalid: string;\n    readonly imageSizeUnknown: string;\n    readonly videoSizeUnknown: string;\n    readonly imageUploadFailed: string;\n    readonly videoUploadFailed: string;\n    /** 전신 uploader.ts:237, 295 — 포스터(썸네일) PUT 실패. */\n    readonly posterUploadFailed: string;\n    readonly photoPermissionRequired: string;\n    readonly mediaPermissionRequired: string;\n    readonly cameraPermissionRequired: string;\n    readonly savePermissionDenied: string;\n    readonly saveDownloadFailed: string;\n    /**\n     * `platform-unsupported` — 비네이티브 포크(web·SSR·RSC)에서 네이티브 전용 경로를 호출했을 때.\n     * 전신에는 대응 문구가 없다(전신의 web 포크는 영어 `Error`를 던졌다 —\n     * devicePhotoLibrary.web.ts:29). 이 코드는 §5.2에서 신설됐는데 §4의 키 목록이 함께\n     * 늘지 않아, 초기 구현이 `pickedMediaInvalid`·`fileNotFound`·`saveDownloadFailed` 셋을\n     * 돌려 쓰고 있었다 — 전부 원인과 무관한 문구다(\"파일을 찾을 수 없습니다\"는 사용자를\n     * 파일 탐색으로 오도한다). 그래서 전용 키로 분리한다.\n     */\n    readonly platformUnsupported: string;\n    /** 크기 초과 — 단위 표기가 언어마다 다르므로 함수. */\n    readonly fileTooLarge: (input: {\n        readonly maxBytes: number;\n        readonly kind: MediaKind;\n    }) => string;\n}"
        },
        {
          "name": "MediaTelemetry",
          "slug": "media-telemetry",
          "kind": "interface",
          "declaration": "interface MediaTelemetry {\n    /**\n     * `run()`을 감싸 성공/예외를 자동 보고한다. 구현은 `run()`의 결과·예외를 그대로\n     * return/rethrow해야 한다. 라이브러리는 호스트 텔레메트리를 관측자로 취급하므로,\n     * 구현 자체의 예외는 실제 업로드·저장 결과를 바꾸지 않는다.\n     */\n    track<T>(operation: MediaOperation, extra: Readonly<Record<string, unknown>>, run: () => Promise<T>): Promise<T>;\n    begin(operation: MediaOperation, extra?: Readonly<Record<string, unknown>> | undefined): MediaActivity;\n}"
        },
        {
          "name": "MediaUploadApi",
          "slug": "media-upload-api",
          "kind": "interface",
          "declaration": "/**\n * 백엔드 계약: presigned 슬롯을 발급받고, 올라간 오브젝트를 등록한다.\n * `TAsset`은 호스트 API가 저장된 자산으로 반환하는 무엇이든 된다.\n */\ninterface MediaUploadApi<TAsset, TCollectionId extends string = string> extends MediaUploadIntentApi<TCollectionId> {\n    completeUpload(input: MediaUploadCompletion<TCollectionId>): Promise<UploadResult<TAsset>>;\n}",
          "sourceDocumentation": "백엔드 계약: presigned 슬롯을 발급받고, 올라간 오브젝트를 등록한다.\n`TAsset`은 호스트 API가 저장된 자산으로 반환하는 무엇이든 된다."
        },
        {
          "name": "MediaUploadCompletion",
          "slug": "media-upload-completion",
          "kind": "type",
          "declaration": "/** 바이트가 스토리지에 올라간 뒤 보내는 등록 페이로드. */\ntype MediaUploadCompletion<TCollectionId extends string = string> = {\n    readonly fileName: string;\n    readonly contentType: MediaContentType;\n    readonly sizeBytes: number;\n    readonly objectName: string;\n    readonly contentHash?: string | undefined;\n    /**\n     * 불투명 그룹 id — 킷은 해석하지 않고 전달만 한다(§6.2 기각: `CollectionId` 브랜드).\n     * memorylog2는 이것을 `albumId`로 매핑한다. 빈 문자열은 런타임 차단(§6.1-⑪) —\n     * falsy 스프레드로 조용히 탈락해 앨범 없이 저장되는 경로를 막는다.\n     */\n    readonly collectionId?: TCollectionId | undefined;\n    readonly photo?: MediaMetadata | undefined;\n    readonly durationMs?: number | undefined;\n    readonly width?: number | undefined;\n    readonly height?: number | undefined;\n    readonly poster?: UploadedPoster | undefined;\n};",
          "sourceDocumentation": "바이트가 스토리지에 올라간 뒤 보내는 등록 페이로드."
        },
        {
          "name": "MediaUploadConfig",
          "slug": "media-upload-config",
          "kind": "type",
          "declaration": "type MediaUploadConfig<TAsset, TCollectionId extends string = string> = MediaUploadBaseConfig<TCollectionId> & {\n    readonly api: MediaUploadApi<TAsset, TCollectionId>;\n};"
        },
        {
          "name": "mediaUploadFailureInfo",
          "slug": "media-upload-failure-info",
          "kind": "function",
          "declaration": "/**\n * 실패한 업로드가 남긴 정리 후보를 읽는다.\n *\n * `MediaError`와 마찬가지로 `instanceof`가 아니라 전역 심볼을 읽으므로, `.`에서 잡은\n * 에러가 `./core` 또는 다른 CJS/ESM 사본에서 만들어졌어도 동작한다. 값에는 presigned URL,\n * HTTP header, 원본 네트워크 에러가 절대 포함되지 않는다.\n */\ndeclare function mediaUploadFailureInfo(error: unknown): MediaUploadFailureInfo | null;",
          "sourceDocumentation": "실패한 업로드가 남긴 정리 후보를 읽는다.\n\n`MediaError`와 마찬가지로 `instanceof`가 아니라 전역 심볼을 읽으므로, `.`에서 잡은\n에러가 `./core` 또는 다른 CJS/ESM 사본에서 만들어졌어도 동작한다. 값에는 presigned URL,\nHTTP header, 원본 네트워크 에러가 절대 포함되지 않는다."
        },
        {
          "name": "MediaUploadFailureInfo",
          "slug": "media-upload-failure-info--type",
          "kind": "type",
          "declaration": "/**\n * `mediaUploadFailureInfo(error)`가 돌려주는, cross-entry-safe 실패 복구 정보.\n *\n * 이 정보는 에러에 전역 심볼로 비열거형 각인되므로 code splitting으로 코어 사본이 갈린\n * 엔트리에서도 검사할 수 있다. `orphanedObjects`는 attachment/등록이 끝나기 전에 남은\n * 정리 후보이며, 앱은 자신의 권한 있는 cleanup API로만 처리해야 한다.\n */\ntype MediaUploadFailureInfo = {\n    readonly stage: MediaUploadFailureStage;\n    readonly orphanedObjects: readonly MediaOrphanedUpload[];\n};",
          "sourceDocumentation": "`mediaUploadFailureInfo(error)`가 돌려주는, cross-entry-safe 실패 복구 정보.\n\n이 정보는 에러에 전역 심볼로 비열거형 각인되므로 code splitting으로 코어 사본이 갈린\n엔트리에서도 검사할 수 있다. `orphanedObjects`는 attachment/등록이 끝나기 전에 남은\n정리 후보이며, 앱은 자신의 권한 있는 cleanup API로만 처리해야 한다."
        },
        {
          "name": "MediaUploadFailureStage",
          "slug": "media-upload-failure-stage",
          "kind": "type",
          "declaration": "/** 업로드 파이프라인에서 안전하게 공개할 수 있는 실패 단계. URL·헤더·원본 예외는 포함하지 않는다. */\ntype MediaUploadFailureStage = 'intent' | 'put' | 'complete';",
          "sourceDocumentation": "업로드 파이프라인에서 안전하게 공개할 수 있는 실패 단계. URL·헤더·원본 예외는 포함하지 않는다."
        },
        {
          "name": "MediaUploadIntent",
          "slug": "media-upload-intent",
          "kind": "type",
          "declaration": "/** 백엔드가 발급한 단일 오브젝트 presigned 업로드 슬롯. */\ntype MediaUploadIntent = {\n    readonly uploadUrl: string;\n    readonly method: 'PUT';\n    readonly headers: Readonly<Record<string, string>>;\n    readonly objectName: string;\n};",
          "sourceDocumentation": "백엔드가 발급한 단일 오브젝트 presigned 업로드 슬롯."
        },
        {
          "name": "MediaUploadIntentApi",
          "slug": "media-upload-intent-api",
          "kind": "interface",
          "declaration": "/**\n * Presign-only backend seam.\n *\n * Some products deliberately attach the uploaded object in a later domain\n * transaction (for example, after creating a record). They must not pretend\n * that a registration endpoint exists just to use the local streaming path.\n * `MediaUploadApi` extends this narrower contract for the usual\n * presign → PUT → complete flow.\n */\ninterface MediaUploadIntentApi<TCollectionId extends string = string> {\n    createUploadIntent(input: MediaUploadIntentRequest<TCollectionId>): Promise<MediaUploadIntent>;\n}",
          "sourceDocumentation": "Presign-only backend seam.\n\nSome products deliberately attach the uploaded object in a later domain\ntransaction (for example, after creating a record). They must not pretend\nthat a registration endpoint exists just to use the local streaming path.\n`MediaUploadApi` extends this narrower contract for the usual\npresign → PUT → complete flow."
        },
        {
          "name": "MediaUploadIntentRequest",
          "slug": "media-upload-intent-request",
          "kind": "type",
          "declaration": "type MediaUploadIntentRequest<TCollectionId extends string = string> = {\n    readonly fileName: string;\n    /** ⚠ 전신은 `string`이었다. 닫힌 8종 유니언으로 좁혀 서버 zod와 클라이언트가 어긋나지 않게 한다. */\n    readonly contentType: MediaContentType;\n    readonly sizeBytes: number;\n    /**\n     * 업로드를 시작하기 전에 백엔드가 권한·용량을 확인해야 하는 경우의 불투명 그룹 id.\n     * 완료 단계의 `collectionId`와 같은 값이며, 킷은 해석하거나 생성하지 않고 그대로 전달한다.\n     * 선택적 필드라 기존 presign-only 소비자는 변경 없이 동작한다.\n     */\n    readonly collectionId?: TCollectionId | undefined;\n};"
        },
        {
          "name": "MediaUploadLimit",
          "slug": "media-upload-limit",
          "kind": "type",
          "declaration": "/**\n * 백엔드 정책을 반영한 클라이언트 크기 캡. 같은 문구로 **빨리** 실패시켜 사용자가 전체 업로드를\n * 마친 뒤 413을 받는 일을 없앤다.\n */\ntype MediaUploadLimit = {\n    readonly maxBytes: number;\n    readonly message?: string | undefined;\n};",
          "sourceDocumentation": "백엔드 정책을 반영한 클라이언트 크기 캡. 같은 문구로 **빨리** 실패시켜 사용자가 전체 업로드를\n마친 뒤 413을 받는 일을 없앤다."
        },
        {
          "name": "MediaUploadLimits",
          "slug": "media-upload-limits",
          "kind": "type",
          "declaration": "/**\n * ⚠ 팩토리에서 이 값은 **생략 불가**다(§6.1-③). 무제한 업로드는 명시적 결정이어야 한다.\n * 서버만 검증하는 정책도 정당하므로 팩토리 설정은 `MediaUploadLimits | 'server-enforced'`를 받는다\n * (`Number.POSITIVE_INFINITY`는 JSON 직렬화 불가라 기각 — §0.4 기각 8).\n */\ntype MediaUploadLimits = {\n    readonly image?: MediaUploadLimit | undefined;\n    readonly video?: MediaUploadLimit | undefined;\n};",
          "sourceDocumentation": "⚠ 팩토리에서 이 값은 **생략 불가**다(§6.1-③). 무제한 업로드는 명시적 결정이어야 한다.\n서버만 검증하는 정책도 정당하므로 팩토리 설정은 `MediaUploadLimits | 'server-enforced'`를 받는다\n(`Number.POSITIVE_INFINITY`는 JSON 직렬화 불가라 기각 — §0.4 기각 8)."
        },
        {
          "name": "MediaUploadObject",
          "slug": "media-upload-object",
          "kind": "type",
          "declaration": "/**\n * 스토리지에 쓸 수 있는 이름으로 발급된 오브젝트의 안전한 식별 정보.\n *\n * URL·헤더·서명은 의도적으로 없다. 앱은 이 값만 자기 cleanup API에 넘겨, 실패한\n * 업로드가 남긴 object를 best-effort로 정리할 수 있다. `objectName`의 권한 검증은\n * 언제나 서버가 다시 해야 하며, 이 타입은 클라이언트 권한 증명이 아니다. 런타임에서는\n * 1024자 이하 ASCII unreserved 경로 세그먼트(`[A-Za-z0-9._~-]`)와 `/`만 허용한다.\n * URL/query/percent-encoding/공백을 받지 않으므로, 서버는 그 문법을 발급 키에도 맞춰야 한다.\n */\ntype MediaUploadObject = {\n    readonly objectName: string;\n    readonly contentType: MediaContentType;\n    readonly sizeBytes: number;\n};",
          "sourceDocumentation": "스토리지에 쓸 수 있는 이름으로 발급된 오브젝트의 안전한 식별 정보.\n\nURL·헤더·서명은 의도적으로 없다. 앱은 이 값만 자기 cleanup API에 넘겨, 실패한\n업로드가 남긴 object를 best-effort로 정리할 수 있다. `objectName`의 권한 검증은\n언제나 서버가 다시 해야 하며, 이 타입은 클라이언트 권한 증명이 아니다. 런타임에서는\n1024자 이하 ASCII unreserved 경로 세그먼트(`[A-Za-z0-9._~-]`)와 `/`만 허용한다.\nURL/query/percent-encoding/공백을 받지 않으므로, 서버는 그 문법을 발급 키에도 맞춰야 한다."
        },
        {
          "name": "NamedBinarySource",
          "slug": "named-binary-source",
          "kind": "interface",
          "declaration": "/** 웹 File의 구조적 최소치 — DOM lib 없이 `isSupportedMediaFile`을 쓰기 위해(§7 하드닝 10). */\ninterface NamedBinarySource extends BinarySource {\n    readonly name: string;\n}",
          "sourceDocumentation": "웹 File의 구조적 최소치 — DOM lib 없이 `isSupportedMediaFile`을 쓰기 위해(§7 하드닝 10)."
        },
        {
          "name": "noopMediaTelemetry",
          "slug": "noop-media-telemetry",
          "kind": "constant",
          "declaration": "noopMediaTelemetry: MediaTelemetry",
          "sourceDocumentation": "팩토리 기본값. `track`은 run()을 그대로 실행하고 `begin`은 no-op 활동을 준다\n(전신 `NOOP_TELEMETRY` — types.ts:94-101 계승).\n텔레메트리를 주입하지 않은 소비자에게 분기(`telemetry?.track ?? …`)를 강요하지 않기 위한 값이므로,\n라이브러리 내부는 항상 이 객체를 통해 호출한다."
        },
        {
          "name": "normalizeDurationMs",
          "slug": "normalize-duration-ms",
          "kind": "function",
          "declaration": "/**\n * 피커가 준 원시 duration을 밀리초로 정규화한다.\n *\n * - `os === 'web'` → 초 단위이므로 ×1000\n * - 그 외(ios·android) → 이미 밀리초이므로 그대로\n * - 0·음수·NaN·Infinity → `undefined` (전신 `asset.duration > 0` 게이트 보존)\n *\n * 반올림 규칙(`Math.round`)도 전신 그대로다 — 서버에 소수점 밀리초를 보내지 않는다.\n */\ndeclare function normalizeDurationMs(raw: number | undefined, os: MediaPlatform): number | undefined;",
          "sourceDocumentation": "피커가 준 원시 duration을 밀리초로 정규화한다.\n\n- `os === 'web'` → 초 단위이므로 ×1000\n- 그 외(ios·android) → 이미 밀리초이므로 그대로\n- 0·음수·NaN·Infinity → `undefined` (전신 `asset.duration > 0` 게이트 보존)\n\n반올림 규칙(`Math.round`)도 전신 그대로다 — 서버에 소수점 밀리초를 보내지 않는다."
        },
        {
          "name": "parseExifWallClock",
          "slug": "parse-exif-wall-clock",
          "kind": "function",
          "declaration": "/**\n * Strictly parses the EXIF datetime representation as a wall clock.\n *\n * `Date.UTC` normally normalises invalid values (for example February 30), so\n * all fields are compared after construction before the value is exposed. This\n * helper has no device-time-zone dependency and is therefore safe for a\n * historical activity or trip whose offset was persisted at record time.\n */\ndeclare function parseExifWallClock(value: unknown): ExifWallClock | undefined;",
          "sourceDocumentation": "Strictly parses the EXIF datetime representation as a wall clock.\n\n`Date.UTC` normally normalises invalid values (for example February 30), so\nall fields are compared after construction before the value is exposed. This\nhelper has no device-time-zone dependency and is therefore safe for a\nhistorical activity or trip whose offset was persisted at record time."
        },
        {
          "name": "PendingAddResult",
          "slug": "pending-add-result",
          "kind": "type",
          "declaration": "type PendingAddResult = {\n    /** The next state. Same reference as the input when nothing was added. */\n    readonly state: PendingSelectionState;\n    /** Items that entered the selection, in input order. */\n    readonly added: readonly PendingMediaItem[];\n    /**\n     * Items that did not enter the selection, in input order, with the reason. Use it for messaging.\n     * A `duplicate` entry can be the very object that is already staged (the host re-added it), so\n     * do not release this list directly — release `releasable`.\n     */\n    readonly rejected: readonly PendingRejection[];\n    /**\n     * Rejected items that are safe to hand to `release`: every rejected item that is not the same\n     * object as an item in `state`, each object once, in input order. Their `revoke` has not been\n     * called. Picked items and binaries without `revoke` may appear here; `release` ignores them.\n     */\n    readonly releasable: readonly PendingMediaItem[];\n};"
        },
        {
          "name": "PendingBinaryItem",
          "slug": "pending-binary-item",
          "kind": "type",
          "declaration": "/**\n * A binary (web `File`, `Blob`, or any `NamedBinarySource`) staged for a later upload.\n *\n * Core never touches DOM APIs, so the preview resource is injected: `previewUri` is whatever the\n * host can render (typically an object URL) and `revoke` releases it. On web,\n * `pendingItemFromFile` from `@gj-kit/expo-media/web` builds this shape from a DOM `File`.\n */\ntype PendingBinaryItem = {\n    readonly kind: 'binary';\n    readonly source: NamedBinarySource;\n    /** Renderable preview URI (for example an object URL). Omit when none exists. */\n    readonly previewUri?: string | undefined;\n    /**\n     * Releases the preview resource. The selection calls it **at most once per item object**, when\n     * the item leaves a selection through `remove`/`clear`, or when the host passes it to `release`.\n     */\n    readonly revoke?: (() => void) | undefined;\n    /**\n     * Modification timestamp (ms epoch) folded into the dedup key, e.g. DOM `File.lastModified`.\n     * Without it two binaries with the same name and size are considered the same file.\n     */\n    readonly lastModified?: number | undefined;\n};",
          "sourceDocumentation": "A binary (web `File`, `Blob`, or any `NamedBinarySource`) staged for a later upload.\n\nCore never touches DOM APIs, so the preview resource is injected: `previewUri` is whatever the\nhost can render (typically an object URL) and `revoke` releases it. On web,\n`pendingItemFromFile` from `@gj-kit/expo-media/web` builds this shape from a DOM `File`."
        },
        {
          "name": "PendingMediaItem",
          "slug": "pending-media-item",
          "kind": "type",
          "declaration": "type PendingMediaItem = PendingPickedItem | PendingBinaryItem;"
        },
        {
          "name": "PendingPickedItem",
          "slug": "pending-picked-item",
          "kind": "type",
          "declaration": "/** A picker, camera, or device-library asset staged for a later upload. */\ntype PendingPickedItem = {\n    readonly kind: 'picked';\n    readonly asset: PickedAsset;\n};",
          "sourceDocumentation": "A picker, camera, or device-library asset staged for a later upload."
        },
        {
          "name": "PendingRejection",
          "slug": "pending-rejection",
          "kind": "type",
          "declaration": "type PendingRejection = {\n    readonly item: PendingMediaItem;\n    readonly reason: PendingRejectionReason;\n};"
        },
        {
          "name": "PendingRejectionReason",
          "slug": "pending-rejection-reason",
          "kind": "type",
          "declaration": "/**\n * Why an item did not enter the selection.\n * - `duplicate`: an item with the same `keyOf` is already staged (or appeared earlier in the batch).\n * - `over-limit`: the selection already holds `max` items.\n */\ntype PendingRejectionReason = 'duplicate' | 'over-limit';",
          "sourceDocumentation": "Why an item did not enter the selection.\n- `duplicate`: an item with the same `keyOf` is already staged (or appeared earlier in the batch).\n- `over-limit`: the selection already holds `max` items."
        },
        {
          "name": "PendingSelection",
          "slug": "pending-selection",
          "kind": "interface",
          "declaration": "interface PendingSelection {\n    /** The configured cap. */\n    readonly max: number;\n    /**\n     * Stable identity of an item: `asset:<assetId>` (falling back to the URI when the picker gave\n     * no asset id) for picked items, `binary:<name>:<size>:<lastModified>` for binaries.\n     */\n    keyOf(item: PendingMediaItem): string;\n    /**\n     * Stage items. Duplicates (by `keyOf`, including duplicates within `items`) are rejected before\n     * the cap is checked, so a re-selected photo reports `duplicate` even when the selection is full.\n     * Nothing is revoked here; pass the result's `releasable` to `release`. Neither `state` nor\n     * `items` is mutated, and the result object and its arrays are frozen.\n     */\n    add(state: PendingSelectionState, items: readonly PendingMediaItem[]): PendingAddResult;\n    /**\n     * Drop every item whose `keyOf` equals `key`, calling `revoke` on removed binaries. Returns the\n     * same reference when no item matched.\n     */\n    remove(state: PendingSelectionState, key: string): PendingSelectionState;\n    /** Drop every item, calling `revoke` on each binary that still holds a preview resource. */\n    clear(state: PendingSelectionState): PendingSelectionState;\n    /**\n     * Call `revoke` on binaries that are not staged — typically `PendingAddResult.releasable`.\n     * Idempotent per item object; picked items and binaries without `revoke` are ignored. Passing an\n     * item that is still staged kills its live preview, which is why `rejected` is not the input here.\n     */\n    release(items: readonly PendingMediaItem[]): void;\n    /** Picked assets in selection order — the input of `uploadPickedAsset` / device resolve paths. */\n    toPickedAssets(state: PendingSelectionState): readonly PickedAsset[];\n    /** Binary sources in selection order — the input of `uploadBinary` / `uploadDropped`. */\n    toBinarySources(state: PendingSelectionState): readonly NamedBinarySource[];\n    /**\n     * URI the host can render, or `null`. Picked assets always preview through their own URI. A binary\n     * previews only when it has a `previewUri` **and** is not HEIC/HEIF — browsers cannot decode those,\n     * and an `<img>` that never loads looks like a broken upload.\n     */\n    previewUriOf(item: PendingMediaItem): string | null;\n    /** `previewUriOf(item) !== null`. */\n    canPreview(item: PendingMediaItem): boolean;\n    /**\n     * Capture time as an ISO string, or `null`. Picked items read the picker EXIF dictionary; binaries\n     * parse the JPEG APP1 segment from bytes (the same parsers the upload path uses). Never throws.\n     */\n    capturedAtOf(item: PendingMediaItem): Promise<string | null>;\n}"
        },
        {
          "name": "PendingSelectionOptions",
          "slug": "pending-selection-options",
          "kind": "type",
          "declaration": "type PendingSelectionOptions = {\n    /** Maximum number of staged items. Must be a positive integer; otherwise `MediaError('config-invalid')`. */\n    readonly max: number;\n};"
        },
        {
          "name": "PendingSelectionState",
          "slug": "pending-selection-state",
          "kind": "type",
          "declaration": "/** Ordered, immutable selection. Every operation returns a new array or the same reference. */\ntype PendingSelectionState = readonly PendingMediaItem[];",
          "sourceDocumentation": "Ordered, immutable selection. Every operation returns a new array or the same reference."
        },
        {
          "name": "PickedAsset",
          "slug": "picked-asset",
          "kind": "type",
          "declaration": "type PickedAsset = {\n    readonly uri: string;\n    /**\n     * 기기 라이브러리 원본 식별자(iOS PhotoKit localIdentifier / Android MediaStore id).\n     * 없을 수 있다(웹 File 드롭, 카메라 캡처).\n     *\n     * ⚠ **왜 필요한가**: 소비자의 dedup 1차 키다 —\n     *   `apps/mobile/src/photos/pendingPhotos.ts:43`\n     *   ``pickerAssetDedupKey = `asset:${asset.assetId ?? asset.uri}` ``\n     * uri 폴백만 남으면 스테이징 사본 uri가 resolve마다 재생성되므로(§7 하드닝 2) 같은 사진을\n     * 두 번 선택했을 때 dedup이 통과해버린다. 전신은 `resolveDeviceAssetForUpload`가\n     * `assetId: asset.id`를 채워 device 자산과 picker 자산의 동일성을 유지했다\n     *   — devicePhotoLibrary.ts:364. 새 설계에서는 `toPickedAsset`이 그 역할을 한다(§5.4-④).\n     */\n    readonly assetId?: string | undefined;\n    readonly fileName?: string | undefined;\n    readonly mimeType?: string | undefined;\n    readonly width?: number | undefined;\n    readonly height?: number | undefined;\n    /**\n     * ⚠ 어댑터는 **원시값 그대로** 넘긴다. 네이티브는 ms, 웹은 s이며 정규화는 core가 한다\n     * (§7 하드닝 4 — 정규화 지점을 하나로 고정. 어댑터가 변환하면 이중 변환이 된다).\n     *\n     * 전신 사고: expo-image-picker 웹이 `HTMLVideoElement.duration`(초)을 그대로 전달해\n     * 20분 영상이 1200ms로 저장됐고, 어떤 길이 캡도 그것을 통과시켰다.\n     */\n    readonly durationRaw?: number | undefined;\n    readonly exif?: Readonly<Record<string, unknown>> | undefined;\n    /**\n     * 어댑터가 \"실제로 스트리밍될 파일\"을 stat해 확인한 크기. 있으면 최우선.\n     * 전신의 뒷문 프로퍼티 `__photoKitVerifiedSizeBytes`를 정식 필드로 승격(§7 하드닝 3).\n     */\n    readonly verifiedSizeBytes?: number | undefined;\n    /**\n     * ⚠ 신뢰 금지 — 최후 폴백이다(§7 하드닝 3).\n     * Android가 `quality < 1`로 재인코딩할 때 `asset.fileSize`는 **원본** 크기를 보고하므로\n     * 스토리지가 실제로 받은 바이트와 어긋나 서버가 업로드를 거절한다.\n     * 필드명이 곧 신뢰도다: `verifiedSizeBytes`(실측) > file-system stat > `reportedSizeBytes`(자칭).\n     */\n    readonly reportedSizeBytes?: number | undefined;\n};"
        },
        {
          "name": "PickerAdapter",
          "slug": "picker-adapter",
          "kind": "interface",
          "declaration": "interface PickerAdapter {\n    requestLibraryPermission(kinds: readonly MediaKind[]): Promise<MediaPermission>;\n    requestCameraPermission(): Promise<MediaPermission>;\n    /**\n     * ⚠ iOS 원본 fast path 고정 조합(§7.1): `quality: 1` · `exif: true` ·\n     * `allowsEditing: false` · `preferredAssetRepresentationMode: Current`.\n     * **단일선택/다중선택이 달라지면 안 된다** — 한쪽만 재인코딩 경로로 빠지면 같은 사진이\n     * 선택 방식에 따라 다른 바이트로 업로드된다.\n     */\n    pickFromLibrary(input: {\n        readonly kinds: readonly MediaKind[];\n        readonly max: number;\n    }): Promise<readonly PickedAsset[]>;\n    /**\n     * 카메라 캡처.\n     * ⚠ 상위 플로우(`captureAndUpload`)는 **항상 최대 1건**으로 자른다\n     * (전신 uploader.ts:1008 `result.assets.slice(0, 1)`).\n     */\n    capture(input: {\n        readonly kind: MediaKind;\n    }): Promise<readonly PickedAsset[]>;\n}"
        },
        {
          "name": "PickerFlows",
          "slug": "picker-flows",
          "kind": "interface",
          "declaration": "interface PickerFlows<TAsset, TCollectionId extends string = string> {\n    pick(options?: {\n        readonly max?: number | undefined;\n        readonly kinds?: readonly MediaKind[] | undefined;\n    } | undefined): Promise<readonly PickedAsset[]>;\n    pickAndUpload(options?: PickUploadOptions<TCollectionId> | undefined): Promise<readonly UploadResult<TAsset>[]>;\n    /**\n     * ⚠ 항상 **최대 1건**이다(전신 uploader.ts:1008 `result.assets.slice(0, 1)`).\n     * 무시되는 옵션은 그 자체로 함정이므로 옵션 타입에서 `max`를 Omit한다(§5.4.1-12).\n     */\n    captureAndUpload(options?: (Omit<PickUploadOptions<TCollectionId>, 'max'> & {\n        readonly kind?: MediaKind | undefined;\n    }) | undefined): Promise<readonly UploadResult<TAsset>[]>;\n}"
        },
        {
          "name": "PickUploadOptions",
          "slug": "pick-upload-options",
          "kind": "type",
          "declaration": "type PickUploadOptions<TCollectionId extends string = string> = {\n    readonly collectionId?: TCollectionId | null | undefined;\n    readonly max?: number | undefined;\n    readonly kinds?: readonly MediaKind[] | undefined;\n};"
        },
        {
          "name": "PlatformAdapter",
          "slug": "platform-adapter",
          "kind": "interface",
          "declaration": "/**\n * core에서 `react-native` import를 제거하는 유일한 이유. 필드 2개뿐인 것이 정상이다.\n *\n * 전신 `debug.ts`는 `Platform`을 직접 import했고, 그 한 줄 때문에 서명 URL 새니타이저\n * (§7 하드닝 8)를 순수 유닛으로 검증할 수 없었다. 주입으로 바꾸면서 debug 모듈이 core로 하강했다.\n */\ninterface PlatformAdapter {\n    readonly os: MediaPlatform;\n    /** `__DEV__` 상당. 디버그 로거 게이트(§7 하드닝 8). expo 기본값: `__DEV__ && NODE_ENV !== 'test'`. */\n    readonly isDev: boolean;\n}",
          "sourceDocumentation": "core에서 `react-native` import를 제거하는 유일한 이유. 필드 2개뿐인 것이 정상이다.\n\n전신 `debug.ts`는 `Platform`을 직접 import했고, 그 한 줄 때문에 서명 URL 새니타이저\n(§7 하드닝 8)를 순수 유닛으로 검증할 수 없었다. 주입으로 바꾸면서 debug 모듈이 core로 하강했다."
        },
        {
          "name": "POSTER_CONTENT_TYPE",
          "slug": "poster-content-type",
          "kind": "constant",
          "declaration": "POSTER_CONTENT_TYPE: 'image/jpeg'",
          "sourceDocumentation": "포스터 contentType. 전신 `videoPoster.ts:4` `VIDEO_POSTER_CONTENT_TYPE`의 공개 계승(§5.4.1-7).\npresign 요청의 contentType과 서버 검증이 맞물리므로 **소비자가 읽을 수 있어야 한다**."
        },
        {
          "name": "PutRequest",
          "slug": "put-request",
          "kind": "type",
          "declaration": "type PutRequest = {\n    readonly url: string;\n    readonly method: 'PUT';\n    readonly headers: Readonly<Record<string, string>>;\n};"
        },
        {
          "name": "ResolvedDeviceAsset",
          "slug": "resolved-device-asset",
          "kind": "type",
          "declaration": "/**\n * `DeviceLibrary.resolveForUpload`의 결과 — 업로드 가능한 바이트의 위치(§5.4-④).\n * 전신 `DeviceAssetUploadSource`(devicePhotoLibrary.ts:255-259).\n */\ntype ResolvedDeviceAsset = {\n    readonly uri: string;\n    readonly verifiedSizeBytes?: number | undefined;\n    /** ⚠ 전신 그대로 `null` 가능이다. `PickedAsset.exif`로 옮길 때 undefined 정규화가 필요하다. */\n    readonly exif: Readonly<Record<string, unknown>> | null;\n    /** 스테이징 사본이면 true — 업로드 후 `StagingCache.cleanup` 대상(§7 하드닝 7). */\n    readonly staged: boolean;\n};",
          "sourceDocumentation": "`DeviceLibrary.resolveForUpload`의 결과 — 업로드 가능한 바이트의 위치(§5.4-④).\n전신 `DeviceAssetUploadSource`(devicePhotoLibrary.ts:255-259)."
        },
        {
          "name": "ResolvedPickedAsset",
          "slug": "resolved-picked-asset",
          "kind": "type",
          "declaration": "/** `DeviceLibrary.resolvePickedAsset` / `toPickedAsset`의 결과(§5.4-④). */\ntype ResolvedPickedAsset = PickedAsset & {\n    readonly staged: boolean;\n};",
          "sourceDocumentation": "`DeviceLibrary.resolvePickedAsset` / `toPickedAsset`의 결과(§5.4-④)."
        },
        {
          "name": "resolveUploadSize",
          "slug": "resolve-upload-size",
          "kind": "function",
          "declaration": "/**\n * 세 후보 중 가장 신뢰도 높은 것을 고른다. 전부 없으면 `null` —\n * 호출자가 `imageSizeUnknown`/`videoSizeUnknown` 문구로 실패시킨다(§6.1-⑮, 전신 유지).\n *\n * ⚠ 순서를 바꾸면 Android 재인코딩 자산이 다시 원본 크기로 presign된다.\n */\ndeclare function resolveUploadSize(input: {\n    /** 어댑터가 \"실제로 스트리밍될 파일\"을 stat해 확인한 크기(§3.3 `PickedAsset.verifiedSizeBytes`). */\n    readonly verifiedSizeBytes?: number | undefined;\n    /** 코어가 `FileSystemAdapter.stat`으로 방금 읽은 크기. */\n    readonly statSizeBytes?: number | undefined;\n    /** ⚠ 피커 자칭(`asset.fileSize`). 최후 폴백이며 단독으로는 신뢰하지 않는다. */\n    readonly reportedSizeBytes?: number | undefined;\n}): {\n    readonly sizeBytes: number;\n    readonly source: UploadSizeSource;\n} | null;",
          "sourceDocumentation": "세 후보 중 가장 신뢰도 높은 것을 고른다. 전부 없으면 `null` —\n호출자가 `imageSizeUnknown`/`videoSizeUnknown` 문구로 실패시킨다(§6.1-⑮, 전신 유지).\n\n⚠ 순서를 바꾸면 Android 재인코딩 자산이 다시 원본 크기로 presign된다."
        },
        {
          "name": "sanitizeMediaErrorMessage",
          "slug": "sanitize-media-error-message",
          "kind": "function",
          "declaration": "/**\n * 전신 `sanitizePhotoErrorMessage`.\n *\n * iOS URLSession 실패는 **서명 업로드 URL 전문을 그대로 에코**한다. 그 쿼리에는 임시 자격증명이\n * 들어 있으므로 개발자 로그에도 활동 로그에도 남겨선 안 된다. 플랫폼 에러 코드와 설명은 남기고\n * URL만 `[URL]`로 치환한 뒤 1000자에서 자른다.\n */\ndeclare function sanitizeMediaErrorMessage(message: string): string;",
          "sourceDocumentation": "전신 `sanitizePhotoErrorMessage`.\n\niOS URLSession 실패는 **서명 업로드 URL 전문을 그대로 에코**한다. 그 쿼리에는 임시 자격증명이\n들어 있으므로 개발자 로그에도 활동 로그에도 남겨선 안 된다. 플랫폼 에러 코드와 설명은 남기고\nURL만 `[URL]`로 치환한 뒤 1000자에서 자른다."
        },
        {
          "name": "SaveableMedia",
          "slug": "saveable-media",
          "kind": "type",
          "declaration": "type SaveableMedia = {\n    /**\n     * 안정 파일명의 1차 소스(G8). 없거나 빈 문자열이면 배열 인덱스+1로 폴백한다 —\n     * 전신 규칙 `${prefix}-${image.id || index + 1}.${ext}`(saveImages.ts:71) 보존.\n     */\n    readonly id?: string | undefined;\n    /**\n     * ⚠ **단일 진실이다.** `originalUrl || thumbnailUrl` 같은 폴백은 호스트 DTO 지식이므로\n     * 라이브러리가 아니라 앱이 소유한다(전신 `imageDownloadUrl` 폐지 — §5.7.3).\n     */\n    readonly url: string;\n    readonly fileName?: string | undefined;\n    readonly contentType?: MediaContentType | undefined;\n};"
        },
        {
          "name": "SaveResult",
          "slug": "save-result",
          "kind": "type",
          "declaration": "type SaveResult = {\n    readonly savedCount: number;\n    readonly mode: SaveTarget['kind'];\n};"
        },
        {
          "name": "SaveTarget",
          "slug": "save-target",
          "kind": "type",
          "declaration": "/**\n * 저장 타깃(§6.1-⑦).\n * 전신은 `platformOS:'web'` + `mediaLibrary` 동시 주입이 통과했고, 결과로 보고되는 `mode`와\n * 실제 동작이 어긋날 수 있었다. 판별 유니언이면 무효 조합이 **표현 불가능**해지고\n * `SaveResult.mode`가 `target.kind`에서 파생되므로 보고와 실동작이 어긋날 수 없다.\n */\ntype SaveTarget = {\n    readonly kind: 'media-library';\n    readonly files: FileSystemAdapter & FileDownloadAdapter;\n    readonly library: MediaLibrarySaveAdapter;\n} | {\n    readonly kind: 'browser-download';\n    readonly browser: BrowserSaveAdapter;\n};",
          "sourceDocumentation": "저장 타깃(§6.1-⑦).\n전신은 `platformOS:'web'` + `mediaLibrary` 동시 주입이 통과했고, 결과로 보고되는 `mode`와\n실제 동작이 어긋날 수 있었다. 판별 유니언이면 무효 조합이 **표현 불가능**해지고\n`SaveResult.mode`가 `target.kind`에서 파생되므로 보고와 실동작이 어긋날 수 없다."
        },
        {
          "name": "Sha256Hasher",
          "slug": "sha256-hasher",
          "kind": "interface",
          "declaration": "/**\n * 증분 해셔. `js-sha256`의 `sha256.create()`와 같은 형태라 전신 호출부가 그대로 옮겨진다.\n *\n * ⚠ `hex()`는 **종료 연산**이다(패딩을 상태에 써 넣는다). 두 번 이상 부르면 같은 값을 돌려주고,\n * 종료 후의 `update()`는 무시한다 — 부분적으로 갱신된 다이제스트를 돌려주는 것보다 낫다.\n */\ninterface Sha256Hasher {\n    update(bytes: Uint8Array): void;\n    hex(): string;\n}",
          "sourceDocumentation": "증분 해셔. `js-sha256`의 `sha256.create()`와 같은 형태라 전신 호출부가 그대로 옮겨진다.\n\n⚠ `hex()`는 **종료 연산**이다(패딩을 상태에 써 넣는다). 두 번 이상 부르면 같은 값을 돌려주고,\n종료 후의 `update()`는 무시한다 — 부분적으로 갱신된 다이제스트를 돌려주는 것보다 낫다."
        },
        {
          "name": "sha256Hex",
          "slug": "sha256-hex",
          "kind": "function",
          "declaration": "/** 단발 해시. 증분 경로와 **같은 구현**을 쓰므로 둘이 어긋날 수 없다. */\ndeclare function sha256Hex(bytes: Uint8Array): string;",
          "sourceDocumentation": "단발 해시. 증분 경로와 **같은 구현**을 쓰므로 둘이 어긋날 수 없다."
        },
        {
          "name": "StagingCache",
          "slug": "staging-cache",
          "kind": "interface",
          "declaration": "interface StagingCache extends Brand<'StagingCache'> {\n    /** `${namespace}-upload-`. */\n    readonly prefix: string;\n    /**\n     * cleanup의 유일한 판정 근거.\n     *\n     * 전신은 `uri.includes(PREFIX)` 한 줄이었다(deviceUploadCache.ts:29). 그 술어는\n     * `file:///other/dir/memorylog-upload-x.jpg`처럼 **우리가 만들지 않은 경로**도 참으로 만들고,\n     * 프리픽스가 중간에 낀 임의의 경로까지 삭제 대상에 넣는다. 3조건으로 좁힌다:\n     *   (i) 앱 캐시 디렉토리로 시작 (ii) 파일명이 prefix로 **시작** (iii) 하위 경로 없음\n     */\n    owns(uri: string | null | undefined): boolean;\n    /** 스테이징 사본이 놓일 자리. 캐시 디렉토리가 없으면 `null`. */\n    uriFor(asset: DeviceAssetRef): string | null;\n    /**\n     * 스테이징 사본 삭제. `owns()`가 false면 **no-op**이다.\n     * 실패는 삼킨다 — 누수의 대가는 디스크 비용뿐이고, 정리 실패로 업로드 결과를 뒤집을 이유가 없다.\n     */\n    cleanup(uri: string | null | undefined): Promise<void>;\n}"
        },
        {
          "name": "summarizeUri",
          "slug": "summarize-uri",
          "kind": "function",
          "declaration": "/**\n * ⚠ **원문 URI를 절대 로깅하지 않는다.** 서명 업로드 URL은 쿼리에 임시 자격증명을 담고,\n * 사진 보관함 경로는 사용자의 미디어를 식별한다. 모양(scheme·확장자·길이·종류)만 남긴다.\n *\n * `hardening-guard`가 로거 인자에 `uri`/`url` 원문을 넘기는 것을 정적으로 차단한다 —\n * 이 함수를 **경유하지 않은** 전달은 실패다(§10.3).\n */\ndeclare function summarizeUri(uri?: string | null): {\n    readonly scheme: string;\n    readonly extension: string | null;\n    readonly length: number;\n    readonly isFile: boolean;\n    readonly isContent: boolean;\n    readonly isPhotoKit: boolean;\n} | null;",
          "sourceDocumentation": "⚠ **원문 URI를 절대 로깅하지 않는다.** 서명 업로드 URL은 쿼리에 임시 자격증명을 담고,\n사진 보관함 경로는 사용자의 미디어를 식별한다. 모양(scheme·확장자·길이·종류)만 남긴다.\n\n`hardening-guard`가 로거 인자에 `uri`/`url` 원문을 넘기는 것을 정적으로 차단한다 —\n이 함수를 **경유하지 않은** 전달은 실패다(§10.3)."
        },
        {
          "name": "toPickedAsset",
          "slug": "to-picked-asset",
          "kind": "function",
          "declaration": "/**\n * `DeviceAsset`의 정체성(id·filename·치수)과 resolve 결과(바이트 위치·실측 크기·EXIF)를 합쳐\n * 피커 업로드 경로가 받는 형태를 만든다.\n *\n * ⚠ **`exif: resolved.exif ?? undefined` — null → undefined 정규화가 필수다.**\n * EOP(`exactOptionalPropertyTypes`) 하에서 `ResolvedDeviceAsset.exif`는\n * `Readonly<Record<string, unknown>> | null`이고 `PickedAsset.exif`는\n * `?: Readonly<Record<string, unknown>> | undefined`라 **null을 그대로 대입하면 TS2322**다\n * (설계 문서 §5.4-④의 실측 확인). 전신은 `exif: null`을 그대로 흘렸다.\n *\n * ⚠ **`reportedSizeBytes`는 채우지 않는다.** 기기 경로에서 크기의 진실은 `verifiedSizeBytes`\n * (= 실제로 스트리밍될 캐시 사본을 stat한 값)뿐이다. 자칭 크기를 여기서 끼워 넣으면\n * §7 하드닝 3이 세운 신뢰도 서열(verified > file-system stat > reported)이 무너지고,\n * Android 재인코딩 자산에서 서버가 업로드를 거절하던 그 사고가 그대로 재발한다.\n *\n * ⚠ **`assetId`를 반드시 채운다.** 소비자의 dedup 1차 키이기 때문이다(§3.3-⑥):\n * `assetId`가 없으면 dedup이 스테이징 사본 uri로 폴백하는데, 그 uri는 resolve마다 재생성되므로\n * 같은 사진을 두 번 선택해도 dedup이 통과해버린다.\n */\ndeclare function toPickedAsset(asset: DeviceAsset, resolved: ResolvedDeviceAsset): ResolvedPickedAsset;",
          "sourceDocumentation": "`DeviceAsset`의 정체성(id·filename·치수)과 resolve 결과(바이트 위치·실측 크기·EXIF)를 합쳐\n피커 업로드 경로가 받는 형태를 만든다.\n\n⚠ **`exif: resolved.exif ?? undefined` — null → undefined 정규화가 필수다.**\nEOP(`exactOptionalPropertyTypes`) 하에서 `ResolvedDeviceAsset.exif`는\n`Readonly<Record<string, unknown>> | null`이고 `PickedAsset.exif`는\n`?: Readonly<Record<string, unknown>> | undefined`라 **null을 그대로 대입하면 TS2322**다\n(설계 문서 §5.4-④의 실측 확인). 전신은 `exif: null`을 그대로 흘렸다.\n\n⚠ **`reportedSizeBytes`는 채우지 않는다.** 기기 경로에서 크기의 진실은 `verifiedSizeBytes`\n(= 실제로 스트리밍될 캐시 사본을 stat한 값)뿐이다. 자칭 크기를 여기서 끼워 넣으면\n§7 하드닝 3이 세운 신뢰도 서열(verified > file-system stat > reported)이 무너지고,\nAndroid 재인코딩 자산에서 서버가 업로드를 거절하던 그 사고가 그대로 재발한다.\n\n⚠ **`assetId`를 반드시 채운다.** 소비자의 dedup 1차 키이기 때문이다(§3.3-⑥):\n`assetId`가 없으면 dedup이 스테이징 사본 uri로 폴백하는데, 그 uri는 resolve마다 재생성되므로\n같은 사진을 두 번 선택해도 dedup이 통과해버린다."
        },
        {
          "name": "UploadedPoster",
          "slug": "uploaded-poster",
          "kind": "type",
          "declaration": "/**\n * 구 `posterObjectName` / `posterSizeBytes` 2필드를 쌍 객체로 통합(§6.1-②).\n * 한쪽만 채워 보내면 서버가 반쪽 메타로 등록해 **썸네일이 영구 누락**된다 — 그 상태를\n * 표현 불가능하게 만든다.\n */\ntype UploadedPoster = {\n    readonly objectName: string;\n    readonly sizeBytes: number;\n};",
          "sourceDocumentation": "구 `posterObjectName` / `posterSizeBytes` 2필드를 쌍 객체로 통합(§6.1-②).\n한쪽만 채워 보내면 서버가 반쪽 메타로 등록해 **썸네일이 영구 누락**된다 — 그 상태를\n표현 불가능하게 만든다."
        },
        {
          "name": "UploadResult",
          "slug": "upload-result",
          "kind": "type",
          "declaration": "/**\n * 전신 `CompletedPhotoUpload<TAsset>`.\n * ⚠ `duplicate`는 **필수**다(§6.1-⑯). 옵셔널이면 호스트가 중복 판정을 반환하지 않을 때\n * 킷이 \"새로 만들어졌다\"로 오독하고, 중복 취소 경로가 **사용자의 예전 사진을 지운다**.\n */\ntype UploadResult<TAsset> = {\n    readonly asset: TAsset;\n    readonly duplicate: boolean;\n};",
          "sourceDocumentation": "전신 `CompletedPhotoUpload<TAsset>`.\n⚠ `duplicate`는 **필수**다(§6.1-⑯). 옵셔널이면 호스트가 중복 판정을 반환하지 않을 때\n킷이 \"새로 만들어졌다\"로 오독하고, 중복 취소 경로가 **사용자의 예전 사진을 지운다**."
        },
        {
          "name": "UploadSizeSource",
          "slug": "upload-size-source",
          "kind": "type",
          "declaration": "/** 크기를 어느 근거로 정했는지. 텔레메트리·디버그 로그의 `sizeSource`에 그대로 쓰인다. */\ntype UploadSizeSource = 'verified' | 'file-system' | 'reported';",
          "sourceDocumentation": "크기를 어느 근거로 정했는지. 텔레메트리·디버그 로그의 `sizeSource`에 그대로 쓰인다."
        },
        {
          "name": "VideoContentType",
          "slug": "video-content-type",
          "kind": "type",
          "declaration": "type VideoContentType = Extract<MediaContentType, `video/${string}`>;"
        }
      ]
    },
    {
      "subpath": "./device",
      "id": "device",
      "declarationTarget": "./dist/device.d.cts",
      "symbols": [
        {
          "name": "expoDeviceLibrary",
          "slug": "expo-device-library",
          "kind": "function",
          "declaration": "/**\n * expo 기기 라이브러리 어댑터(§5.6).\n *\n * peer: `expo-media-library`(`/legacy` 서브패스). `\".\"`·`\"./core\"`는 이 모듈을 **문자열로도**\n * 참조하지 않으므로, 소비자가 `\"./device\"`를 import하지 않으면 Metro는 이 peer의 해석을\n * 시도조차 하지 않는다(§3.2 — `dist-peer-graph` 가드가 CI에서 단언한다).\n */\ndeclare function expoDeviceLibrary(): DeviceLibraryAdapter;",
          "sourceDocumentation": "expo 기기 라이브러리 어댑터(§5.6).\n\npeer: `expo-media-library`(`/legacy` 서브패스). `\".\"`·`\"./core\"`는 이 모듈을 **문자열로도**\n참조하지 않으므로, 소비자가 `\"./device\"`를 import하지 않으면 Metro는 이 peer의 해석을\n시도조차 하지 않는다(§3.2 — `dist-peer-graph` 가드가 CI에서 단언한다)."
        }
      ]
    },
    {
      "subpath": "./image",
      "id": "image",
      "declarationTarget": "./dist/image.d.cts",
      "symbols": [
        {
          "name": "createExpoImageProcessor",
          "slug": "create-expo-image-processor",
          "kind": "function",
          "declaration": "/** Creates the Expo-native image processor. Import only from `@gj-kit/expo-media/image`. */\ndeclare function createExpoImageProcessor(options?: CreateExpoImageProcessorOptions): ExpoImageProcessor;",
          "sourceDocumentation": "Creates the Expo-native image processor. Import only from `@gj-kit/expo-media/image`."
        },
        {
          "name": "CreateExpoImageProcessorOptions",
          "slug": "create-expo-image-processor-options",
          "kind": "interface",
          "declaration": "interface CreateExpoImageProcessorOptions {\n    readonly strings?: MediaStrings | undefined;\n}"
        },
        {
          "name": "CropDisplayedImageInput",
          "slug": "crop-displayed-image-input",
          "kind": "interface",
          "declaration": "interface CropDisplayedImageInput extends ImageProcessingOptions {\n    readonly uri: string;\n    /** Dimensions React Native used to render the source image. */\n    readonly sourceSize: ImageSize;\n    readonly cropRect: ImageRect;\n    readonly renderedImageRect: ImageRect;\n}"
        },
        {
          "name": "ExpoImageProcessor",
          "slug": "expo-image-processor",
          "kind": "interface",
          "declaration": "interface ExpoImageProcessor {\n    getImageSize(uri: string): Promise<ImageSize>;\n    /** Re-encodes once so the platform decoder applies EXIF orientation; never rotate from EXIF manually. */\n    normalizeOrientation(uri: string): Promise<ProcessedImage>;\n    /** Re-encodes to JPEG and reduces only when the source exceeds `maxWidth`. */\n    resizeToMaxWidth(input: ResizeImageToMaxWidthInput): Promise<ProcessedImage>;\n    rotate(input: RotateImageInput): Promise<ProcessedImage>;\n    /** Crops display coordinates; Android first normalizes the raster to the displayed source dimensions. */\n    cropDisplayed(input: CropDisplayedImageInput): Promise<ProcessedImage>;\n}"
        },
        {
          "name": "ImageProcessingOptions",
          "slug": "image-processing-options",
          "kind": "interface",
          "declaration": "interface ImageProcessingOptions {\n    /** JPEG compression in Expo's 0..1 range. */\n    readonly compress?: number | undefined;\n}"
        },
        {
          "name": "ImageRect",
          "slug": "image-rect",
          "kind": "interface",
          "declaration": "/** A rectangle in the rendered image coordinate space. */\ninterface ImageRect {\n    readonly x: number;\n    readonly y: number;\n    readonly width: number;\n    readonly height: number;\n}",
          "sourceDocumentation": "A rectangle in the rendered image coordinate space."
        },
        {
          "name": "ImageSize",
          "slug": "image-size",
          "kind": "interface",
          "declaration": "/** Dependency-free geometry and resize decisions for `@gj-kit/expo-media/image`. */\ninterface ImageSize {\n    readonly width: number;\n    readonly height: number;\n}",
          "sourceDocumentation": "Dependency-free geometry and resize decisions for `@gj-kit/expo-media/image`."
        },
        {
          "name": "PixelCropInput",
          "slug": "pixel-crop-input",
          "kind": "interface",
          "declaration": "interface PixelCropInput {\n    readonly sourceSize: ImageSize;\n    readonly cropRect: ImageRect;\n    readonly renderedImageRect: ImageRect;\n}"
        },
        {
          "name": "PixelCropRect",
          "slug": "pixel-crop-rect",
          "kind": "interface",
          "declaration": "/** A crop rectangle in source-image pixels for expo-image-manipulator. */\ninterface PixelCropRect {\n    readonly originX: number;\n    readonly originY: number;\n    readonly width: number;\n    readonly height: number;\n}",
          "sourceDocumentation": "A crop rectangle in source-image pixels for expo-image-manipulator."
        },
        {
          "name": "ProcessedImage",
          "slug": "processed-image",
          "kind": "interface",
          "declaration": "interface ProcessedImage {\n    readonly uri: string;\n}"
        },
        {
          "name": "ResizeImageToMaxWidthInput",
          "slug": "resize-image-to-max-width-input",
          "kind": "interface",
          "declaration": "interface ResizeImageToMaxWidthInput extends ImageProcessingOptions {\n    readonly uri: string;\n    readonly maxWidth: number;\n}"
        },
        {
          "name": "RotateImageInput",
          "slug": "rotate-image-input",
          "kind": "interface",
          "declaration": "interface RotateImageInput extends ImageProcessingOptions {\n    readonly uri: string;\n    readonly degrees: number;\n}"
        }
      ]
    },
    {
      "subpath": "./image/pure",
      "id": "image--pure",
      "declarationTarget": "./dist/image/pure.d.cts",
      "symbols": [
        {
          "name": "ImageRect",
          "slug": "image-rect",
          "kind": "interface",
          "declaration": "/** A rectangle in the rendered image coordinate space. */\ninterface ImageRect {\n    readonly x: number;\n    readonly y: number;\n    readonly width: number;\n    readonly height: number;\n}",
          "sourceDocumentation": "A rectangle in the rendered image coordinate space."
        },
        {
          "name": "ImageSize",
          "slug": "image-size",
          "kind": "interface",
          "declaration": "/** Dependency-free geometry and resize decisions for `@gj-kit/expo-media/image`. */\ninterface ImageSize {\n    readonly width: number;\n    readonly height: number;\n}",
          "sourceDocumentation": "Dependency-free geometry and resize decisions for `@gj-kit/expo-media/image`."
        },
        {
          "name": "PixelCropInput",
          "slug": "pixel-crop-input",
          "kind": "interface",
          "declaration": "interface PixelCropInput {\n    readonly sourceSize: ImageSize;\n    readonly cropRect: ImageRect;\n    readonly renderedImageRect: ImageRect;\n}"
        },
        {
          "name": "PixelCropRect",
          "slug": "pixel-crop-rect",
          "kind": "interface",
          "declaration": "/** A crop rectangle in source-image pixels for expo-image-manipulator. */\ninterface PixelCropRect {\n    readonly originX: number;\n    readonly originY: number;\n    readonly width: number;\n    readonly height: number;\n}",
          "sourceDocumentation": "A crop rectangle in source-image pixels for expo-image-manipulator."
        },
        {
          "name": "shouldResizeToMaxWidth",
          "slug": "should-resize-to-max-width",
          "kind": "function",
          "declaration": "/** Returns true only when reducing the source width; it never requests an upscale. */\ndeclare function shouldResizeToMaxWidth(sourceWidth: number, maxWidth: number): boolean;",
          "sourceDocumentation": "Returns true only when reducing the source width; it never requests an upscale."
        },
        {
          "name": "toPixelCropRect",
          "slug": "to-pixel-crop-rect",
          "kind": "function",
          "declaration": "/**\n * Converts a crop drawn over a scaled or zoomed image into source-image pixels.\n *\n * The caller owns gesture constraints; this function preserves the existing\n * display-to-source mapping and clamps the ending edge to source bounds.\n */\ndeclare function toPixelCropRect({ sourceSize, cropRect, renderedImageRect, }: PixelCropInput): PixelCropRect;",
          "sourceDocumentation": "Converts a crop drawn over a scaled or zoomed image into source-image pixels.\n\nThe caller owns gesture constraints; this function preserves the existing\ndisplay-to-source mapping and clamps the ending edge to source bounds."
        }
      ]
    },
    {
      "subpath": "./picker",
      "id": "picker",
      "declarationTarget": "./dist/picker.d.cts",
      "symbols": [
        {
          "name": "expoPicker",
          "slug": "expo-picker",
          "kind": "function",
          "declaration": "/** §5.6 — `\"./picker\"`의 유일한 공개 심볼. */\ndeclare function expoPicker(options?: {\n    /**\n     * 기본 `true` — iOS PhotoKit 원본 표현 fast path(§7.1).\n     *\n     * `false`로 내리면 `preferredAssetRepresentationMode`만 빠져 시스템이 표현을 고르게 된다\n     * (HEIC → JPEG 트랜스코딩이 일어날 수 있다). `quality`·`exif`·`allowsEditing` 세 값은\n     * **어떤 경우에도 바뀌지 않는다** — 그 셋이 흔들리면 크기 정합(§7 하드닝 3)과 메타데이터가\n     * 함께 깨지기 때문이다. 즉 이 옵션은 fast path의 on/off이지 옵션 가방이 아니다.\n     */\n    readonly preferOriginalRepresentation?: boolean | undefined;\n    /**\n     * When original PhotoKit data is unsuitable for an app-owned processing flow, ask iOS for a\n     * compatible representation. This keeps the default upload-oriented original fast path intact.\n     */\n    readonly preferCompatibleRepresentation?: boolean | undefined;\n    /**\n     * Retry a failed single-image library launch with UIImagePickerController's editing flow.\n     * This is useful for iCloud/HEIC assets that PHPicker cannot materialize. The retry is limited\n     * to one selection because Expo does not support editing together with multi-selection.\n     */\n    readonly retryWithEditingOnError?: boolean | undefined;\n}): PickerAdapter;",
          "sourceDocumentation": "§5.6 — `\"./picker\"`의 유일한 공개 심볼."
        }
      ]
    },
    {
      "subpath": "./save",
      "id": "save",
      "declarationTarget": "./dist/save.d.cts",
      "symbols": [
        {
          "name": "expoDeviceSave",
          "slug": "expo-device-save",
          "kind": "function",
          "declaration": "/**\n * expo 기기 저장 어댑터(§5.6).\n *\n * @param input.isExpoGo `Constants.appOwnership === 'expo'` 판정 결과.\n *   ⚠ **호스트가 판정하고 값만 넘긴다.** 이것이 라이브러리에서 `expo-constants` 의존을 완전히\n *   걷어내는 지점이다(§0.2). 전신은 `saveImages.ts:96`에서 `Constants`를 직접 import했고,\n *   그 한 줄 때문에 저장 기능을 쓰지 않는 소비자까지 peer 하나를 더 짊어졌다.\n */\ndeclare function expoDeviceSave(input?: {\n    readonly isExpoGo?: boolean | undefined;\n} | undefined): MediaLibrarySaveAdapter;",
          "sourceDocumentation": "expo 기기 저장 어댑터(§5.6)."
        }
      ]
    },
    {
      "subpath": "./storage",
      "id": "storage",
      "declarationTarget": "./dist/storage.d.cts",
      "symbols": [
        {
          "name": "createExpoDocumentFileStore",
          "slug": "create-expo-document-file-store",
          "kind": "function",
          "declaration": "/**\n * App-owned persistent local-file store backed by Expo's document directory.\n *\n * It deliberately does not share the cache-backed upload staging store: files\n * returned from `copy()` are safe to persist in an application's database.\n */\ndeclare function createExpoDocumentFileStore(input: {\n    readonly root: string;\n}): DurableFileStore;",
          "sourceDocumentation": "App-owned persistent local-file store backed by Expo's document directory.\n\nIt deliberately does not share the cache-backed upload staging store: files\nreturned from `copy()` are safe to persist in an application's database."
        },
        {
          "name": "DURABLE_FILE_ERROR_CODES",
          "slug": "durable-file-error-codes",
          "kind": "constant",
          "declaration": "DURABLE_FILE_ERROR_CODES: readonly [\n    \"durable-file-invalid-path\",\n    \"durable-file-root-unavailable\",\n    \"durable-file-source-required\",\n    \"durable-file-source-not-found\",\n    \"durable-file-destination-exists\",\n    \"durable-file-copy-empty\",\n    \"durable-file-copy-size-mismatch\",\n    \"durable-file-copy-failed\",\n    \"durable-file-unsupported-asset\"\n]",
          "sourceDocumentation": "Durable-file failures are deliberately independent from `MediaError`.\n\nA durable attachment store has no user-facing upload policy, and its host\nadapter can receive local URIs that must never reach an error boundary or a\nlog. Every code below is therefore actionable without exposing a source or\ndestination URI."
        },
        {
          "name": "DurableFile",
          "slug": "durable-file",
          "kind": "type",
          "declaration": "type DurableFile = {\n    readonly uri: string;\n    readonly sizeBytes: number;\n};"
        },
        {
          "name": "DurableFileError",
          "slug": "durable-file-error",
          "kind": "class",
          "declaration": "/** A URI-safe error emitted by durable attachment storage. */\ndeclare class DurableFileError extends Error {\n    readonly code: DurableFileErrorCode;\n    constructor(code: DurableFileErrorCode);\n}",
          "sourceDocumentation": "A URI-safe error emitted by durable attachment storage."
        },
        {
          "name": "DurableFileErrorCode",
          "slug": "durable-file-error-code",
          "kind": "type",
          "declaration": "type DurableFileErrorCode = (typeof DURABLE_FILE_ERROR_CODES)[number];"
        },
        {
          "name": "DurableFileStore",
          "slug": "durable-file-store",
          "kind": "interface",
          "declaration": "interface DurableFileStore {\n    /** True only for a safe, non-empty file path rooted in this store. */\n    owns(uri: string | null | undefined): boolean;\n    /** Atomically-enough copy: a failed, empty, or size-mismatched destination is removed before rejection. */\n    copy(input: DurableFileStoreCopyInput): Promise<DurableFile>;\n    /** Copy a picker result while retaining a supported, validated media extension. */\n    copyPickedAsset(input: DurablePickedAssetCopyInput): Promise<DurablePickedAssetFile>;\n    /** Best-effort no-op for paths outside this store. */\n    remove(uri: string | null | undefined): Promise<void>;\n}"
        },
        {
          "name": "DurableFileStoreCopyInput",
          "slug": "durable-file-store-copy-input",
          "kind": "type",
          "declaration": "type DurableFileStoreCopyInput = {\n    readonly sourceUri: string;\n    /** Nested folders below the configured root. Every segment is validated independently. */\n    readonly directory?: readonly string[] | undefined;\n    /** The final file name, including any extension the host chose. */\n    readonly fileName: string;\n};"
        },
        {
          "name": "DurablePickedAssetCopyInput",
          "slug": "durable-picked-asset-copy-input",
          "kind": "type",
          "declaration": "/**\n * Copy a picker result with a caller-owned stable filename stem.\n *\n * The source filename is metadata, not a safe destination path. The store\n * validates `fileNameStem` as a path segment and appends an extension from the\n * selected asset's supported content type. This prevents an app from silently\n * relabeling every picked asset as `.jpg` while retaining its own stable ID.\n */\ntype DurablePickedAssetCopyInput = {\n    readonly asset: PickedAsset;\n    /** Nested folders below the configured root. Every segment is validated independently. */\n    readonly directory?: readonly string[] | undefined;\n    /** Safe, caller-owned destination filename stem; the store adds the validated extension. */\n    readonly fileNameStem: string;\n};",
          "sourceDocumentation": "Copy a picker result with a caller-owned stable filename stem.\n\nThe source filename is metadata, not a safe destination path. The store\nvalidates `fileNameStem` as a path segment and appends an extension from the\nselected asset's supported content type. This prevents an app from silently\nrelabeling every picked asset as `.jpg` while retaining its own stable ID."
        },
        {
          "name": "DurablePickedAssetFile",
          "slug": "durable-picked-asset-file",
          "kind": "type",
          "declaration": "type DurablePickedAssetFile = DurableFile & {\n    readonly fileName: string;\n    readonly contentType: MediaContentType;\n};"
        },
        {
          "name": "isDurableFileError",
          "slug": "is-durable-file-error",
          "kind": "function",
          "declaration": "/** Cross-entry-safe `DurableFileError` guard. */\ndeclare function isDurableFileError(error: unknown): error is DurableFileError;",
          "sourceDocumentation": "Cross-entry-safe `DurableFileError` guard."
        }
      ]
    },
    {
      "subpath": "./testing",
      "id": "testing",
      "declarationTarget": "./dist/testing.d.cts",
      "symbols": [
        {
          "name": "bytesToBase64",
          "slug": "bytes-to-base64",
          "kind": "function",
          "declaration": "/**\n * 바이트 → base64.\n *\n * ⚠ **인메모리 파일시스템이 `readBase64`를 이걸로 구현한다.** 코어의 스트리밍 해시는\n * 이 문자열을 다시 디코드해 SHA-256에 먹이므로, 여기가 틀리면 §7 하드닝 9(3의 배수 창 정렬)의\n * 유닛 검증이 **틀린 기준을 통과**하게 된다 — 페이크가 부실하면 테스트도 부실해지는 지점의 실례다.\n * 그래서 패딩까지 표준 그대로 구현한다.\n */\ndeclare function bytesToBase64(bytes: Uint8Array): string;",
          "sourceDocumentation": "바이트 → base64.\n\n⚠ **인메모리 파일시스템이 `readBase64`를 이걸로 구현한다.** 코어의 스트리밍 해시는\n이 문자열을 다시 디코드해 SHA-256에 먹이므로, 여기가 틀리면 §7 하드닝 9(3의 배수 창 정렬)의\n유닛 검증이 **틀린 기준을 통과**하게 된다 — 페이크가 부실하면 테스트도 부실해지는 지점의 실례다.\n그래서 패딩까지 표준 그대로 구현한다."
        },
        {
          "name": "createBinarySource",
          "slug": "create-binary-source",
          "kind": "function",
          "declaration": "/**\n * `Uint8Array` → `NamedBinarySource`(§3.3).\n *\n * 웹 업로드 경로(`createBinaryUploads`·`uploadDropped`)를 **DOM `Blob` 없이** 태우는 수단이다.\n * `BinarySource`가 `{size, type?, arrayBuffer()}` 구조 최소 타입인 이유가 바로 이것이며\n * (§3.3 주석), 이 함수가 그 설계 의도를 실제로 발화시킨다.\n */\ndeclare function createBinarySource(bytes: Uint8Array, input: {\n    readonly name: string;\n    readonly type?: string | undefined;\n}): NamedBinarySource;",
          "sourceDocumentation": "`Uint8Array` → `NamedBinarySource`(§3.3).\n\n웹 업로드 경로(`createBinaryUploads`·`uploadDropped`)를 **DOM `Blob` 없이** 태우는 수단이다.\n`BinarySource`가 `{size, type?, arrayBuffer()}` 구조 최소 타입인 이유가 바로 이것이며\n(§3.3 주석), 이 함수가 그 설계 의도를 실제로 발화시킨다."
        },
        {
          "name": "createFakeDeviceLibrary",
          "slug": "create-fake-device-library",
          "kind": "function",
          "declaration": "declare function createFakeDeviceLibrary(options?: FakeDeviceLibraryOptions | undefined): FakeDeviceLibrary;"
        },
        {
          "name": "createFakePicker",
          "slug": "create-fake-picker",
          "kind": "function",
          "declaration": "declare function createFakePicker(assets: readonly PickedAsset[], options?: FakePickerOptions | undefined): FakePicker;"
        },
        {
          "name": "createFakePlatform",
          "slug": "create-fake-platform",
          "kind": "function",
          "declaration": "/** 객체 인자 판. `isDev`까지 정할 때 쓴다. 기본값은 `fakePlatform('ios')`와 같다. */\ndeclare function createFakePlatform(input?: {\n    readonly os?: MediaPlatform | undefined;\n    readonly isDev?: boolean | undefined;\n} | undefined): PlatformAdapter;",
          "sourceDocumentation": "객체 인자 판. `isDev`까지 정할 때 쓴다. 기본값은 `fakePlatform('ios')`와 같다."
        },
        {
          "name": "createFakeUploadApi",
          "slug": "create-fake-upload-api",
          "kind": "function",
          "declaration": "declare function createFakeUploadApi<TAsset>(options: FakeUploadApiOptions<TAsset>): FakeUploadApi<TAsset>;"
        },
        {
          "name": "createMemoryFileSystem",
          "slug": "create-memory-file-system",
          "kind": "function",
          "declaration": "declare function createMemoryFileSystem(options?: MemoryFileSystemOptions | undefined): MemoryFileSystem;"
        },
        {
          "name": "createRecordingTelemetry",
          "slug": "create-recording-telemetry",
          "kind": "function",
          "declaration": "declare function createRecordingTelemetry(): RecordingTelemetry;"
        },
        {
          "name": "createRecordingTransport",
          "slug": "create-recording-transport",
          "kind": "function",
          "declaration": "declare function createRecordingTransport(options?: RecordingTransportOptions | undefined): RecordingTransport;"
        },
        {
          "name": "EXIF_CAPTURED_AT",
          "slug": "exif-captured-at",
          "kind": "constant",
          "declaration": "EXIF_CAPTURED_AT = \"2024:01:02 03:04:05\"",
          "sourceDocumentation": "EXIF 원문 벽시계. ⚠ 타임존이 없다 — 그것이 하드닝 11의 전제다."
        },
        {
          "name": "EXIF_FIXTURE",
          "slug": "exif-fixture",
          "kind": "constant",
          "declaration": "EXIF_FIXTURE: Readonly<Record<string, unknown>>",
          "sourceDocumentation": "피커·PhotoKit이 주는 형태의 EXIF dict.\n좌표는 도·분·초 배열이고 부호는 Ref가 정한다(§5.3의 GPS 3형식 중 대표형).\n⚠ 아래 `jpegWithExif()`가 **같은 값**을 바이트로 표현한다 — 두 경로의 동치가 계약이다."
        },
        {
          "name": "EXIF_GEO_POINT",
          "slug": "exif-geo-point",
          "kind": "constant",
          "declaration": "EXIF_GEO_POINT: {\n    readonly latitude: 37.5665;\n    readonly longitude: 126.978;\n}",
          "sourceDocumentation": "`EXIF_FIXTURE`가 표현하는 좌표. `validGeoPoint`의 소수 6자리 반올림까지 반영된 값이다."
        },
        {
          "name": "exifCapturedAtIso",
          "slug": "exif-captured-at-iso",
          "kind": "function",
          "declaration": "/**\n * `EXIF_CAPTURED_AT`을 **기기 로컬 타임존**으로 해석한 ISO 문자열.\n *\n * ⚠ 상수가 아니라 함수인 것이 요점이다. §7 하드닝 11의 회귀는 `TZ=Asia/Seoul`과 `TZ=UTC`\n * 두 실행에서 **서로 다른 값**이 나오는지로만 잡힌다 — 고정 문자열로 박아 두면 그 유닛이\n * 한 타임존에서만 통과하는 가짜 검증이 된다.\n */\ndeclare function exifCapturedAtIso(): string;",
          "sourceDocumentation": "`EXIF_CAPTURED_AT`을 **기기 로컬 타임존**으로 해석한 ISO 문자열.\n\n⚠ 상수가 아니라 함수인 것이 요점이다. §7 하드닝 11의 회귀는 `TZ=Asia/Seoul`과 `TZ=UTC`\n두 실행에서 **서로 다른 값**이 나오는지로만 잡힌다 — 고정 문자열로 박아 두면 그 유닛이\n한 타임존에서만 통과하는 가짜 검증이 된다."
        },
        {
          "name": "fakeBytes",
          "slug": "fake-bytes",
          "kind": "function",
          "declaration": "/** 결정론적 더미 바이트 — `0,1,2,…,255` 순환. 크기만 중요한 테스트가 쓴다. */\ndeclare function fakeBytes(sizeBytes: number): Uint8Array;",
          "sourceDocumentation": "결정론적 더미 바이트 — `0,1,2,…,255` 순환. 크기만 중요한 테스트가 쓴다."
        },
        {
          "name": "FakeCallLog",
          "slug": "fake-call-log",
          "kind": "type",
          "declaration": "/**\n * 페이크가 기록하는 호출 이력.\n *\n * ⚠ **인자를 통째로 기록한다** — 호출 횟수만 세면 \"무엇을 가지고 불렀는가\"를 잃는다.\n * 하드닝 2의 후보 순회 검증은 `copy.from`이 어느 후보였는지를 봐야 성립하고,\n * 하드닝 9의 창 분할 검증은 `readBase64.range`가 3의 배수인지를 봐야 성립한다.\n */\ntype FakeCallLog = {\n    readonly stat: readonly string[];\n    readonly copy: readonly {\n        readonly from: string;\n        readonly to: string;\n    }[];\n    readonly remove: readonly string[];\n    readonly ensureDirectory: readonly string[];\n    readonly readBase64: readonly {\n        readonly uri: string;\n        readonly range: ChunkRange;\n    }[];\n    readonly download: readonly {\n        readonly url: string;\n        readonly to: string;\n    }[];\n};",
          "sourceDocumentation": "페이크가 기록하는 호출 이력.\n\n⚠ **인자를 통째로 기록한다** — 호출 횟수만 세면 \"무엇을 가지고 불렀는가\"를 잃는다.\n하드닝 2의 후보 순회 검증은 `copy.from`이 어느 후보였는지를 봐야 성립하고,\n하드닝 9의 창 분할 검증은 `readBase64.range`가 3의 배수인지를 봐야 성립한다."
        },
        {
          "name": "FakeDeviceLibrary",
          "slug": "fake-device-library",
          "kind": "interface",
          "declaration": "interface FakeDeviceLibrary extends DeviceLibraryAdapter {\n    readonly calls: {\n        readonly getPermission: number;\n        /** ⚠ iOS UI 데드록 차단(`canAskAgain:false` → 요청 0회)의 직접 증거다(§7.1). */\n        readonly requestPermission: number;\n        readonly listAssets: readonly {\n            readonly albumId?: string | null | undefined;\n            readonly after?: string | undefined;\n            readonly pageSize: number;\n            readonly kinds: readonly MediaKind[];\n        }[];\n        readonly listAlbums: number;\n        readonly getAssetInfo: readonly {\n            readonly assetId: string;\n            readonly downloadFromNetwork: boolean;\n        }[];\n    };\n}"
        },
        {
          "name": "FakeDeviceLibraryOptions",
          "slug": "fake-device-library-options",
          "kind": "type",
          "declaration": "type FakeDeviceLibraryOptions = {\n    /** 열거 대상. ⚠ **주어진 순서를 그대로 유지한다** — 정렬 계약의 주인은 어댑터다(§3.3). */\n    readonly assets?: readonly DeviceAsset[] | undefined;\n    /** ⚠ `count: 0` 앨범과 뒤섞인 순서를 일부러 줄 수 있어야 한다(§7.1 unit 2케이스). */\n    readonly albums?: readonly DeviceAlbum[] | undefined;\n    /** albumId → 그 앨범에 속한 자산 id. 없으면 `albumId` 필터는 무시된다. */\n    readonly albumAssets?: Readonly<Record<string, readonly string[]>> | undefined;\n    /** `getPermission()` 결과. 기본 허용. */\n    readonly permission?: MediaPermission | undefined;\n    /** `requestPermission()` 결과. 기본은 `permission`과 동일. */\n    readonly requestedPermission?: MediaPermission | undefined;\n    /**\n     * true면 원본이 iCloud에만 있다고 보고한다 — 단, `downloadFromNetwork: true`로 조회하면\n     * 해소된다. 그래야 §7 하드닝 6의 \"옵트인 → 60s 데드라인 → 재조회\" 흐름이 실제로 돈다.\n     */\n    readonly networkOnly?: boolean | undefined;\n    /** assetId별 EXIF. 기기 경로의 EXIF가 `PickedAsset.exif`까지 흐르는지 검증한다. */\n    readonly assetExif?: Readonly<Record<string, Readonly<Record<string, unknown>>>> | undefined;\n    /** assetId별 `DeviceAssetInfo` 전체 치환. 후보 순서(localUri → uri)를 직접 조작할 때 쓴다. */\n    readonly assetInfo?: Readonly<Record<string, DeviceAssetInfo>> | undefined;\n    /**\n     * `getAssetInfo`가 **영영 응답하지 않는다**. §7 하드닝 6의 15s/60s 데드라인이 core에 있음을\n     * 가짜 타이머로 검증하는 유일한 수단이다.\n     */\n    readonly hangInfo?: boolean | undefined;\n    /**\n     * `getAssetInfo`가 이 값을 throw한다.\n     * ⚠ §7.1 「정보 조회 실패 2조건」의 ②(어댑터 raw 예외)를 만드는 주입구다. `MediaError`를 주면\n     *   ①(후보 유무와 무관하게 항상 재throw)도 같은 손잡이로 검증된다.\n     */\n    readonly failInfoWith?: unknown;\n};"
        },
        {
          "name": "FakePicker",
          "slug": "fake-picker",
          "kind": "interface",
          "declaration": "interface FakePicker extends PickerAdapter {\n    readonly calls: {\n        /** 요청된 kinds 목록 — 권한 거부 문구가 kinds로 갈리므로(§4) 인자를 남긴다. */\n        readonly libraryPermission: readonly (readonly MediaKind[])[];\n        readonly cameraPermission: number;\n        readonly pick: readonly {\n            readonly kinds: readonly MediaKind[];\n            readonly max: number;\n        }[];\n        readonly capture: readonly {\n            readonly kind: MediaKind;\n        }[];\n    };\n}"
        },
        {
          "name": "FakePickerOptions",
          "slug": "fake-picker-options",
          "kind": "type",
          "declaration": "type FakePickerOptions = {\n    /** `pickFromLibrary` 전 권한. 기본 허용. */\n    readonly libraryPermission?: MediaPermission | undefined;\n    /** `capture` 전 권한. 기본 허용. */\n    readonly cameraPermission?: MediaPermission | undefined;\n    /**\n     * 카메라가 돌려줄 자산. 생략 시 라이브러리 자산과 같다.\n     * ⚠ 여러 건을 줘야 §5.4.1-12의 \"카메라 캡처 1장 제한\"이 검증된다 — 1건만 주면\n     *   자르는 코드가 있든 없든 결과가 같아서 그 규칙이 통과한 척한다.\n     */\n    readonly captureAssets?: readonly PickedAsset[] | undefined;\n};"
        },
        {
          "name": "fakePlatform",
          "slug": "fake-platform",
          "kind": "function",
          "declaration": "/**\n * 설계 문서 §5.6의 확정 시그니처.\n *\n * ⚠ `isDev`는 **false**가 기본이다. true로 두면 모든 유닛이 콘솔 로그를 뿜고, 그 소음 속에서\n * \"로그에 원문 uri가 실렸는가\"(§7 하드닝 8) 같은 진짜 신호를 잃는다. 로거를 검증하는 테스트만\n * 명시적으로 켠다 — `createFakePlatform({ os, isDev: true })`.\n */\ndeclare function fakePlatform(os: MediaPlatform): PlatformAdapter;",
          "sourceDocumentation": "설계 문서 §5.6의 확정 시그니처.\n\n⚠ `isDev`는 **false**가 기본이다. true로 두면 모든 유닛이 콘솔 로그를 뿜고, 그 소음 속에서\n\"로그에 원문 uri가 실렸는가\"(§7 하드닝 8) 같은 진짜 신호를 잃는다. 로거를 검증하는 테스트만\n명시적으로 켠다 — `createFakePlatform({ os, isDev: true })`."
        },
        {
          "name": "FakeUploadApi",
          "slug": "fake-upload-api",
          "kind": "interface",
          "declaration": "interface FakeUploadApi<TAsset> extends MediaUploadApi<TAsset> {\n    readonly intents: readonly MediaUploadIntentRequest[];\n    readonly completions: readonly MediaUploadCompletion[];\n    /** 발급한 슬롯 — `intents[i]`와 순번이 같다. PUT된 URL과의 대조에 쓴다. */\n    readonly issued: readonly MediaUploadIntent[];\n}"
        },
        {
          "name": "FakeUploadApiOptions",
          "slug": "fake-upload-api-options",
          "kind": "type",
          "declaration": "type FakeUploadApiOptions<TAsset> = {\n    /** 완료 페이로드 → 호스트 자산. 테스트가 무엇을 자산으로 볼지 직접 정한다. */\n    readonly asset: (input: MediaUploadCompletion) => TAsset;\n    /**\n     * 중복 판정.\n     * ⚠ `UploadResult.duplicate`는 **필수 필드**다(§6.1-⑯) — 옵셔널이면 호스트가 판정을\n     *   돌려주지 않을 때 킷이 \"새로 만들어졌다\"로 오독하고, 중복 취소 경로가 사용자의 예전\n     *   사진을 지운다. 그 분기를 유닛에서 돌리려면 여기에 주입구가 있어야 한다.\n     */\n    readonly duplicateWhen?: ((input: MediaUploadCompletion) => boolean) | undefined;\n    /** 오브젝트 키 결정. 기본 `objects/<순번>-<fileName>`. */\n    readonly objectName?: ((input: MediaUploadIntentRequest, index: number) => string) | undefined;\n    /** 서명 URL 발급. 기본은 §7 하드닝 8용 서명 URL 픽스처다. */\n    readonly uploadUrl?: ((objectName: string) => string) | undefined;\n    /** presign 응답 헤더. 기본 `{ 'content-type': <요청 contentType> }`. */\n    readonly headers?: ((input: MediaUploadIntentRequest) => Readonly<Record<string, string>>) | undefined;\n};"
        },
        {
          "name": "jpegWithExif",
          "slug": "jpeg-with-exif",
          "kind": "function",
          "declaration": "/**\n * `EXIF_FIXTURE`와 **같은 값**을 담은 최소 JPEG(빅엔디언 'MM' TIFF).\n *\n * 구조: `SOI` → `APP1(Exif)` → `EOI`. 이미지 데이터가 없어도 파서에는 충분하다 —\n * `parseJpegExif`는 SOS(0xda) 이전 세그먼트만 훑기 때문이다.\n *\n * ⚠ 이 픽스처가 없으면 웹 업로드 경로(`mediaMetadataFromJpeg`)의 EXIF 추출은 **영영 유닛으로\n * 검증되지 않는다** — 그 경로가 조용히 죽으면 촬영시각·위치가 영구 소실되고, 그것은 타입에도\n * 테스트에도 잡히지 않는다(§5.3 머리말).\n */\ndeclare function jpegWithExif(): Uint8Array;",
          "sourceDocumentation": "`EXIF_FIXTURE`와 **같은 값**을 담은 최소 JPEG(빅엔디언 'MM' TIFF).\n\n구조: `SOI` → `APP1(Exif)` → `EOI`. 이미지 데이터가 없어도 파서에는 충분하다 —\n`parseJpegExif`는 SOS(0xda) 이전 세그먼트만 훑기 때문이다.\n\n⚠ 이 픽스처가 없으면 웹 업로드 경로(`mediaMetadataFromJpeg`)의 EXIF 추출은 **영영 유닛으로\n검증되지 않는다** — 그 경로가 조용히 죽으면 촬영시각·위치가 영구 소실되고, 그것은 타입에도\n테스트에도 잡히지 않는다(§5.3 머리말)."
        },
        {
          "name": "jpegWithoutExif",
          "slug": "jpeg-without-exif",
          "kind": "function",
          "declaration": "/**\n * EXIF가 없는 최소 JPEG(APP0 JFIF만). `mediaMetadataFromJpeg`가 `undefined`를 내고\n * fallback으로 내려가는 경로(규칙 ④)를 태운다.\n */\ndeclare function jpegWithoutExif(): Uint8Array;",
          "sourceDocumentation": "EXIF가 없는 최소 JPEG(APP0 JFIF만). `mediaMetadataFromJpeg`가 `undefined`를 내고\nfallback으로 내려가는 경로(규칙 ④)를 태운다."
        },
        {
          "name": "MemoryFileSystem",
          "slug": "memory-file-system",
          "kind": "interface",
          "declaration": "interface MemoryFileSystem extends FileSystemAdapter, FileDownloadAdapter, DurableFileStoreAdapter {\n    readonly calls: FakeCallLog;\n    /** 현재 존재하는 파일 uri 전량(정렬됨). 스테이징 누수 검증의 직접 증거다(§7 하드닝 7). */\n    list(): readonly string[];\n    /** 파일 바이트를 그대로 읽는다. 없으면 `null`. 업로드된 바이트 대조에 쓴다. */\n    read(uri: string): Uint8Array | null;\n    /** 파일을 심는다. 테스트 도중 상태를 바꿔야 하는 경우(재시도 시나리오)를 위해 열어 둔다. */\n    write(uri: string, bytes: Uint8Array): void;\n}"
        },
        {
          "name": "MemoryFileSystemOptions",
          "slug": "memory-file-system-options",
          "kind": "type",
          "declaration": "type MemoryFileSystemOptions = {\n    /** 초기 파일 — uri → 바이트. 값은 복사되므로 이후 호출자가 고쳐도 페이크는 흔들리지 않는다. */\n    readonly files?: Readonly<Record<string, Uint8Array>> | undefined;\n    /**\n     * 앱 소유 캐시 디렉토리(끝에 '/'). 기본 `'file:///cache/'`.\n     * ⚠ **`null`을 줄 수 있다** — \"쓸 수 있는 디렉토리가 하나도 없는 기기\"가 실제 분기이기 때문이다:\n     *   `StagingCache.uriFor`가 `null`을 반환하는 경로(§5.3)와 `createMediaSaver`가 plain Error를\n     *   던지는 경로(§5.4-⑥)는 이 값이 없어야만 도달한다.\n     */\n    readonly cacheDirectory?: string | null | undefined;\n    /** App-owned durable root. Default `'file:///documents/'`; null exercises unavailable storage. */\n    readonly rootDirectory?: string | null | undefined;\n    /** 디렉토리로 취급할 uri. 끝이 '/'인 uri는 지정하지 않아도 디렉토리다. */\n    readonly directories?: readonly string[] | undefined;\n    /**\n     * `download`의 응답을 결정한다. 기본은 `{ status: 200 }` + 8바이트 더미.\n     * ⚠ 3xx·4xx를 돌려주는 수단이 없으면 §7.1의 \"다운로드 status **2xx 범위** 검증 + 실패 시\n     *   임시 파일 정리\"를 검증할 방법이 없다. 그래서 상태코드가 주입구다.\n     */\n    readonly download?: ((input: {\n        readonly url: string;\n        readonly to: string;\n    }) => {\n        readonly status: number;\n        readonly bytes?: Uint8Array | undefined;\n    }) | undefined;\n};"
        },
        {
          "name": "RecordedPut",
          "slug": "recorded-put",
          "kind": "type",
          "declaration": "/**\n * 기록된 PUT 1건.\n *\n * `PutRequest`를 확장한 형태라 `readonly PutRequest[]`로 받는 소비자와 호환된다(§5.6 시그니처).\n * `uri`(로컬 경로)와 `body`(바이너리)는 어느 경로로 들어왔는지에 따라 한쪽만 채워진다 —\n * 그 자체가 \"이 업로드가 네이티브였나 웹이었나\"의 판별자다.\n */\ntype RecordedPut = PutRequest & {\n    readonly uri?: string | undefined;\n    readonly body?: BinarySource | undefined;\n    /** 바이너리 PUT의 크기. presign `sizeBytes`와 실제 전송 바이트의 일치 검증에 쓴다(§7 하드닝 3). */\n    readonly sizeBytes?: number | undefined;\n};",
          "sourceDocumentation": "기록된 PUT 1건.\n\n`PutRequest`를 확장한 형태라 `readonly PutRequest[]`로 받는 소비자와 호환된다(§5.6 시그니처).\n`uri`(로컬 경로)와 `body`(바이너리)는 어느 경로로 들어왔는지에 따라 한쪽만 채워진다 —\n그 자체가 \"이 업로드가 네이티브였나 웹이었나\"의 판별자다."
        },
        {
          "name": "RecordedSpan",
          "slug": "recorded-span",
          "kind": "type",
          "declaration": "type RecordedSpan = {\n    readonly operation: MediaOperation;\n    /** `track`은 시작 payload를 항상 주고, `begin`은 생략할 수 있다(§7.2 표). */\n    readonly extra?: Readonly<Record<string, unknown>> | undefined;\n    readonly kind: 'track' | 'begin';\n    /** 아직 끝나지 않았으면 `null`. ⚠ **한 스팬은 정확히 한 번만 종료돼야 한다**(§5.1). */\n    readonly outcome: 'succeed' | 'fail' | 'cancel' | null;\n    readonly finish?: MediaActivityFinish | undefined;\n    readonly error?: unknown;\n};"
        },
        {
          "name": "RecordingTelemetry",
          "slug": "recording-telemetry",
          "kind": "interface",
          "declaration": "interface RecordingTelemetry extends MediaTelemetry {\n    readonly spans: readonly RecordedSpan[];\n    /** 관측된 operation 이름을 등장 순서대로. `MEDIA_OPERATIONS`와의 집합 대조에 쓴다. */\n    operations(): readonly MediaOperation[];\n}"
        },
        {
          "name": "RecordingTransport",
          "slug": "recording-transport",
          "kind": "interface",
          "declaration": "interface RecordingTransport extends LocalFileTransport, BinaryTransport {\n    readonly puts: readonly RecordedPut[];\n    /** `start:<index>` / `end:<index>` 순서. 순차 실행 단언의 직접 증거(위 `onPut` 참조). */\n    readonly timeline: readonly string[];\n}"
        },
        {
          "name": "RecordingTransportOptions",
          "slug": "recording-transport-options",
          "kind": "type",
          "declaration": "type RecordingTransportOptions = {\n    /**\n     * 이 상태코드로 응답한다. 생략 시 200.\n     * ⚠ PUT의 모호한 2xx 밖 응답은 `upload-failed` + `possibly-uploaded` cleanup 후보가 된다\n     *   (`isSuccessStatus`, §5.4-①). 상태코드 주입구가 없으면 그 안전한 실패 경계가 검증되지 않는다.\n     */\n    readonly failWithStatus?: number | undefined;\n    /**\n     * PUT 본문 실행 훅 — 시작 직후 await된다.\n     *\n     * ⚠ §7.1의 「기기 자산 업로드 루프의 **의도적 순차 실행**」은 \"동시 진행 0\"을 단언해야 하는데,\n     *   그것은 전송이 실제로 지연될 때만 관측된다. 이 훅에 지연을 넣고 `timeline`을 보면\n     *   `start:0, end:0, start:1, end:1`(순차) 과 `start:0, start:1, …`(병렬)이 구분된다.\n     */\n    readonly onPut?: ((put: RecordedPut, index: number) => Promise<void> | void) | undefined;\n};"
        },
        {
          "name": "SIGNED_UPLOAD_URL",
          "slug": "signed-upload-url",
          "kind": "constant",
          "declaration": "SIGNED_UPLOAD_URL: string",
          "sourceDocumentation": "임시 자격증명이 쿼리에 실린 presigned PUT URL.\n⚠ 실제 자격증명이 아니다 — 형태만 재현한 고정 문자열이다."
        },
        {
          "name": "signedUploadUrl",
          "slug": "signed-upload-url--function",
          "kind": "function",
          "declaration": "/** 오브젝트별 서명 URL. `createFakeUploadApi`의 기본 발급기이기도 하다. */\ndeclare function signedUploadUrl(objectName: string): string;",
          "sourceDocumentation": "오브젝트별 서명 URL. `createFakeUploadApi`의 기본 발급기이기도 하다."
        },
        {
          "name": "signedUrlErrorMessage",
          "slug": "signed-url-error-message",
          "kind": "function",
          "declaration": "/**\n * iOS URLSession이 서명 URL을 그대로 에코한 실패 메시지의 모양.\n * `sanitizeMediaErrorMessage` 유닛이 \"플랫폼 코드·설명은 남고 URL만 `[URL]`이 되는가\"를 본다.\n */\ndeclare function signedUrlErrorMessage(url?: string): string;",
          "sourceDocumentation": "iOS URLSession이 서명 URL을 그대로 에코한 실패 메시지의 모양.\n`sanitizeMediaErrorMessage` 유닛이 \"플랫폼 코드·설명은 남고 URL만 `[URL]`이 되는가\"를 본다."
        },
        {
          "name": "toArrayBuffer",
          "slug": "to-array-buffer",
          "kind": "function",
          "declaration": "/**\n * `Uint8Array` → 독립 `ArrayBuffer`.\n *\n * `bytes.buffer`를 그대로 쓰지 않는 이유 둘: (i) `subarray` 결과는 **버퍼를 공유**해\n * 호출자가 페이크 내부 바이트를 뒤에서 고칠 수 있고, (ii) TS 5.7+의 `ArrayBufferLike`\n * 타입 파라미터화 때문에 `Uint8Array.buffer`가 `ArrayBuffer`로 좁혀지지 않는 조합이 있다.\n * 새 버퍼에 복사하면 두 문제가 동시에 사라지고 캐스트도 필요 없다.\n */\ndeclare function toArrayBuffer(bytes: Uint8Array): ArrayBuffer;",
          "sourceDocumentation": "`Uint8Array` → 독립 `ArrayBuffer`.\n\n`bytes.buffer`를 그대로 쓰지 않는 이유 둘: (i) `subarray` 결과는 **버퍼를 공유**해\n호출자가 페이크 내부 바이트를 뒤에서 고칠 수 있고, (ii) TS 5.7+의 `ArrayBufferLike`\n타입 파라미터화 때문에 `Uint8Array.buffer`가 `ArrayBuffer`로 좁혀지지 않는 조합이 있다.\n새 버퍼에 복사하면 두 문제가 동시에 사라지고 캐스트도 필요 없다."
        },
        {
          "name": "truncatedJpegWithExif",
          "slug": "truncated-jpeg-with-exif",
          "kind": "function",
          "declaration": "/**\n * APP1 길이 필드가 실제 바이트보다 긴 **잘린** JPEG.\n *\n * ⚠ 파서의 경계 검사(`isReadable`)가 살아 있으면 예외 없이 `undefined`가 나오고, 죽어 있으면\n * `DataView` RangeError가 업로드 전체를 죽인다(§5.3 파서 주석). 그 차이를 이 픽스처가 가른다.\n */\ndeclare function truncatedJpegWithExif(): Uint8Array;",
          "sourceDocumentation": "APP1 길이 필드가 실제 바이트보다 긴 **잘린** JPEG.\n\n⚠ 파서의 경계 검사(`isReadable`)가 살아 있으면 예외 없이 `undefined`가 나오고, 죽어 있으면\n`DataView` RangeError가 업로드 전체를 죽인다(§5.3 파서 주석). 그 차이를 이 픽스처가 가른다."
        }
      ]
    },
    {
      "subpath": "./video",
      "id": "video",
      "declarationTarget": "./dist/video.d.cts",
      "symbols": [
        {
          "name": "expoVideoPoster",
          "slug": "expo-video-poster",
          "kind": "function",
          "declaration": "/** §5.6 — `\"./video\"`의 유일한 공개 심볼. */\ndeclare function expoVideoPoster(): LocalPosterAdapter;",
          "sourceDocumentation": "§5.6 — `\"./video\"`의 유일한 공개 심볼."
        }
      ]
    },
    {
      "subpath": "./web",
      "id": "web",
      "declarationTarget": "./dist/web.d.cts",
      "symbols": [
        {
          "name": "createBrowserSaveTarget",
          "slug": "create-browser-save-target",
          "kind": "function",
          "declaration": "/**\n * 서빙 URL을 브라우저 다운로드로 저장하는 `BrowserSaveAdapter`(§5.6).\n * `createMediaSaver({ target: { kind: 'browser-download', browser } })`에 꽂는다.\n *\n * ⚠ `document`/`fetch`는 **필수 주입**이다(§5.6 시그니처 · §6.1-⑬ \"런타임 + 생성 차단\").\n *   옵셔널로 두고 `globalThis`로 폴백하면 네이티브·SSR에서도 **생성이 조용히 성공**해\n *   \"웹 전용 API를 네이티브에서\" 조합이 타입에서 걸리지 않는다 — ⑬이 막으려던 바로 그 결말이다.\n *   `dist/web.d.ts`의 DOM 각인(§2.4)이 무DOM 소비자에게서 이 두 필드를 지켜주는 것도\n *   **필드가 시그니처에 존재할 때만** 의미가 있다.\n *   호출 시점 판정(§8.5의 `platform-unsupported`)은 내부 `createDirectBrowserSave`가 계속 맡는다 —\n *   비네이티브 포크(`src/save/web.ts`)는 SSR에 `document`가 없어도 조립될 수 있어야 하기 때문이다.\n */\ndeclare function createBrowserSaveTarget(input: {\n    readonly document: Document;\n    readonly fetch: typeof fetch;\n}): BrowserSaveAdapter;",
          "sourceDocumentation": "서빙 URL을 브라우저 다운로드로 저장하는 `BrowserSaveAdapter`(§5.6).\n`createMediaSaver({ target: { kind: 'browser-download', browser } })`에 꽂는다.\n\n⚠ `document`/`fetch`는 **필수 주입**이다(§5.6 시그니처 · §6.1-⑬ \"런타임 + 생성 차단\").\n  옵셔널로 두고 `globalThis`로 폴백하면 네이티브·SSR에서도 **생성이 조용히 성공**해\n  \"웹 전용 API를 네이티브에서\" 조합이 타입에서 걸리지 않는다 — ⑬이 막으려던 바로 그 결말이다.\n  `dist/web.d.ts`의 DOM 각인(§2.4)이 무DOM 소비자에게서 이 두 필드를 지켜주는 것도\n  **필드가 시그니처에 존재할 때만** 의미가 있다.\n  호출 시점 판정(§8.5의 `platform-unsupported`)은 내부 `createDirectBrowserSave`가 계속 맡는다 —\n  비네이티브 포크(`src/save/web.ts`)는 SSR에 `document`가 없어도 조립될 수 있어야 하기 때문이다."
        },
        {
          "name": "createFetchBinarySourceLoader",
          "slug": "create-fetch-binary-source-loader",
          "kind": "function",
          "declaration": "/**\n * `blob:`/`data:` URI → `NamedBinarySource`(§3.3-⑤-b).\n *\n * `fetch`는 주입 가능하지만 **호출 시점에** 해석한다(생성 시점 캡처는 SSR·폴리필 환경에서\n * undefined를 영구 고정한다).\n */\ndeclare function createFetchBinarySourceLoader(input?: {\n    readonly fetch?: typeof fetch | undefined;\n} | undefined): BinarySourceLoader;",
          "sourceDocumentation": "`blob:`/`data:` URI → `NamedBinarySource`(§3.3-⑤-b).\n\n`fetch`는 주입 가능하지만 **호출 시점에** 해석한다(생성 시점 캡처는 SSR·폴리필 환경에서\nundefined를 영구 고정한다)."
        },
        {
          "name": "createFetchBinaryTransport",
          "slug": "create-fetch-binary-transport",
          "kind": "function",
          "declaration": "/**\n * fetch 기반 바이너리 PUT 전송기(§5.6).\n *\n * `fetch`는 주입 가능하지만 **호출 시점에** 해석한다. 생성 시점에 붙잡으면 SSR·폴리필 환경에서\n * 모듈 평가 순간의 값(대개 undefined)이 영구히 고정된다.\n */\ndeclare function createFetchBinaryTransport(input?: {\n    readonly fetch?: typeof fetch | undefined;\n} | undefined): BinaryTransport;",
          "sourceDocumentation": "fetch 기반 바이너리 PUT 전송기(§5.6).\n\n`fetch`는 주입 가능하지만 **호출 시점에** 해석한다. 생성 시점에 붙잡으면 SSR·폴리필 환경에서\n모듈 평가 순간의 값(대개 undefined)이 영구히 고정된다."
        },
        {
          "name": "pendingItemFromFile",
          "slug": "pending-item-from-file",
          "kind": "function",
          "declaration": "/**\n * Wrap a DOM `File` as a `PendingBinaryItem` for `createPendingSelection`.\n *\n * Creates an object URL as `previewUri` (skipped for HEIC/HEIF, which browsers cannot render, and\n * in environments without `URL.createObjectURL`) and a `revoke` closure that releases it. The\n * selection calls `revoke` at most once, when the item is removed or cleared; pass the add result's\n * `releasable` items to `PendingSelection.release` yourself. `File.lastModified` becomes part of\n * the dedup key.\n */\ndeclare function pendingItemFromFile(file: File): PendingBinaryItem;",
          "sourceDocumentation": "Wrap a DOM `File` as a `PendingBinaryItem` for `createPendingSelection`.\n\nCreates an object URL as `previewUri` (skipped for HEIC/HEIF, which browsers cannot render, and\nin environments without `URL.createObjectURL`) and a `revoke` closure that releases it. The\nselection calls `revoke` at most once, when the item is removed or cleared; pass the add result's\n`releasable` items to `PendingSelection.release` yourself. `File.lastModified` becomes part of\nthe dedup key."
        },
        {
          "name": "webCanvasVideoPoster",
          "slug": "web-canvas-video-poster",
          "kind": "function",
          "declaration": "/**\n * 브라우저 canvas 기반 동영상 포스터 추출기(§5.6).\n *\n * ⚠ 실패는 전부 `null`이다 — 예외를 밖으로 내보내지 않는다. **포스터 실패가 동영상 업로드를\n *   막지 않는다**는 것이 계약이고(§7.1), 그 정책의 절반이 여기(어댑터가 null을 준다),\n *   나머지 절반이 코어(`resolvePoster`의 try/catch)에 있다.\n *\n * `document`는 주입 가능하지만 **호출 시점에** 해석한다. 생성 시점에 붙잡으면 SSR에서\n * 모듈이 평가되는 순간의 `undefined`가 하이드레이션 이후까지 남는다(§8.5 케이스 H 계열의 함정).\n */\ndeclare function webCanvasVideoPoster(input?: {\n    readonly document?: Document | undefined;\n} | undefined): BinaryPosterAdapter;",
          "sourceDocumentation": "브라우저 canvas 기반 동영상 포스터 추출기(§5.6).\n\n⚠ 실패는 전부 `null`이다 — 예외를 밖으로 내보내지 않는다. **포스터 실패가 동영상 업로드를\n  막지 않는다**는 것이 계약이고(§7.1), 그 정책의 절반이 여기(어댑터가 null을 준다),\n  나머지 절반이 코어(`resolvePoster`의 try/catch)에 있다.\n\n`document`는 주입 가능하지만 **호출 시점에** 해석한다. 생성 시점에 붙잡으면 SSR에서\n모듈이 평가되는 순간의 `undefined`가 하이드레이션 이후까지 남는다(§8.5 케이스 H 계열의 함정)."
        }
      ]
    }
  ]
}
