{
  "slug": "expo-workouts",
  "name": "@gj-kit/expo-workouts",
  "version": "0.1.2",
  "description": "HealthKit and Health Connect workout, GPS-route, authorization, and incremental sync bridge for Expo.",
  "homepage": "https://gj-kit.github.io/gj-kit/packages/expo-workouts/",
  "repository": "git+https://github.com/gj-kit/gj-kit.git",
  "license": "MIT",
  "engines": {
    "node": ">=20"
  },
  "peerDependencies": {
    "expo": ">=56.0.0 <58.0.0"
  },
  "peerDependenciesMeta": {
    "expo": {
      "optional": true
    }
  },
  "entries": [
    {
      "subpath": ".",
      "id": "root",
      "declarationTarget": "./dist/index.d.mts",
      "symbols": [
        {
          "name": "activeDurationS",
          "slug": "active-duration-s",
          "kind": "function",
          "declaration": "/**\n * `(endMs - startMs - Σ pause overlap) / 1000`, clamped at 0. Overlapping pauses are merged, so\n * double-counting is not expressible.\n *\n * ⚠ This is how ANDROID derives it. iOS reports the store's own `duration`, which honours the\n *   writer's explicit argument and can differ; `Workout.activeDurationS` carries whichever the\n *   platform gave.\n */\ndeclare function activeDurationS(startMs: number, endMs: number, pauses: readonly Pause[]): number;",
          "sourceDocumentation": "`(endMs - startMs - Σ pause overlap) / 1000`, clamped at 0. Overlapping pauses are merged, so\ndouble-counting is not expressible.\n\n⚠ This is how ANDROID derives it. iOS reports the store's own `duration`, which honours the\n  writer's explicit argument and can differ; `Workout.activeDurationS` carries whichever the\n  platform gave."
        },
        {
          "name": "ANDROID_HISTORY_PERMISSION",
          "slug": "android-history-permission",
          "kind": "constant",
          "declaration": "ANDROID_HISTORY_PERMISSION = \"android.permission.health.READ_HEALTH_DATA_HISTORY\"",
          "sourceDocumentation": "D10. 매니페스트와 런타임 요청 양쪽에 필요한 history 권한."
        },
        {
          "name": "ANDROID_HISTORY_WINDOW_MS",
          "slug": "android-history-window-ms",
          "kind": "constant",
          "declaration": "ANDROID_HISTORY_WINDOW_MS = 2592000000",
          "sourceDocumentation": "30 days in ms — Health Connect's history wall without READ_HEALTH_DATA_HISTORY (D10)."
        },
        {
          "name": "ANDROID_READ_PERMISSIONS",
          "slug": "android-read-permissions",
          "kind": "constant",
          "declaration": "ANDROID_READ_PERMISSIONS: Readonly<Record<Scope, string>>",
          "sourceDocumentation": "Health Connect READ 권한. `routes`는 **매니페스트 전용**이며 런타임 요청 집합에 넣지 않는다(f110)."
        },
        {
          "name": "ANDROID_WRITE_PERMISSIONS",
          "slug": "android-write-permissions",
          "kind": "constant",
          "declaration": "ANDROID_WRITE_PERMISSIONS: Readonly<Record<Scope, string>>",
          "sourceDocumentation": "Health Connect WRITE 권한. `routes`만 단수형(`WRITE_EXERCISE_ROUTE`)이다."
        },
        {
          "name": "androidExerciseTypeFromKind",
          "slug": "android-exercise-type-from-kind",
          "kind": "function",
          "declaration": "/**\n * WRITE direction, Android. `indoor` selects between the constant PAIR where one exists\n * (running / cycling / swimming / rowing) and is otherwise silently dropped — Health Connect has\n * nowhere to store it.\n * ⚠ `kind: 'other'` writes OTHER_WORKOUT(0) and is NOT recoverable on read.\n */\ndeclare function androidExerciseTypeFromKind(kind: WorkoutKind, indoor?: boolean | undefined): number;",
          "sourceDocumentation": "WRITE direction, Android. `indoor` selects between the constant PAIR where one exists\n(running / cycling / swimming / rowing) and is otherwise silently dropped — Health Connect has\nnowhere to store it.\n⚠ `kind: 'other'` writes OTHER_WORKOUT(0) and is NOT recoverable on read."
        },
        {
          "name": "androidRequestPermissions",
          "slug": "android-request-permissions",
          "kind": "function",
          "declaration": "/**\n * Android: scope -> `android.permission.health.*`, 방향별로.\n * `'routes'`의 READ는 **절대 포함하지 않는다** — 플랫폼이 조용히 걸러내고 설정 화면이나 per-route\n * 다이얼로그에서만 부여된다(f110, f121). `history`는 READ 쪽에 실린다(D10).\n */\ndeclare function androidRequestPermissions(request: AuthorizationRequest): DirectedPermissions;",
          "sourceDocumentation": "Android: scope -> `android.permission.health.*`, 방향별로.\n`'routes'`의 READ는 **절대 포함하지 않는다** — 플랫폼이 조용히 걸러내고 설정 화면이나 per-route\n다이얼로그에서만 부여된다(f110, f121). `history`는 READ 쪽에 실린다(D10)."
        },
        {
          "name": "androidRuntimeRequestPermissions",
          "slug": "android-runtime-request-permissions",
          "kind": "function",
          "declaration": "/**\n * 런타임 요청 집합. `'routes'`의 READ는 **절대 포함하지 않는다** — 플랫폼이 조용히 걸러내고\n * 설정 화면이나 per-route 다이얼로그에서만 부여된다(f110, f121).\n */\ndeclare function androidRuntimeRequestPermissions(request: AuthorizationRequest): readonly string[];",
          "sourceDocumentation": "런타임 요청 집합. `'routes'`의 READ는 **절대 포함하지 않는다** — 플랫폼이 조용히 걸러내고\n설정 화면이나 per-route 다이얼로그에서만 부여된다(f110, f121)."
        },
        {
          "name": "AndroidWorkout",
          "slug": "android-workout",
          "kind": "interface",
          "declaration": "interface AndroidWorkout extends WorkoutBase {\n    readonly platform: 'android';\n    readonly platformData: AndroidWorkoutData;\n}"
        },
        {
          "name": "AndroidWorkoutData",
          "slug": "android-workout-data",
          "kind": "interface",
          "declaration": "/** Raw Android values the common model deliberately does not model. */\ninterface AndroidWorkoutData {\n    /** Raw ExerciseSessionRecord.exerciseType. */\n    readonly exerciseType: number;\n    readonly packageName: string;\n    readonly recordingMethod: number;\n    readonly deviceType?: number | undefined;\n    /**\n     * The writer's own client record id. Foreign apps' values ARE visible here — treat it as PUBLIC\n     * data, never as a private namespace.\n     */\n    readonly clientRecordId?: string | undefined;\n    readonly clientRecordVersion?: number | undefined;\n    readonly endUtcOffsetMin?: number | undefined;\n    /** Foreign-app authored text. This library never writes a title or notes. */\n    readonly title?: string | undefined;\n    readonly notes?: string | undefined;\n    /** Every segment, including REST (44), which `pauses` deliberately excludes. PAUSE is 39. */\n    readonly segments: readonly {\n        readonly type: number;\n        readonly startMs: number;\n        readonly endMs: number;\n    }[];\n}",
          "sourceDocumentation": "Raw Android values the common model deliberately does not model."
        },
        {
          "name": "assertNeverWorkoutsCode",
          "slug": "assert-never-workouts-code",
          "kind": "function",
          "declaration": "/**\n * Call it from a `switch` default so a future code becomes a compile error for you.\n * ⚠ This is only honest because the code union is CLOSED for 1.x: adding a code is a major.\n */\ndeclare function assertNeverWorkoutsCode(code: never): never;",
          "sourceDocumentation": "Call it from a `switch` default so a future code becomes a compile error for you.\n⚠ This is only honest because the code union is CLOSED for 1.x: adding a code is a major."
        },
        {
          "name": "authorizationAdvice",
          "slug": "authorization-advice",
          "kind": "function",
          "declaration": "/**\n * Our opinion about what a settings screen should render, as a PURE function rather than a field on\n * `AuthorizationState`: if the platform's permission UI changes, an opinion baked into the contract\n * would be wrong in a way the raw facts would not have been. Adopt it or re-implement it.\n *\n * The one rule that matters: `'unknown'` NEVER produces `'openSettings'`. Every iOS read scope is\n * permanently `'unknown'`, so treating it as a problem would show every iOS user \"go check\n * Settings\" forever.\n */\ndeclare function authorizationAdvice(facts: AuthorizationFacts): AuthorizationAdvice;",
          "sourceDocumentation": "Our opinion about what a settings screen should render, as a PURE function rather than a field on\n`AuthorizationState`: if the platform's permission UI changes, an opinion baked into the contract\nwould be wrong in a way the raw facts would not have been. Adopt it or re-implement it.\n\nThe one rule that matters: `'unknown'` NEVER produces `'openSettings'`. Every iOS read scope is\npermanently `'unknown'`, so treating it as a problem would show every iOS user \"go check\nSettings\" forever."
        },
        {
          "name": "AuthorizationAdvice",
          "slug": "authorization-advice--type",
          "kind": "type",
          "declaration": "/** What a settings screen should do next. */\ntype AuthorizationAdvice = 'ready' | 'requestable' | 'openSettings' | 'openStoreListing' | 'unsupported';",
          "sourceDocumentation": "What a settings screen should do next."
        },
        {
          "name": "AuthorizationDerivationFacts",
          "slug": "authorization-derivation-facts",
          "kind": "interface",
          "declaration": "/** 스냅샷에서 상태를 도출할 때 필요한 부가 사실. 전부 optional이며 없으면 보수적으로 판정한다. */\ninterface AuthorizationDerivationFacts {\n    /**\n     * 방금 끝난 **결론적인** 요청에서 사용자가 실제로 거부한 플랫폼 문자열. `before`/`after` 비교의\n     * 결과이며(f120이 강제하는 유일한 정직한 판정 근거), 요청이 `conclusive: false`였다면 **비어\n     * 있어야 한다** — 그때 scope 상태는 불변이다.\n     */\n    readonly denied?: readonly string[] | undefined;\n}",
          "sourceDocumentation": "스냅샷에서 상태를 도출할 때 필요한 부가 사실. 전부 optional이며 없으면 보수적으로 판정한다."
        },
        {
          "name": "AuthorizationFacts",
          "slug": "authorization-facts",
          "kind": "interface",
          "declaration": "/** Input for the pure derivation, so a second consumer can re-derive it from facts it stored earlier. */\ninterface AuthorizationFacts {\n    readonly state: AuthorizationState;\n    /** The scopes THIS screen actually needs — may be narrower than everything the build declares. */\n    readonly requiredRead: readonly Scope[];\n    readonly requiredWrite?: readonly Scope[] | undefined;\n    readonly requiresHistory?: boolean | undefined;\n}",
          "sourceDocumentation": "Input for the pure derivation, so a second consumer can re-derive it from facts it stored earlier."
        },
        {
          "name": "AuthorizationRequest",
          "slug": "authorization-request",
          "kind": "interface",
          "declaration": "interface AuthorizationRequest {\n    /**\n     * Coarse form — one token, and the recipe to copy:\n     * `read: [...WORKOUT_TOTALS_SCOPES, 'routes']`.\n     * Fine form — name the members: `read: ['workouts', 'heartRate']`.\n     *\n     * ⚠ A metric scope without `'workouts'` is `invalidArgument`. `read: ['distance']` alone is a\n     *   100 % mistake — no API in this library reads distance except through a workout — so `./core`\n     *   rejects it before any platform call.\n     */\n    readonly read: readonly Scope[];\n    readonly write?: readonly Scope[] | undefined;\n    /**\n     * D10, opt-in. Android only. Without it, reads are walled to the last 30 days and a wider window\n     * throws `historyRequired`. It ALSO needs the config-plugin `history: true` prop; requesting it\n     * without the manifest entry throws `invalidArgument` naming the missing prop.\n     */\n    readonly history?: boolean | undefined;\n}"
        },
        {
          "name": "AuthorizationResult",
          "slug": "authorization-result",
          "kind": "type",
          "declaration": "/**\n * `requestAuthorization`'s result: the state afterwards, plus whether we can attribute it to the\n * user. `conclusive: false` means the OS returned an answer we cannot attribute — on Android,\n * bouncing off Health Connect's first-run onboarding with \"Go back\" returns an EMPTY permission set\n * after ~20 s, byte-identical to denying everything. Treat it as \"ask again later\", NEVER as denial.\n */\ntype AuthorizationResult = AuthorizationState & {\n    readonly conclusive: boolean;\n};",
          "sourceDocumentation": "`requestAuthorization`'s result: the state afterwards, plus whether we can attribute it to the\nuser. `conclusive: false` means the OS returned an answer we cannot attribute — on Android,\nbouncing off Health Connect's first-run onboarding with \"Go back\" returns an EMPTY permission set\nafter ~20 s, byte-identical to denying everything. Treat it as \"ask again later\", NEVER as denial."
        },
        {
          "name": "AuthorizationSnapshotDto",
          "slug": "authorization-snapshot-dto",
          "kind": "interface",
          "declaration": "/**\n * 인가 스냅샷. **판정은 하지 않는다** — 원시 사실만 넘긴다.\n * iOS: `authorizationStatus`(공유) + `statusForAuthorizationRequest`(시트 여부).\n * Android: `getGrantedPermissions()` + `processImportance()` + `declaredPermissions()`.\n */\ninterface AuthorizationSnapshotDto {\n    readonly platform: WorkoutsPlatform;\n    readonly availability: AvailabilityDto;\n    /** Android: granted permission strings. iOS: share-authorized HK type identifiers. */\n    readonly granted: readonly string[];\n    /**\n     * iOS only, and the reason `write.*` can say `'denied'` rather than a permanent `'undetermined'`:\n     * `HKHealthStore.authorizationStatus(for:)` per DECLARED type identifier, already reduced to our\n     * vocabulary (`sharingAuthorized` -> `'granted'`, `sharingDenied` -> `'denied'`,\n     * `notDetermined` -> `'undetermined'`). It is a SHARE-side fact only — HealthKit never reports a\n     * read status, which is exactly why every iOS read scope is permanently `'unknown'`.\n     * `null` on Android, where the direction is encoded in the permission string and `granted` is\n     * already the whole truth.\n     */\n    readonly statuses?: Readonly<Record<string, 'granted' | 'denied' | 'undetermined'>> | null | undefined;\n    /** Manifest / Info.plist 선언 집합. 선언되지 않은 scope 요청은 `invalidArgument`가 된다. */\n    readonly declared: readonly string[];\n    /** iOS only: a sheet would still appear for at least one requested type. */\n    readonly wouldPrompt: boolean;\n    /** Android only: the process is at IMPORTANCE_FOREGROUND (a hard precondition for foreign routes). */\n    readonly foreground: boolean;\n    /** AOSP `getExerciseRouteReadAccessType`, already reduced to our vocabulary. iOS: always `'all'`. */\n    readonly routeAccess: RouteAccess;\n    /** Android READ_HEALTH_DATA_HISTORY. `null` on iOS — that platform has no wall. */\n    readonly history: boolean | null;\n}",
          "sourceDocumentation": "인가 스냅샷. **판정은 하지 않는다** — 원시 사실만 넘긴다.\niOS: `authorizationStatus`(공유) + `statusForAuthorizationRequest`(시트 여부).\nAndroid: `getGrantedPermissions()` + `processImportance()` + `declaredPermissions()`."
        },
        {
          "name": "AuthorizationState",
          "slug": "authorization-state",
          "kind": "type",
          "declaration": "/**\n * Availability and authorization fused into ONE union. Reading a scope's status on a platform that\n * has no usable health store is **unrepresentable** rather than merely discouraged.\n */\ntype AuthorizationState = {\n    readonly availability: 'unavailable';\n    readonly reason: 'platformTooOld' | 'notSupported';\n} | {\n    readonly availability: 'updateRequired';\n} | {\n    readonly availability: 'available';\n    /** Every scope is always present — no `undefined` holes to guard. */\n    readonly read: Readonly<Record<Scope, ScopeStatus>>;\n    readonly write: Readonly<Record<Scope, ScopeStatus>>;\n    /** Android READ_HEALTH_DATA_HISTORY. Always `'unknown'` on iOS — that platform has no wall,\n     *  and reporting a grant the user never gave would be a lying field. */\n    readonly history: ScopeStatus;\n    readonly routeAccess: RouteAccess;\n};",
          "sourceDocumentation": "Availability and authorization fused into ONE union. Reading a scope's status on a platform that\nhas no usable health store is **unrepresentable** rather than merely discouraged."
        },
        {
          "name": "Availability",
          "slug": "availability",
          "kind": "type",
          "declaration": "type Availability = {\n    readonly status: 'available';\n} | {\n    readonly status: 'unavailable';\n    readonly reason: 'platformTooOld' | 'notSupported';\n}\n/** Android 9–13 without the Play Health Connect provider. Pair with `openStoreListing()`. */\n | {\n    readonly status: 'updateRequired';\n};"
        },
        {
          "name": "AvailabilityDto",
          "slug": "availability-dto",
          "kind": "type",
          "declaration": "type AvailabilityDto = {\n    readonly status: 'available';\n} | {\n    readonly status: 'unavailable';\n    readonly reason: 'platformTooOld' | 'notSupported';\n} | {\n    readonly status: 'updateRequired';\n};"
        },
        {
          "name": "collectRoute",
          "slug": "collect-route",
          "kind": "function",
          "declaration": "/**\n * Concatenate a `getRoute()` stream into one array. Convenience only: a 36 000-point route costs\n * ~15 MB of JS heap, which is why the stream is the default and this is the opt-in.\n */\ndeclare function collectRoute(chunks: AsyncIterable<readonly RoutePoint[]>): Promise<RoutePoint[]>;",
          "sourceDocumentation": "Concatenate a `getRoute()` stream into one array. Convenience only: a 36 000-point route costs\n~15 MB of JS heap, which is why the stream is the default and this is the opt-in."
        },
        {
          "name": "createWorkoutsApi",
          "slug": "create-workouts-api",
          "kind": "function",
          "declaration": "/**\n * The ONLY implementation of the twelve functions.\n *\n * With `native === null` every function rejects with `unavailable` except `getAvailability()`,\n * which resolves to `{ status: 'unavailable', reason: 'notSupported' }`. That is the whole\n * difference between the two `.` branches — the surfaces are structurally identical, which is what\n * `export-parity-guard` locks down.\n */\ndeclare function createWorkoutsApi(native: NativeWorkoutsModule | null, options?: CreateWorkoutsApiOptions): WorkoutsApi;",
          "sourceDocumentation": "The ONLY implementation of the twelve functions.\n\nWith `native === null` every function rejects with `unavailable` except `getAvailability()`,\nwhich resolves to `{ status: 'unavailable', reason: 'notSupported' }`. That is the whole\ndifference between the two `.` branches — the surfaces are structurally identical, which is what\n`export-parity-guard` locks down."
        },
        {
          "name": "CreateWorkoutsApiOptions",
          "slug": "create-workouts-api-options",
          "kind": "interface",
          "declaration": "/** `createWorkoutsApi`의 주입 지점. 전부 테스트가 시간·예산을 소유하기 위한 것이다. */\ninterface CreateWorkoutsApiOptions {\n    readonly now?: (() => number) | undefined;\n    /**\n     * 클라이언트측 읽기 페이서. 기본값은 **Android에서만** 켜진 `ReadBudget` 하나이고\n     * (f102의 계수는 Health Connect의 것이며 HealthKit에는 대응물이 없다), `null`이면 끈다.\n     */\n    readonly budget?: ReadBudget | null | undefined;\n    /** per-route 동의 다이얼로그 상한 (f104). 테스트가 짧은 값으로 hang을 재현한다. */\n    readonly routeConsentTimeoutMs?: number | undefined;\n}",
          "sourceDocumentation": "`createWorkoutsApi`의 주입 지점. 전부 테스트가 시간·예산을 소유하기 위한 것이다."
        },
        {
          "name": "CURSOR_FORMAT_VERSION",
          "slug": "cursor-format-version",
          "kind": "constant",
          "declaration": "CURSOR_FORMAT_VERSION = 1",
          "sourceDocumentation": "OUR format version, not the platform token's."
        },
        {
          "name": "CursorInfo",
          "slug": "cursor-info",
          "kind": "interface",
          "declaration": "interface CursorInfo {\n    /** OUR format version, not the platform token's. */\n    readonly formatVersion: number;\n    readonly platform: WorkoutsPlatform;\n    readonly issuedAtMs: number;\n}"
        },
        {
          "name": "CursorResetReason",
          "slug": "cursor-reset-reason",
          "kind": "type",
          "declaration": "type CursorResetReason = \n/** `cursor === null` — a fresh start. */\n'noCursor'\n/** Bad magic / bad base64url / bad JSON / failed shape validation. */\n | 'malformed'\n/** Magic ok, format version not in `READABLE_CURSOR_VERSIONS`. */\n | 'formatUnsupported'\n/** Minted on the other platform (server-synced cursor, device switch, restore). */\n | 'platformMismatch'\n/** Android: `ChangesResponse.changesTokenExpired === true` (30-day idle). */\n | 'expired'\n/** The granted-scope fingerprint differs from the one baked into the cursor. */\n | 'scopesChanged';"
        },
        {
          "name": "DeleteRefDto",
          "slug": "delete-ref-dto",
          "kind": "interface",
          "declaration": "interface DeleteRefDto {\n    readonly nativeId?: string | null | undefined;\n    readonly clientId?: string | null | undefined;\n}"
        },
        {
          "name": "DeleteResult",
          "slug": "delete-result",
          "kind": "interface",
          "declaration": "interface DeleteResult {\n    /** `false` for an id that was not there. Deleting something absent is never an error. */\n    readonly deleted: boolean;\n}"
        },
        {
          "name": "deleteWorkout",
          "slug": "delete-workout",
          "kind": "constant",
          "declaration": "deleteWorkout: (ref: WorkoutRef) => Promise<DeleteResult>"
        },
        {
          "name": "deniedFromOutcome",
          "slug": "denied-from-outcome",
          "kind": "function",
          "declaration": "/**\n * f120's rule, as a function: a request is only evidence of DENIAL when the platform actually\n * answered. An empty returned set after the Android onboarding \"Go back\" is byte-identical to\n * denying everything, so it must never flip a scope to `'denied'`.\n *\n * Returns the permission strings we asked for and did not get back, or `[]` when the outcome was\n * inconclusive.\n */\ndeclare function deniedFromOutcome(requested: readonly string[], outcome: PermissionOutcomeDto): readonly string[];",
          "sourceDocumentation": "f120's rule, as a function: a request is only evidence of DENIAL when the platform actually\nanswered. An empty returned set after the Android onboarding \"Go back\" is byte-identical to\ndenying everything, so it must never flip a scope to `'denied'`.\n\nReturns the permission strings we asked for and did not get back, or `[]` when the outcome was\ninconclusive."
        },
        {
          "name": "deriveAuthorizationState",
          "slug": "derive-authorization-state",
          "kind": "function",
          "declaration": "/**\n * `AuthorizationSnapshotDto` -> `AuthorizationState`. The ONE place the platform's raw facts become\n * our vocabulary (design §8.8 + the iOS \"read is permanently `unknown`\" rule + the before/after\n * comparison that f120 makes the only honest source of `'denied'`).\n *\n * Every scope is always present in both records — there are no `undefined` holes to guard.\n */\ndeclare function deriveAuthorizationState(snapshot: AuthorizationSnapshotDto, facts?: AuthorizationDerivationFacts | undefined): AuthorizationState;",
          "sourceDocumentation": "`AuthorizationSnapshotDto` -> `AuthorizationState`. The ONE place the platform's raw facts become\nour vocabulary (design §8.8 + the iOS \"read is permanently `unknown`\" rule + the before/after\ncomparison that f120 makes the only honest source of `'denied'`).\n\nEvery scope is always present in both records — there are no `undefined` holes to guard."
        },
        {
          "name": "derivePauses",
          "slug": "derive-pauses",
          "kind": "function",
          "declaration": "/**\n * Gaps of at least `minGapMs` between consecutive points, as pauses. `minGapMs` is required — the\n * threshold that separates \"a GPS fix was late\" from \"the user stopped\" is the caller's domain.\n *\n * `auto` is left `undefined`: these are DERIVED by you, not reported by the platform.\n */\ndeclare function derivePauses(points: readonly RoutePoint[], minGapMs: number): Pause[];",
          "sourceDocumentation": "Gaps of at least `minGapMs` between consecutive points, as pauses. `minGapMs` is required — the\nthreshold that separates \"a GPS fix was late\" from \"the user stopped\" is the caller's domain.\n\n`auto` is left `undefined`: these are DERIVED by you, not reported by the platform."
        },
        {
          "name": "describeCursor",
          "slug": "describe-cursor",
          "kind": "function",
          "declaration": "/**\n * Inspect a cursor for diagnostics and progress UI. Returns `null` for anything this build cannot\n * read - it NEVER throws.\n *\n * It NEVER returns the platform token (HKQueryAnchor / Health Connect changes token): an app that\n * stores its cursor on a server would otherwise be storing the platform's own token. A guard test\n * asserts no substring of the encoded token appears in the returned object.\n */\ndeclare function describeCursor(cursor: string): CursorInfo | null;",
          "sourceDocumentation": "Inspect a cursor for diagnostics and progress UI. Returns `null` for anything this build cannot\nread - it NEVER throws.\n\nIt NEVER returns the platform token (HKQueryAnchor / Health Connect changes token): an app that\nstores its cursor on a server would otherwise be storing the platform's own token. A guard test\nasserts no substring of the encoded token appears in the returned object."
        },
        {
          "name": "DirectedPermissions",
          "slug": "directed-permissions",
          "kind": "interface",
          "declaration": "/** `read`/`write` 두 방향의 플랫폼 문자열. `native-contract.ts`의 `PermissionRequestDto`와 같은 모양. */\ninterface DirectedPermissions {\n    readonly read: readonly string[];\n    readonly write: readonly string[];\n}",
          "sourceDocumentation": "`read`/`write` 두 방향의 플랫폼 문자열. `native-contract.ts`의 `PermissionRequestDto`와 같은 모양."
        },
        {
          "name": "DrainBatchDto",
          "slug": "drain-batch-dto",
          "kind": "interface",
          "declaration": "/** 드레인 한 배치. `checkpoint`는 **이 배치를 만들기 전에** 잡힌 값이다. */\ninterface DrainBatchDto {\n    readonly added: readonly WorkoutDto[];\n    readonly removed: readonly RemovedDto[];\n    readonly checkpoint: string;\n    readonly hasMore: boolean;\n    /** Android `ChangesResponse.changesTokenExpired`. */\n    readonly expired: boolean;\n}",
          "sourceDocumentation": "드레인 한 배치. `checkpoint`는 **이 배치를 만들기 전에** 잡힌 값이다."
        },
        {
          "name": "EPOCH_MS_FLOOR",
          "slug": "epoch-ms-floor",
          "kind": "constant",
          "declaration": "EPOCH_MS_FLOOR = 100000000000",
          "sourceDocumentation": "Every epoch-millisecond input in this library is validated against this floor.\n`1e11` ms is 1973-03-03. A \"now\" expressed in SECONDS is ~1.79e9, which is far below it, while no\nreal workout predates 1973 — so `0 < value < EPOCH_MS_FLOOR` is exactly the seconds-in-a-\nmilliseconds-field mistake and nothing else. It is rejected with `invalidArgument`.\nThis is the one unit accident types cannot catch and the library therefore catches at runtime."
        },
        {
          "name": "estimateAndroidRecordBytes",
          "slug": "estimate-android-record-bytes",
          "kind": "function",
          "declaration": "/**\n * Exact Health Connect record-size model, fitted with residual 0 over six failure samples:\n *   `bytes = 160 + 48·routePoints + 2·(title + notes + clientRecordId chars) + 24·(segments + laps)`\n *\n * The optional route fields are FREE — a 21 000-point route serialises to the byte-identical size\n * with and without altitude and accuracies.\n *\n * Pinned boundary (f99): a 13-char title + 13-char clientRecordId gives `bytes = 212 + 48·points`,\n * and 20 829 points is exactly 1 000 004 B — the first failing size.\n *\n * ⚠ One Mainline build's parcel encoding. A safety margin, not a contract.\n */\ndeclare function estimateAndroidRecordBytes(input: {\n    readonly routePoints: number;\n    readonly clientRecordIdLength: number;\n    readonly titleLength?: number | undefined;\n    readonly notesLength?: number | undefined;\n    readonly segments?: number | undefined;\n    readonly laps?: number | undefined;\n}): number;",
          "sourceDocumentation": "Exact Health Connect record-size model, fitted with residual 0 over six failure samples:\n  `bytes = 160 + 48·routePoints + 2·(title + notes + clientRecordId chars) + 24·(segments + laps)`\n\nThe optional route fields are FREE — a 21 000-point route serialises to the byte-identical size\nwith and without altitude and accuracies.\n\nPinned boundary (f99): a 13-char title + 13-char clientRecordId gives `bytes = 212 + 48·points`,\nand 20 829 points is exactly 1 000 004 B — the first failing size.\n\n⚠ One Mainline build's parcel encoding. A safety margin, not a contract."
        },
        {
          "name": "ExistingWorkoutDto",
          "slug": "existing-workout-dto",
          "kind": "interface",
          "declaration": "interface ExistingWorkoutDto {\n    readonly nativeId: string;\n    readonly version: number;\n}"
        },
        {
          "name": "getAuthorizationState",
          "slug": "get-authorization-state",
          "kind": "constant",
          "declaration": "getAuthorizationState: () => Promise<AuthorizationState>"
        },
        {
          "name": "getAvailability",
          "slug": "get-availability",
          "kind": "constant",
          "declaration": "getAvailability: () => Promise<Availability>"
        },
        {
          "name": "getRoute",
          "slug": "get-route",
          "kind": "constant",
          "declaration": "getRoute: (workoutId: string, options?: GetRouteOptions) => AsyncIterable<readonly RoutePoint[]>"
        },
        {
          "name": "GetRouteOptions",
          "slug": "get-route-options",
          "kind": "interface",
          "declaration": "interface GetRouteOptions {\n    /**\n     * What to do when `routeState === 'consentRequired'` (Android only — HealthKit has no per-route\n     * consent).\n     * - `'skip'` (default) — throw `consentRequired`. Never shows UI, never blocks.\n     * - `'prompt'`         — show the platform's per-route dialog and, if the user allows, stream the\n     *   route from that same call. Can block for tens of seconds, so it must be driven by an explicit\n     *   user gesture. Only one prompt may be in flight per process; a concurrent call throws `busy`.\n     */\n    readonly consent?: 'skip' | 'prompt' | undefined;\n}"
        },
        {
          "name": "hasAndroidIndoorPair",
          "slug": "has-android-indoor-pair",
          "kind": "function",
          "declaration": "/** 이 kind가 Android에서 `indoor`를 왕복시키는가 (= 상수 쌍이 있는가). */\ndeclare function hasAndroidIndoorPair(kind: WorkoutKind): boolean;",
          "sourceDocumentation": "이 kind가 Android에서 `indoor`를 왕복시키는가 (= 상수 쌍이 있는가)."
        },
        {
          "name": "HeartRateDto",
          "slug": "heart-rate-dto",
          "kind": "interface",
          "declaration": "interface HeartRateDto {\n    readonly t: number;\n    readonly bpm: number;\n}"
        },
        {
          "name": "HeartRateSample",
          "slug": "heart-rate-sample",
          "kind": "interface",
          "declaration": "/** One heart-rate reading. The same shape on read and on write. */\ninterface HeartRateSample {\n    /** Epoch MILLISECONDS. */\n    readonly t: number;\n    /** Integer beats per minute, 1..300. Samples outside that range are dropped on write. */\n    readonly bpm: number;\n}",
          "sourceDocumentation": "One heart-rate reading. The same shape on read and on write."
        },
        {
          "name": "HeartRateSummaryDto",
          "slug": "heart-rate-summary-dto",
          "kind": "interface",
          "declaration": "interface HeartRateSummaryDto {\n    readonly avgBpm?: number | null | undefined;\n    readonly minBpm?: number | null | undefined;\n    readonly maxBpm?: number | null | undefined;\n}"
        },
        {
          "name": "Interval",
          "slug": "interval",
          "kind": "interface",
          "declaration": "interface Interval {\n    readonly startMs: number;\n    readonly endMs: number;\n}"
        },
        {
          "name": "IOS_SCOPE_TYPES",
          "slug": "ios-scope-types",
          "kind": "constant",
          "declaration": "IOS_SCOPE_TYPES: Readonly<Record<Scope, readonly string[]>>",
          "sourceDocumentation": "HealthKit 타입 식별자. `elevation`이 **빈 집합**인 것이 이 표의 핵심이다."
        },
        {
          "name": "iosActivityTypeFromKind",
          "slug": "ios-activity-type-from-kind",
          "kind": "function",
          "declaration": "/**\n * WRITE direction, iOS. `indoor` is NOT part of the integer choice on this platform — it is written\n * separately to `HKMetadataKeyIndoorWorkout` (and `HKMetadataKeySwimmingLocationType` for swimming),\n * and OMITTED entirely when `undefined` so the read side can keep telling \"outdoor\" and \"unknown\"\n * apart.\n * ⚠ `kind: 'other'` writes `.other`(3000) and is NOT recoverable on read.\n * ⚠ Never emits 20 or 71 — those are read-aliases only.\n */\ndeclare function iosActivityTypeFromKind(kind: WorkoutKind, indoor?: boolean | undefined): number;",
          "sourceDocumentation": "WRITE direction, iOS. `indoor` is NOT part of the integer choice on this platform — it is written\nseparately to `HKMetadataKeyIndoorWorkout` (and `HKMetadataKeySwimmingLocationType` for swimming),\nand OMITTED entirely when `undefined` so the read side can keep telling \"outdoor\" and \"unknown\"\napart.\n⚠ `kind: 'other'` writes `.other`(3000) and is NOT recoverable on read.\n⚠ Never emits 20 or 71 — those are read-aliases only."
        },
        {
          "name": "iosRequestIdentifiers",
          "slug": "ios-request-identifiers",
          "kind": "function",
          "declaration": "/**\n * iOS: scope -> HK 타입 식별자, **방향별로**. 같은 식별자가 양쪽에 나오는 것이 정상이다 —\n * HealthKit은 하나의 타입에 대해 read와 share를 따로 인가한다.\n * `'elevation'`은 빈 집합이므로 어느 쪽에도 아무것도 더하지 않는다(§8.8).\n */\ndeclare function iosRequestIdentifiers(request: AuthorizationRequest): DirectedPermissions;",
          "sourceDocumentation": "iOS: scope -> HK 타입 식별자, **방향별로**. 같은 식별자가 양쪽에 나오는 것이 정상이다 —\nHealthKit은 하나의 타입에 대해 read와 share를 따로 인가한다.\n`'elevation'`은 빈 집합이므로 어느 쪽에도 아무것도 더하지 않는다(§8.8)."
        },
        {
          "name": "IosWorkout",
          "slug": "ios-workout",
          "kind": "interface",
          "declaration": "interface IosWorkout extends WorkoutBase {\n    readonly platform: 'ios';\n    readonly platformData: IosWorkoutData;\n}"
        },
        {
          "name": "IosWorkoutData",
          "slug": "ios-workout-data",
          "kind": "interface",
          "declaration": "/** Raw iOS values the common model deliberately does not model. */\ninterface IosWorkoutData {\n    /** Raw HKWorkoutActivityType — the escape hatch for everything D11 collapses into 'other'. */\n    readonly activityTypeRaw: number;\n    readonly bundleIdentifier: string;\n    readonly productType?: string | undefined;\n    readonly osVersion?: string | undefined;\n    /** IANA identifier from HKMetadataKeyTimeZone, present only when the writer supplied one. */\n    readonly timeZoneId?: string | undefined;\n    readonly elevationDescendedM?: number | undefined;\n    /** `(endMs - startMs) / 1000`. Differs from `activeDurationS`, which honours the writer's own\n     *  `duration` argument. */\n    readonly wallClockS: number;\n    readonly syncIdentifier?: string | undefined;\n    readonly syncVersion?: number | undefined;\n    /** Number of HKWorkoutActivity entries (multi-sport workouts). */\n    readonly activityCount: number;\n    /** Whether the HKIndoorWorkout metadata key was present — the only honest indoor discriminator. */\n    readonly hasIndoorMetadataKey: boolean;\n    readonly routeSampleCount: number;\n}",
          "sourceDocumentation": "Raw iOS values the common model deliberately does not model."
        },
        {
          "name": "isWorkoutsError",
          "slug": "is-workouts-error",
          "kind": "function",
          "declaration": "/**\n * `instanceof` is unreliable across entries (see the file header). This guard uses the\n * `Symbol.for('gj-kit.workouts.error')` tag, which every copy of the class shares.\n */\ndeclare function isWorkoutsError(error: unknown): error is WorkoutsError;",
          "sourceDocumentation": "`instanceof` is unreliable across entries (see the file header). This guard uses the\n`Symbol.for('gj-kit.workouts.error')` tag, which every copy of the class shares."
        },
        {
          "name": "kindFromAndroidExerciseType",
          "slug": "kind-from-android-exercise-type",
          "kind": "function",
          "declaration": "/**\n * Health Connect exerciseType → WorkoutKind + indoor. TOTAL over `number`, same contract as above.\n *\n * ⚠ **The raw value is already destroyed before it reaches us.** Health Connect's\n *   `IntDefMappingsKt` collapses any unmapped int to 0 (`EXERCISE_TYPE_OTHER_WORKOUT`) on BOTH the\n *   read and the write IPC path. So for a future activity `platformData.android.exerciseType` reads\n *   0, not the real value, and `'other'` is all the information that exists.\n *\n * `indoor` is only decidable for the four kinds with a constant PAIR; for the other five it is\n * `undefined` because Health Connect stores the fact nowhere.\n *\n * Android has no read-aliases: every Health Connect constant we accept, we also emit.\n */\ndeclare function kindFromAndroidExerciseType(raw: number): {\n    kind: WorkoutKind;\n    indoor?: boolean | undefined;\n};",
          "sourceDocumentation": "Health Connect exerciseType → WorkoutKind + indoor. TOTAL over `number`, same contract as above.\n\n⚠ **The raw value is already destroyed before it reaches us.** Health Connect's\n  `IntDefMappingsKt` collapses any unmapped int to 0 (`EXERCISE_TYPE_OTHER_WORKOUT`) on BOTH the\n  read and the write IPC path. So for a future activity `platformData.android.exerciseType` reads\n  0, not the real value, and `'other'` is all the information that exists.\n\n`indoor` is only decidable for the four kinds with a constant PAIR; for the other five it is\n`undefined` because Health Connect stores the fact nowhere.\n\nAndroid has no read-aliases: every Health Connect constant we accept, we also emit."
        },
        {
          "name": "kindFromIosActivityType",
          "slug": "kind-from-ios-activity-type",
          "kind": "function",
          "declaration": "/**\n * Raw HKWorkoutActivityType → WorkoutKind. TOTAL over `number`: anything not in the table —\n * including negative, non-integer and huge values that only the JS boundary can produce — returns\n * `{ kind: 'other' }` with `indoor` left `undefined`.\n *\n * ⚠ **`indoor` never comes from this function on iOS.** HealthKit carries it in the\n *   `HKIndoorWorkout` metadata key (and, for swimming, in `HKMetadataKeySwimmingLocationType`),\n *   orthogonally to the activity type. The return shape keeps `indoor` for symmetry with the\n *   Android mapper and is always `undefined` here.\n *\n * ⚠ **Nothing collapses on iOS.** `HKWorkoutActivityType` is a plain `NSUInteger`, so an unknown\n *   value (e.g. 16 = Elliptical) arrives intact and IS preserved in\n *   `platformData.ios.activityTypeRaw` — an app can recover it. Contrast\n *   `kindFromAndroidExerciseType`.\n *\n * READ-ALIASES: 20 (FunctionalStrengthTraining) → `'strength'` and 71 (WheelchairRunPace) →\n * `'wheelchair'` map INTO kinds the write direction never emits, so the two mapper directions are\n * NOT literal inverses. The asserted property is write-then-read only.\n */\ndeclare function kindFromIosActivityType(raw: number): {\n    kind: WorkoutKind;\n    indoor?: boolean | undefined;\n};",
          "sourceDocumentation": "Raw HKWorkoutActivityType → WorkoutKind. TOTAL over `number`: anything not in the table —\nincluding negative, non-integer and huge values that only the JS boundary can produce — returns\n`{ kind: 'other' }` with `indoor` left `undefined`.\n\n⚠ **`indoor` never comes from this function on iOS.** HealthKit carries it in the\n  `HKIndoorWorkout` metadata key (and, for swimming, in `HKMetadataKeySwimmingLocationType`),\n  orthogonally to the activity type. The return shape keeps `indoor` for symmetry with the\n  Android mapper and is always `undefined` here.\n\n⚠ **Nothing collapses on iOS.** `HKWorkoutActivityType` is a plain `NSUInteger`, so an unknown\n  value (e.g. 16 = Elliptical) arrives intact and IS preserved in\n  `platformData.ios.activityTypeRaw` — an app can recover it. Contrast\n  `kindFromAndroidExerciseType`.\n\nREAD-ALIASES: 20 (FunctionalStrengthTraining) → `'strength'` and 71 (WheelchairRunPace) →\n`'wheelchair'` map INTO kinds the write direction never emits, so the two mapper directions are\nNOT literal inverses. The asserted property is write-then-read only."
        },
        {
          "name": "Lap",
          "slug": "lap",
          "kind": "interface",
          "declaration": "interface Lap extends Interval {\n    readonly distanceM?: number | undefined;\n}"
        },
        {
          "name": "LapDto",
          "slug": "lap-dto",
          "kind": "interface",
          "declaration": "interface LapDto {\n    readonly startMs: number;\n    readonly endMs: number;\n    readonly distanceM?: number | null | undefined;\n}"
        },
        {
          "name": "ListQuery",
          "slug": "list-query",
          "kind": "interface",
          "declaration": "interface ListQuery extends TimeWindow {\n    /** From a previous page's `nextPageToken`. **NOT a sync cursor** — the two carry different magic. */\n    readonly pageToken?: WorkoutsPageToken | undefined;\n}"
        },
        {
          "name": "listWorkouts",
          "slug": "list-workouts",
          "kind": "constant",
          "declaration": "listWorkouts: (query: ListQuery) => Promise<WorkoutPage>"
        },
        {
          "name": "MAX_ANDROID_ROUTE_POINTS",
          "slug": "max-android-route-points",
          "kind": "constant",
          "declaration": "MAX_ANDROID_ROUTE_POINTS = 20000",
          "sourceDocumentation": "The largest route this library will write **on Android**. Health Connect's record ceiling is\nexactly 1 000 000 bytes at `160 + 48·points + 2·chars + 24·(segments+laps)` (20 828 points OK /\n20 829 FAIL, and the optional point fields are FREE). 20 000 leaves ~40 KB of headroom for a\nMainline encoding change.\n\n⚠ This guard does NOT run on iOS. HealthKit was measured storing and streaming a 36 000-point\n  route with no leak and no ceiling. Discarding a user's 8-hour 1 Hz hike on iOS to mirror an\n  Android parcel limit is not defensible. Portability-conscious apps call\n  `estimateAndroidRecordBytes()` themselves."
        },
        {
          "name": "MAX_HEART_RATE_WINDOW_MS",
          "slug": "max-heart-rate-window-ms",
          "kind": "constant",
          "declaration": "MAX_HEART_RATE_WINDOW_MS = 86400000",
          "sourceDocumentation": "24 h. `readHeartRate` refuses wider windows so one call cannot return an unbounded array.\n⚠ This bounds the WINDOW, not the density: a 1 Hz watch still returns ~86 400 samples."
        },
        {
          "name": "MetricProvenance",
          "slug": "metric-provenance",
          "kind": "type",
          "declaration": "/**\n * Where a distance/energy number came from.\n * - 'associated' — summed from samples explicitly associated with the workout.\n * - 'total'      — a total the writer stated but did not back with samples (iOS legacy workouts).\n * - 'derived'    — summed over the workout's window from whatever samples were there.\n *                  **May include other sources.** Treat `derived` as a hint, never as the workout's\n *                  own number.\n */\ntype MetricProvenance = 'associated' | 'total' | 'derived';",
          "sourceDocumentation": "Where a distance/energy number came from.\n- 'associated' — summed from samples explicitly associated with the workout.\n- 'total'      — a total the writer stated but did not back with samples (iOS legacy workouts).\n- 'derived'    — summed over the workout's window from whatever samples were there.\n                 **May include other sources.** Treat `derived` as a hint, never as the workout's\n                 own number."
        },
        {
          "name": "MetricRowDto",
          "slug": "metric-row-dto",
          "kind": "interface",
          "declaration": "/** 한 메트릭 레코드 행. 세션당이 아니라 **페이지 창당** 한 번 읽는다(§8.4). */\ninterface MetricRowDto {\n    readonly type: MetricTypeDto;\n    readonly startMs: number;\n    readonly endMs: number;\n    readonly value: number;\n    /** `dataOrigin.packageName` — 소스별 합산을 `./core`가 하기 위해 필요하다. */\n    readonly origin: string;\n}",
          "sourceDocumentation": "한 메트릭 레코드 행. 세션당이 아니라 **페이지 창당** 한 번 읽는다(§8.4)."
        },
        {
          "name": "MetricTypeDto",
          "slug": "metric-type-dto",
          "kind": "type",
          "declaration": "type MetricTypeDto = 'distance' | 'activeEnergy' | 'elevation' | 'steps';"
        },
        {
          "name": "missingDeclarations",
          "slug": "missing-declarations",
          "kind": "function",
          "declaration": "/**\n * 이 빌드가 **선언한** 집합(`declared`) 밖의 것을 요청했는지. 반환값은 소비자가 고쳐야 할\n * **config-plugin prop 이름**이다 (§5.7 58행: 메시지가 빠진 prop 이름을 말해야 한다).\n *\n * `'elevation'`은 iOS에서 빈 집합이라 선언할 것이 없으므로 여기서 절대 걸리지 않는다.\n */\ndeclare function missingDeclarations(request: AuthorizationRequest, platform: WorkoutsPlatform, declared: readonly string[]): readonly string[];",
          "sourceDocumentation": "이 빌드가 **선언한** 집합(`declared`) 밖의 것을 요청했는지. 반환값은 소비자가 고쳐야 할\n**config-plugin prop 이름**이다 (§5.7 58행: 메시지가 빠진 prop 이름을 말해야 한다).\n\n`'elevation'`은 iOS에서 빈 집합이라 선언할 것이 없으므로 여기서 절대 걸리지 않는다."
        },
        {
          "name": "NATIVE_ERROR_CODES",
          "slug": "native-error-codes",
          "kind": "constant",
          "declaration": "NATIVE_ERROR_CODES: Readonly<Record<string, WorkoutsErrorCode>>",
          "sourceDocumentation": "`ERR_WORKOUTS_*` -> 공개 코드. 14종 전수, 다른 것은 없다."
        },
        {
          "name": "nativeErrorCodeFor",
          "slug": "native-error-code-for",
          "kind": "function",
          "declaration": "/** `'routeTooLarge'` -> `'ERR_WORKOUTS_ROUTE_TOO_LARGE'`. */\ndeclare function nativeErrorCodeFor(code: WorkoutsErrorCode): string;",
          "sourceDocumentation": "`'routeTooLarge'` -> `'ERR_WORKOUTS_ROUTE_TOO_LARGE'`."
        },
        {
          "name": "NativePayloadDto",
          "slug": "native-payload-dto",
          "kind": "interface",
          "declaration": "/** 네이티브가 던지는 예외의 평면 표현 — `mapErrors.ts`의 입력이다. */\ninterface NativePayloadDto {\n    /** `ERR_WORKOUTS_*` — Expo 런타임이 예외 클래스명에서 만든 코드. */\n    readonly code?: string | null | undefined;\n    /** 템플릿으로 만든 짧은 진단 문자열. 좌표·건강값·제목·메모는 절대 들어가지 않는다. */\n    readonly message?: string | null | undefined;\n    /** Health Connect `HealthConnectException` errorCode / HKError code. */\n    readonly platformCode?: number | null | undefined;\n    readonly exceptionClass?: string | null | undefined;\n}",
          "sourceDocumentation": "네이티브가 던지는 예외의 평면 표현 — `mapErrors.ts`의 입력이다."
        },
        {
          "name": "NativeWorkoutsModule",
          "slug": "native-workouts-module",
          "kind": "interface",
          "declaration": "/**\n * 네이티브 모듈 계약. `./testing`의 `createFakeNativeWorkouts()`가 이것을 인메모리로 구현하고,\n * 실물은 `requireOptionalNativeModule('GjKitWorkouts')`가 돌려준다.\n */\ninterface NativeWorkoutsModule {\n    availability(): Promise<AvailabilityDto>;\n    authorizationSnapshot(): Promise<AuthorizationSnapshotDto>;\n    /**\n     * No internal timeout (f120, f122). Returns the raw before/after granted sets.\n     *\n     * ⚠ **Phase 3 correction (design defect found by running the example app).** This member used to\n     *   take ONE flat `readonly string[]`. That is lossless on Android — the direction lives in the\n     *   permission string itself (`READ_EXERCISE` vs `WRITE_EXERCISE`) — but on iOS the SAME type\n     *   identifier serves both directions, so a flat array cannot express what\n     *   `HKHealthStore.requestAuthorization(toShare:read:)` requires: the iOS lane could only either\n     *   over-request share access or silently drop it. The two sets are now explicit.\n     *   `read`/`write` are Android permission strings on Android and HK type identifiers on iOS.\n     *   `history` is Android's `READ_HEALTH_DATA_HISTORY` and rides in `read`.\n     */\n    requestPermissions(request: PermissionRequestDto): Promise<PermissionOutcomeDto>;\n    /** 커서의 `g` 지문을 만드는 원시 연산. 정렬된 granted 권한 문자열 목록을 그대로 준다. */\n    grantedScopeFingerprint(): Promise<string>;\n    /** Start instant in `[fromMs, toMs)`. iOS `.strictStartDate`, Android `TimeRangeFilter.between`. */\n    readWorkoutPage(query: WindowDto & {\n        readonly pageSize: number;\n        readonly pageToken?: string | undefined;\n    }): Promise<WorkoutPageDto>;\n    /** One call per metric type per PAGE WINDOW - never per session (§8.4). Never `aggregate()` (f109). */\n    readMetricRecords(query: WindowDto & {\n        readonly type: MetricTypeDto;\n        readonly origins: readonly string[];\n    }): Promise<readonly MetricRowDto[]>;\n    readHeartRateSamples(query: WindowDto): Promise<readonly HeartRateDto[]>;\n    /** iOS provenance discriminator required by RESULTS 206 / f71. */\n    hasAssociatedSamples(nativeId: string, quantity: QuantityKindDto): Promise<boolean>;\n    takeCheckpoint(): Promise<string>;\n    drainCheckpoint(checkpoint: string, limit: number): Promise<DrainBatchDto>;\n    openRoute(nativeId: string, consent: 'skip' | 'prompt'): Promise<RouteHandleDto>;\n    readRouteChunk(handle: string, maxPoints: number): Promise<readonly RoutePointDto[] | null>;\n    closeRoute(handle: string): Promise<void>;\n    /** iOS: look the workout up by sync identifier BEFORE writing (idx f26). Android: `null`. */\n    findBySyncIdentifier(clientId: string): Promise<ExistingWorkoutDto | null>;\n    saveWorkout(spec: WorkoutWriteDto): Promise<SaveOutcomeDto>;\n    /** Android only, ALWAYS called after a save (f93, f94). `null` when nothing was found. */\n    readBackVersion(clientId: string): Promise<number | null>;\n    deleteWorkout(ref: DeleteRefDto): Promise<boolean>;\n    openSettings(): Promise<void>;\n    openStoreListing(): Promise<void>;\n}",
          "sourceDocumentation": "네이티브 모듈 계약. `./testing`의 `createFakeNativeWorkouts()`가 이것을 인메모리로 구현하고,\n실물은 `requireOptionalNativeModule('GjKitWorkouts')`가 돌려준다."
        },
        {
          "name": "normalizeRouteForWrite",
          "slug": "normalize-route-for-write",
          "kind": "function",
          "declaration": "/**\n * Apply the write-side hygiene the library performs, so you can see what will happen before you call\n * `saveWorkout`. Throws `invalidArgument` for out-of-range coordinates. The rules and their order\n * are §8.2's and are pinned by `tests/fixtures/route-vectors.json`, which also drives the Swift and\n * Kotlin tests.\n *\n * Order (do not reorder — the platforms disagree and this order is what makes them agree):\n *  1. an EMPTY array is `invalidArgument` — say `route: 'none'` instead;\n *  2. non-finite `t`/`lat`/`lon`, or `lat` outside ±90 / `lon` outside ±180, is `invalidArgument`\n *     (rejected, NOT dropped — silently discarding hides a data-corruption signal);\n *  3. points with `hAccM < 0` or `hAccM > 50` are DROPPED (`hAccM === undefined` is kept);\n *  4. points outside `[window.startMs, window.endMs)` are DROPPED — deliberately not clamped:\n *     clamping piles every out-of-window point onto one boundary instant and rule 5 then collapses\n *     them into a single point, i.e. it fabricates timestamps AND destroys more data;\n *  5. sorted ascending by `t`, and for equal `t` the LAST point in input order wins — matching what\n *     HealthKit silently does, so that both platforms agree instead of one throwing.\n *\n * Zero survivors is not an error here: `saveWorkout` reports it as `route: 'dropped'`.\n */\ndeclare function normalizeRouteForWrite(points: readonly RoutePoint[], window: Interval): readonly RoutePoint[];",
          "sourceDocumentation": "Apply the write-side hygiene the library performs, so you can see what will happen before you call\n`saveWorkout`. Throws `invalidArgument` for out-of-range coordinates. The rules and their order\nare §8.2's and are pinned by `tests/fixtures/route-vectors.json`, which also drives the Swift and\nKotlin tests.\n\nOrder (do not reorder — the platforms disagree and this order is what makes them agree):\n 1. an EMPTY array is `invalidArgument` — say `route: 'none'` instead;\n 2. non-finite `t`/`lat`/`lon`, or `lat` outside ±90 / `lon` outside ±180, is `invalidArgument`\n    (rejected, NOT dropped — silently discarding hides a data-corruption signal);\n 3. points with `hAccM < 0` or `hAccM > 50` are DROPPED (`hAccM === undefined` is kept);\n 4. points outside `[window.startMs, window.endMs)` are DROPPED — deliberately not clamped:\n    clamping piles every out-of-window point onto one boundary instant and rule 5 then collapses\n    them into a single point, i.e. it fabricates timestamps AND destroys more data;\n 5. sorted ascending by `t`, and for equal `t` the LAST point in input order wins — matching what\n    HealthKit silently does, so that both platforms agree instead of one throwing.\n\nZero survivors is not an error here: `saveWorkout` reports it as `route: 'dropped'`."
        },
        {
          "name": "openSettings",
          "slug": "open-settings",
          "kind": "constant",
          "declaration": "openSettings: () => Promise<void>"
        },
        {
          "name": "openStoreListing",
          "slug": "open-store-listing",
          "kind": "constant",
          "declaration": "openStoreListing: () => Promise<void>"
        },
        {
          "name": "Pause",
          "slug": "pause",
          "kind": "interface",
          "declaration": "/** `auto` is true for platform-detected pauses (HK motionPaused / HC REST-flagged segments). */\ninterface Pause extends Interval {\n    readonly auto?: boolean | undefined;\n}",
          "sourceDocumentation": "`auto` is true for platform-detected pauses (HK motionPaused / HC REST-flagged segments)."
        },
        {
          "name": "PauseDto",
          "slug": "pause-dto",
          "kind": "interface",
          "declaration": "interface PauseDto {\n    readonly startMs: number;\n    readonly endMs: number;\n    readonly auto?: boolean | null | undefined;\n}"
        },
        {
          "name": "PermissionOutcomeDto",
          "slug": "permission-outcome-dto",
          "kind": "interface",
          "declaration": "interface PermissionOutcomeDto {\n    readonly before: readonly string[];\n    readonly after: readonly string[];\n    readonly conclusive: boolean;\n}"
        },
        {
          "name": "PermissionRequestDto",
          "slug": "permission-request-dto",
          "kind": "interface",
          "declaration": "/**\n * 권한 요청의 원시 결과. **before/after 비교가 판정의 유일한 근거다** — contract의 반환 집합을\n * \"사용자가 방금 부여한 것\"으로 읽지 않는다.\n * `conclusive: false`는 플랫폼이 아무것도 돌려주지 않았다는 뜻이며(f120), 그때 scope 상태는 불변이다.\n */\n/**\n * 권한 요청의 **방향이 있는** 입력 (Phase 3 결함 B). 플랫폼별 문자열 어휘:\n *  - Android — `android.permission.health.READ_*` / `…WRITE_*`. `read`와 `write`의 합집합이\n *    contract 집합이며, `READ_EXERCISE_ROUTES`는 **절대 여기 들어오지 않는다**(f110).\n *  - iOS — HealthKit 타입 식별자. `read`는 `requestAuthorization(read:)`, `write`는 `toShare:`다.\n *    같은 식별자가 양쪽에 동시에 나타나는 것이 정상이다.\n */\ninterface PermissionRequestDto {\n    readonly read: readonly string[];\n    readonly write: readonly string[];\n}",
          "sourceDocumentation": "권한 요청의 **방향이 있는** 입력 (Phase 3 결함 B). 플랫폼별 문자열 어휘:\n - Android — `android.permission.health.READ_*` / `…WRITE_*`. `read`와 `write`의 합집합이\n   contract 집합이며, `READ_EXERCISE_ROUTES`는 **절대 여기 들어오지 않는다**(f110).\n - iOS — HealthKit 타입 식별자. `read`는 `requestAuthorization(read:)`, `write`는 `toShare:`다.\n   같은 식별자가 양쪽에 동시에 나타나는 것이 정상이다."
        },
        {
          "name": "QuantityKindDto",
          "slug": "quantity-kind-dto",
          "kind": "type",
          "declaration": "/** iOS provenance 판별에 쓰이는 두 종류 (RESULTS 206 / f71). */\ntype QuantityKindDto = 'distance' | 'activeEnergy';",
          "sourceDocumentation": "iOS provenance 판별에 쓰이는 두 종류 (RESULTS 206 / f71)."
        },
        {
          "name": "READABLE_CURSOR_VERSIONS",
          "slug": "readable-cursor-versions",
          "kind": "constant",
          "declaration": "READABLE_CURSOR_VERSIONS: readonly number[]",
          "sourceDocumentation": "Every version this build can still READ.\n주의: 이 목록을 줄이는 것은 BREAKING change다 — 기존 사용자 전부가 전체 백필로 되돌아간다.\nCHANGELOG에 그렇게 명시해야 한다."
        },
        {
          "name": "ReadBudget",
          "slug": "read-budget",
          "kind": "class",
          "declaration": "/**\n * The client-side read pacer.\n *\n * Budgets: 900 / 15 min and 4 500 / 24 h — 10 % under the measured device constants (1000 / 5000),\n * because those are server-pushed and a Mainline update can move them.\n *\n * It NEVER blocks: it refuses an over-budget call BEFORE the platform call with `rateLimited` and a\n * computed `retryAfterMs`. Sleeping inside a 60-second poll would stall the consumer's whole\n * single-flight pipeline.\n *\n * ⚠ Process-local and blind to another health library in the same app: Health Connect's own limiter\n *   is per-uid, so during a migration off another library our accounting is optimistic.\n */\ndeclare class ReadBudget {\n    private readonly now;\n    private readonly spends;\n    constructor(options?: ReadBudgetOptions);\n    private prune;\n    private countSince;\n    /**\n     * `null` when `count` more reads fit right now; otherwise the milliseconds to wait before the\n     * oldest blocking spend leaves its window. NEVER blocks and NEVER throws.\n     */\n    retryAfterMs(count?: number): number | null;\n    /**\n     * Charge `count` reads, or refuse with `rateLimited` + `retryAfterMs` BEFORE the platform call.\n     * Nothing is charged when it refuses.\n     */\n    spend(count?: number): void;\n    /** 진단용 — 두 창의 현재 사용량. */\n    usage(): {\n        readonly shortWindow: number;\n        readonly longWindow: number;\n    };\n}",
          "sourceDocumentation": "The client-side read pacer.\n\nBudgets: 900 / 15 min and 4 500 / 24 h — 10 % under the measured device constants (1000 / 5000),\nbecause those are server-pushed and a Mainline update can move them.\n\nIt NEVER blocks: it refuses an over-budget call BEFORE the platform call with `rateLimited` and a\ncomputed `retryAfterMs`. Sleeping inside a 60-second poll would stall the consumer's whole\nsingle-flight pipeline.\n\n⚠ Process-local and blind to another health library in the same app: Health Connect's own limiter\n  is per-uid, so during a migration off another library our accounting is optimistic."
        },
        {
          "name": "ReadBudgetOptions",
          "slug": "read-budget-options",
          "kind": "interface",
          "declaration": "interface ReadBudgetOptions {\n    /** 주입 클록. 테스트가 시간을 소유한다. */\n    readonly now?: (() => number) | undefined;\n}"
        },
        {
          "name": "readHeartRate",
          "slug": "read-heart-rate",
          "kind": "constant",
          "declaration": "readHeartRate: (window: TimeWindow) => Promise<readonly HeartRateSample[]>"
        },
        {
          "name": "readSteps",
          "slug": "read-steps",
          "kind": "constant",
          "declaration": "readSteps: (window: TimeWindow) => Promise<StepTotal>"
        },
        {
          "name": "reconcileSyncPage",
          "slug": "reconcile-sync-page",
          "kind": "function",
          "declaration": "/**\n * Split one sync page into the three operations a local store actually performs.\n *\n * - `rekeys` come from `removed[].replaced === true` matched against the same page's `added` —\n *   iOS replaces a workout's native id when the same sync identifier is re-saved. Apply them as an\n *   UPDATE of the primary key, NEVER as DELETE + INSERT, or you lose your local join data (server\n *   ids, upload state, notes).\n * - `deletes` are the genuinely-gone ids. Applying one for an id you never held must be a no-op.\n *\n * ⚠ Matching heuristic, stated out loud: `RemovedWorkout` deliberately does not carry a\n *   `replacedById` field, so a replaced removal is paired, in order, with the same page's own\n *   writes (`isOwn && clientId != null`). A batch that carries several replacements at once pairs\n *   them positionally, which is what the platform emits. A replaced removal that finds no partner\n *   is NEVER turned into a delete — `replaced: true` means the workout still exists, and deleting\n *   it would destroy the caller's join data.\n */\ndeclare function reconcileSyncPage(page: Pick<SyncPage, 'added' | 'removed'>): {\n    readonly upserts: readonly Workout[];\n    readonly deletes: readonly string[];\n    readonly rekeys: readonly {\n        readonly fromId: string;\n        readonly toId: string;\n    }[];\n};",
          "sourceDocumentation": "Split one sync page into the three operations a local store actually performs.\n\n- `rekeys` come from `removed[].replaced === true` matched against the same page's `added` —\n  iOS replaces a workout's native id when the same sync identifier is re-saved. Apply them as an\n  UPDATE of the primary key, NEVER as DELETE + INSERT, or you lose your local join data (server\n  ids, upload state, notes).\n- `deletes` are the genuinely-gone ids. Applying one for an id you never held must be a no-op.\n\n⚠ Matching heuristic, stated out loud: `RemovedWorkout` deliberately does not carry a\n  `replacedById` field, so a replaced removal is paired, in order, with the same page's own\n  writes (`isOwn && clientId != null`). A batch that carries several replacements at once pairs\n  them positionally, which is what the platform emits. A replaced removal that finds no partner\n  is NEVER turned into a delete — `replaced: true` means the workout still exists, and deleting\n  it would destroy the caller's join data."
        },
        {
          "name": "RemovedDto",
          "slug": "removed-dto",
          "kind": "interface",
          "declaration": "interface RemovedDto {\n    readonly id: string;\n    readonly replaced: boolean;\n}"
        },
        {
          "name": "RemovedWorkout",
          "slug": "removed-workout",
          "kind": "interface",
          "declaration": "interface RemovedWorkout {\n    readonly id: string;\n    /**\n     * `true` only with POSITIVE evidence that the same logical workout still exists under a different\n     * native id. **Always `false` on Android** — an upsert there keeps the same deterministic UUID.\n     *\n     * `false` does NOT mean \"definitely and permanently deleted\": HealthKit may purge deletion records\n     * before we ever see them, so a workout can vanish with no `removed` entry at all.\n     */\n    readonly replaced: boolean;\n}"
        },
        {
          "name": "requestAuthorization",
          "slug": "request-authorization",
          "kind": "constant",
          "declaration": "requestAuthorization: (request: AuthorizationRequest) => Promise<AuthorizationResult>"
        },
        {
          "name": "requiredWriteScopes",
          "slug": "required-write-scopes",
          "kind": "function",
          "declaration": "/**\n * WRITE-side pre-flight. Derives, from the fields a `WorkoutWrite` ACTUALLY CARRIES, which write\n * scopes its single `insertRecords` transaction will need. A workout with no `distanceM` needs no\n * `'distance'` scope, so nothing is over-demanded.\n *\n * `saveWorkout` calls this BEFORE touching the store and throws `notAuthorized` naming the missing\n * scope. Without it, owner decision ②'s split would ship a hard REGRESSION: Android writes the\n * session and all five metric records in ONE transaction, so a missing `WRITE_DISTANCE` fails the\n * whole transaction and the workout is not saved at all.\n *\n * `'routes'` is the documented exception and is NEVER a throw: a missing route scope stays the\n * established non-fatal path (`SaveResult.route === 'notPermitted'`).\n *\n * `steps <= 0` deliberately does NOT demand `'steps'`: Health Connect throws on a zero-count\n * `StepsRecord`, so the library never writes one.\n */\ndeclare function requiredWriteScopes(workout: WorkoutWrite): readonly Scope[];",
          "sourceDocumentation": "WRITE-side pre-flight. Derives, from the fields a `WorkoutWrite` ACTUALLY CARRIES, which write\nscopes its single `insertRecords` transaction will need. A workout with no `distanceM` needs no\n`'distance'` scope, so nothing is over-demanded.\n\n`saveWorkout` calls this BEFORE touching the store and throws `notAuthorized` naming the missing\nscope. Without it, owner decision ②'s split would ship a hard REGRESSION: Android writes the\nsession and all five metric records in ONE transaction, so a missing `WRITE_DISTANCE` fails the\nwhole transaction and the workout is not saved at all.\n\n`'routes'` is the documented exception and is NEVER a throw: a missing route scope stays the\nestablished non-fatal path (`SaveResult.route === 'notPermitted'`).\n\n`steps <= 0` deliberately does NOT demand `'steps'`: Health Connect throws on a zero-count\n`StepsRecord`, so the library never writes one."
        },
        {
          "name": "RouteAccess",
          "slug": "route-access",
          "kind": "type",
          "declaration": "/**\n * How far route reads reach right now.\n * - 'all'      — the route permission is held AND the app is in the foreground.\n * - 'own'      — only routes this app wrote read inline.\n * - 'perRoute' — nothing reads inline; each route needs `getRoute(id, { consent: 'prompt' })`.\n *\n * ⚠ On iOS this is always `'all'` and is NOT evidence of anything — read it together with\n *   `read.routes === 'unknown'`.\n * ⚠ On Android `'all'` does NOT guarantee a route read succeeds: Health Connect's first-run\n *   onboarding is an undocumented further precondition. `'all'` + `getRoute` throwing\n *   `consentRequired` is the signature of incomplete onboarding — send the user to `openSettings()`.\n * CLOSED for 1.x.\n */\ntype RouteAccess = 'all' | 'own' | 'perRoute';",
          "sourceDocumentation": "How far route reads reach right now.\n- 'all'      — the route permission is held AND the app is in the foreground.\n- 'own'      — only routes this app wrote read inline.\n- 'perRoute' — nothing reads inline; each route needs `getRoute(id, { consent: 'prompt' })`.\n\n⚠ On iOS this is always `'all'` and is NOT evidence of anything — read it together with\n  `read.routes === 'unknown'`.\n⚠ On Android `'all'` does NOT guarantee a route read succeeds: Health Connect's first-run\n  onboarding is an undocumented further precondition. `'all'` + `getRoute` throwing\n  `consentRequired` is the signature of incomplete onboarding — send the user to `openSettings()`.\nCLOSED for 1.x."
        },
        {
          "name": "routeDistanceM",
          "slug": "route-distance-m",
          "kind": "function",
          "declaration": "/** Great-circle length of a route, metres. Ignores altitude. */\ndeclare function routeDistanceM(points: readonly RoutePoint[]): number;",
          "sourceDocumentation": "Great-circle length of a route, metres. Ignores altitude."
        },
        {
          "name": "routeElevationGainM",
          "slug": "route-elevation-gain-m",
          "kind": "function",
          "declaration": "/**\n * Cumulative ascent, metres, with hysteresis: only rises of at least `minRiseM` count.\n * Required on purpose — \"what counts as a climb\" differs between hiking and cycling apps and there\n * is no defensible default.\n *\n * Points without `altM` are skipped; they neither break nor extend a rise.\n */\ndeclare function routeElevationGainM(points: readonly RoutePoint[], minRiseM: number): number;",
          "sourceDocumentation": "Cumulative ascent, metres, with hysteresis: only rises of at least `minRiseM` count.\nRequired on purpose — \"what counts as a climb\" differs between hiking and cycling apps and there\nis no defensible default.\n\nPoints without `altM` are skipped; they neither break nor extend a rise."
        },
        {
          "name": "RouteHandleDto",
          "slug": "route-handle-dto",
          "kind": "interface",
          "declaration": "interface RouteHandleDto {\n    /** 핸들 문자열. `closeRoute(handle)`에 그대로 되돌려준다. */\n    readonly handle: string;\n    /** 이 워크아웃의 route 상태 — **매 읽기마다 재계산된 값**이다(f114, 절대 캐시 금지). */\n    readonly state: RouteState;\n}"
        },
        {
          "name": "RoutePoint",
          "slug": "route-point",
          "kind": "interface",
          "declaration": "/**\n * One GPS fix. SI units, unit in the field name.\n *\n * Negative CoreLocation sentinels (`-1`) are mapped to `undefined` for `hAccM`, `vAccM`, `speedMps`\n * and `courseDeg`; an explicit `0` is PRESERVED as `0`, because HealthKit preserves it.\n * `altM` is passed through verbatim — a negative altitude is a legal value (Dead Sea), not a\n * sentinel; `vAccM` is the actual validity flag for it.\n */\ninterface RoutePoint {\n    /** Epoch MILLISECONDS. Strictly increasing after our normalisation. */\n    readonly t: number;\n    /** WGS84 degrees, -90..90. Out of range is `invalidArgument` on BOTH platforms. */\n    readonly lat: number;\n    /** WGS84 degrees, -180..180. */\n    readonly lon: number;\n    readonly altM?: number | undefined;\n    /** Horizontal accuracy, metres. */\n    readonly hAccM?: number | undefined;\n    /** Vertical accuracy, metres. */\n    readonly vAccM?: number | undefined;\n    /** iOS only — Health Connect's `ExerciseRoute.Location` has no speed field. */\n    readonly speedMps?: number | undefined;\n    /** iOS only. */\n    readonly courseDeg?: number | undefined;\n}",
          "sourceDocumentation": "One GPS fix. SI units, unit in the field name.\n\nNegative CoreLocation sentinels (`-1`) are mapped to `undefined` for `hAccM`, `vAccM`, `speedMps`\nand `courseDeg`; an explicit `0` is PRESERVED as `0`, because HealthKit preserves it.\n`altM` is passed through verbatim — a negative altitude is a legal value (Dead Sea), not a\nsentinel; `vAccM` is the actual validity flag for it."
        },
        {
          "name": "RoutePointDto",
          "slug": "route-point-dto",
          "kind": "interface",
          "declaration": "interface RoutePointDto {\n    readonly t: number;\n    readonly lat: number;\n    readonly lon: number;\n    readonly altM?: number | null | undefined;\n    readonly hAccM?: number | null | undefined;\n    readonly vAccM?: number | null | undefined;\n    readonly speedMps?: number | null | undefined;\n    readonly courseDeg?: number | null | undefined;\n}"
        },
        {
          "name": "RouteState",
          "slug": "route-state",
          "kind": "type",
          "declaration": "/**\n * Whether a GPS route can be read for a workout, RECOMPUTED ON EVERY READ.\n * Never cache this across app sessions: on Android an app can lose read access to routes it wrote\n * itself once both route scopes are revoked.\n *\n * - 'available'       — the route can be streamed with `getRoute()` right now.\n * - 'consentRequired' — a route EXISTS but is not readable. **Never collapse this to 'none'.**\n * - 'none'            — there is no route at all. On iOS this is also what a denied read looks like.\n * CLOSED for 1.x.\n */\ntype RouteState = 'available' | 'consentRequired' | 'none';",
          "sourceDocumentation": "Whether a GPS route can be read for a workout, RECOMPUTED ON EVERY READ.\nNever cache this across app sessions: on Android an app can lose read access to routes it wrote\nitself once both route scopes are revoked.\n\n- 'available'       — the route can be streamed with `getRoute()` right now.\n- 'consentRequired' — a route EXISTS but is not readable. **Never collapse this to 'none'.**\n- 'none'            — there is no route at all. On iOS this is also what a denied read looks like.\nCLOSED for 1.x."
        },
        {
          "name": "RouteWriteOutcome",
          "slug": "route-write-outcome",
          "kind": "type",
          "declaration": "/**\n * What happened to `route`.\n *  - 'stored'       — written and readable.\n *  - 'none'         — you passed `'none'`.\n *  - 'dropped'      — you passed points but NOTHING survived hygiene; the workout was still saved.\n *  - 'notPermitted' — Android: WRITE_EXERCISE_ROUTE is not granted; the workout was still saved.\n *                     ⚠ On a re-save this means the previously stored route is now GONE.\n *  - 'deferred'     — `status === 'pendingUnlock'`; the retry will attach it.\n */\ntype RouteWriteOutcome = 'stored' | 'none' | 'dropped' | 'notPermitted' | 'deferred';",
          "sourceDocumentation": "What happened to `route`.\n - 'stored'       — written and readable.\n - 'none'         — you passed `'none'`.\n - 'dropped'      — you passed points but NOTHING survived hygiene; the workout was still saved.\n - 'notPermitted' — Android: WRITE_EXERCISE_ROUTE is not granted; the workout was still saved.\n                    ⚠ On a re-save this means the previously stored route is now GONE.\n - 'deferred'     — `status === 'pendingUnlock'`; the retry will attach it."
        },
        {
          "name": "SaveOutcomeDto",
          "slug": "save-outcome-dto",
          "kind": "interface",
          "declaration": "interface SaveOutcomeDto {\n    readonly status: 'saved' | 'pendingUnlock';\n    readonly nativeId?: string | null | undefined;\n    readonly route: RouteWriteOutcome;\n    readonly routePointsWritten: number;\n}"
        },
        {
          "name": "SaveResult",
          "slug": "save-result",
          "kind": "type",
          "declaration": "/**\n * A discriminated union, so `nativeId` does not EXIST on the `pendingUnlock` branch. That branch\n * only ever appears on a locked device, i.e. never during development, so a type that merely made\n * `nativeId` optional would be forgotten by everyone.\n */\ntype SaveResult = {\n    readonly status: 'saved';\n    /** Echo of `WorkoutWrite.id`. */\n    readonly id: string;\n    /** The platform's own id for the stored workout. */\n    readonly nativeId: string;\n    readonly route: Exclude<RouteWriteOutcome, 'deferred'>;\n    /** How many points actually reached the store. Compare it against what you sent to see how\n     *  much our mandatory hygiene removed. */\n    readonly routePointsWritten: number;\n} | {\n    /**\n     * The store accepted the workout but cannot confirm it while the device is locked.\n     * **Do not re-save blindly.** Call `saveWorkout` again with the SAME `id` and `version` once\n     * the device is unlocked; that call is idempotent and completes the route.\n     */\n    readonly status: 'pendingUnlock';\n    readonly id: string;\n    readonly route: 'deferred';\n    readonly routePointsWritten: 0;\n};",
          "sourceDocumentation": "A discriminated union, so `nativeId` does not EXIST on the `pendingUnlock` branch. That branch\nonly ever appears on a locked device, i.e. never during development, so a type that merely made\n`nativeId` optional would be forgotten by everyone."
        },
        {
          "name": "saveWorkout",
          "slug": "save-workout",
          "kind": "constant",
          "declaration": "saveWorkout: (workout: WorkoutWrite) => Promise<SaveResult>"
        },
        {
          "name": "Scope",
          "slug": "scope",
          "kind": "type",
          "declaration": "type Scope = (typeof SCOPES)[number];"
        },
        {
          "name": "SCOPES",
          "slug": "scopes",
          "kind": "constant",
          "declaration": "SCOPES: readonly [\n    \"workouts\",\n    \"distance\",\n    \"activeEnergy\",\n    \"elevation\",\n    \"routes\",\n    \"heartRate\",\n    \"steps\"\n]",
          "sourceDocumentation": "One authorization vocabulary for both platforms. Read calls have NO `include` flags — capability\nis chosen once, at authorization time. CLOSED for 1.x.\n\nOwner decision ② (2026-08-22) split this union from four members to seven so the consuming\ndeveloper chooses the granularity. Use `WORKOUT_TOTALS_SCOPES` for the coarse form; name the\nmembers individually for the fine form.\n\n⚠ **`'workouts'` no longer implies totals.** `read: ['workouts']` is valid code before and after\n  this change and means something materially different after: ONE Android permission row instead\n  of four, and `distanceM` / `activeEnergyKcal` / `elevationGainM` `undefined` on EVERY workout.\n\n- `workouts`     — the exercise SESSION and its intrinsic fields only.\n- `distance`     — gates `Workout.distanceM` + `distanceProvenance`. iOS requests BOTH\n                   `.distanceWalkingRunning` AND `.distanceCycling`, always both.\n- `activeEnergy` — gates `Workout.activeEnergyKcal` + `activeEnergyProvenance`. Named\n                   `activeEnergy` and NOT `energy`: `TotalCaloriesBurnedRecord` is forbidden as a\n                   fallback because it silently mixes in BMR.\n- `elevation`    — gates `Workout.elevationGainM`. ⚠ On iOS this maps to the EMPTY HealthKit set\n                   and therefore ALIASES `workouts`.\n- `routes`       — read maps to READ_EXERCISE_ROUTES, which is manifest-declared and NEVER\n                   requestable at runtime; write maps to WRITE_EXERCISE_ROUTE (singular).\n- `heartRate` / `steps` — the READ_/WRITE_ pair for that type; each also gates its own top-level\n                   read function."
        },
        {
          "name": "ScopeStatus",
          "slug": "scope-status",
          "kind": "type",
          "declaration": "/**\n * - 'granted'      — proceed.\n * - 'denied'       — the user said no. `openSettings()`; asking again will not help.\n * - 'undetermined' — never asked, OR the last request was inconclusive. Call `requestAuthorization()`.\n * - 'unknown'      — unknowable by platform design. EVERY iOS read scope that has already been asked\n *                    about reports this, permanently. Proceed, and treat an empty result as\n *                    ambiguous rather than as \"no data\".\n * CLOSED for 1.x.\n */\ntype ScopeStatus = 'granted' | 'denied' | 'undetermined' | 'unknown';",
          "sourceDocumentation": "- 'granted'      — proceed.\n- 'denied'       — the user said no. `openSettings()`; asking again will not help.\n- 'undetermined' — never asked, OR the last request was inconclusive. Call `requestAuthorization()`.\n- 'unknown'      — unknowable by platform design. EVERY iOS read scope that has already been asked\n                   about reports this, permanently. Proceed, and treat an empty result as\n                   ambiguous rather than as \"no data\".\nCLOSED for 1.x."
        },
        {
          "name": "SourceDto",
          "slug": "source-dto",
          "kind": "interface",
          "declaration": "interface SourceDto {\n    readonly id: string;\n    readonly name?: string | null | undefined;\n    readonly version?: string | null | undefined;\n    readonly deviceModel?: string | null | undefined;\n}"
        },
        {
          "name": "StepTotal",
          "slug": "step-total",
          "kind": "interface",
          "declaration": "interface StepTotal {\n    /**\n     * Steps in the window. `0` is a real answer.\n     * When several apps wrote steps over the window this is the LARGEST SINGLE-`dataOrigin` total, not\n     * the sum — a phone + watch device is never double-counted. It will therefore disagree with the\n     * number Health Connect's own UI shows, which merges by an app-priority list we cannot read.\n     * On iOS a denied read scope is indistinguishable from no data, so `0` can also mean \"not granted\".\n     */\n    readonly count: number;\n}"
        },
        {
          "name": "SyncPage",
          "slug": "sync-page",
          "kind": "interface",
          "declaration": "interface SyncPage {\n    /**\n     * An idempotent UPSERT SET keyed by `id` (or by `clientId` for own writes), never a delta append.\n     * The same workout may legitimately appear in two consecutive results. Health Connect emits an\n     * upsertion change even for a write that changed nothing, so the presence of a workout here is not\n     * a claim that it changed.\n     */\n    readonly added: readonly Workout[];\n    /** May contain ids this app never held. `remove(unknown id)` MUST be a no-op. */\n    readonly removed: readonly RemovedWorkout[];\n    /**\n     * Persist this together with `added`/`removed` **IN ONE TRANSACTION**. Persisting the cursor\n     * without the items loses those workouts permanently and the library cannot prevent it.\n     */\n    readonly cursor: WorkoutsSyncCursor;\n    /** `true` = call `syncWorkouts(result.cursor)` again immediately. */\n    readonly hasMore: boolean;\n}"
        },
        {
          "name": "SyncResult",
          "slug": "sync-result",
          "kind": "type",
          "declaration": "/**\n * Discriminated on `reset`, so `resetReason` is unreachable without narrowing and unforgettable when\n * present. `const b: boolean = result.reset` still compiles, so this stays read-compatible with the\n * mission's `reset: boolean` sketch at every call site.\n */\ntype SyncResult = (SyncPage & {\n    readonly reset: false;\n}) | (SyncPage & {\n    readonly added: readonly [\n    ];\n    readonly removed: readonly [\n    ];\n    readonly hasMore: false;\n    readonly reset: true;\n    readonly resetReason: CursorResetReason;\n});",
          "sourceDocumentation": "Discriminated on `reset`, so `resetReason` is unreachable without narrowing and unforgettable when\npresent. `const b: boolean = result.reset` still compiles, so this stays read-compatible with the\nmission's `reset: boolean` sketch at every call site."
        },
        {
          "name": "syncWorkouts",
          "slug": "sync-workouts",
          "kind": "constant",
          "declaration": "syncWorkouts: (cursor: WorkoutsSyncCursor | null) => Promise<SyncResult>"
        },
        {
          "name": "TimeWindow",
          "slug": "time-window",
          "kind": "interface",
          "declaration": "/**\n * Epoch-ms half-open window. Everywhere in this library it means: the record's **START instant** in\n * `[fromMs, toMs)`. There is no overlap variant and no local-day variant — day bucketing is your\n * job, done afterwards from `utcOffsetMin`.\n *\n * Both bounds are validated against `EPOCH_MS_FLOOR`: a value in `(0, 1e11)` is rejected with\n * `invalidArgument` because it is a seconds timestamp in a milliseconds field.\n */\ninterface TimeWindow {\n    /** Inclusive. Epoch MILLISECONDS, integer. */\n    readonly fromMs: number;\n    /** EXCLUSIVE. Epoch MILLISECONDS, integer, > `fromMs`. */\n    readonly toMs: number;\n}",
          "sourceDocumentation": "Epoch-ms half-open window. Everywhere in this library it means: the record's **START instant** in\n`[fromMs, toMs)`. There is no overlap variant and no local-day variant — day bucketing is your\njob, done afterwards from `utcOffsetMin`.\n\nBoth bounds are validated against `EPOCH_MS_FLOOR`: a value in `(0, 1e11)` is rejected with\n`invalidArgument` because it is a seconds timestamp in a milliseconds field."
        },
        {
          "name": "unpopulatedWorkoutMetrics",
          "slug": "unpopulated-workout-metrics",
          "kind": "function",
          "declaration": "/**\n * READ-side answer to \"why is this field `undefined` on every workout?\", without a device.\n *\n * Returns the `Workout` FIELD names — not scope names — whose gating read scope is `'denied'` or\n * `'undetermined'`. `'undetermined'` is the load-bearing half: it is the exact shape of the\n * `read: ['workouts']` trap (never asked), and an implementation that only looked for `'denied'`\n * would miss the trap entirely.\n *\n * It returns ONLY what we positively know:\n * - `'unknown'` NEVER produces an accusation, so on iOS this always returns `[]`.\n * - On an unavailable / updateRequired platform it returns `[]`.\n */\ndeclare function unpopulatedWorkoutMetrics(state: AuthorizationState): readonly WorkoutMetricField[];",
          "sourceDocumentation": "READ-side answer to \"why is this field `undefined` on every workout?\", without a device.\n\nReturns the `Workout` FIELD names — not scope names — whose gating read scope is `'denied'` or\n`'undetermined'`. `'undetermined'` is the load-bearing half: it is the exact shape of the\n`read: ['workouts']` trap (never asked), and an implementation that only looked for `'denied'`\nwould miss the trap entirely.\n\nIt returns ONLY what we positively know:\n- `'unknown'` NEVER produces an accusation, so on iOS this always returns `[]`.\n- On an unavailable / updateRequired platform it returns `[]`."
        },
        {
          "name": "WindowDto",
          "slug": "window-dto",
          "kind": "interface",
          "declaration": "interface WindowDto {\n    readonly fromMs: number;\n    readonly toMs: number;\n}"
        },
        {
          "name": "Workout",
          "slug": "workout",
          "kind": "type",
          "declaration": "/** Discriminated by `platform` — `if (w.platform === 'ios')` narrows `platformData` with ZERO casts. */\ntype Workout = IosWorkout | AndroidWorkout;",
          "sourceDocumentation": "Discriminated by `platform` — `if (w.platform === 'ios')` narrows `platformData` with ZERO casts."
        },
        {
          "name": "WORKOUT_KINDS",
          "slug": "workout-kinds",
          "kind": "constant",
          "declaration": "WORKOUT_KINDS: readonly [\n    \"running\",\n    \"walking\",\n    \"hiking\",\n    \"cycling\",\n    \"swimming\",\n    \"rowing\",\n    \"strength\",\n    \"wheelchair\",\n    \"other\"\n]",
          "sourceDocumentation": "D11, as amended by the product owner on 2026-08-22 (the original D11 named five members:\nrunning | walking | hiking | cycling | other).\n\nAnything the platform reports that is not one of the eight named kinds collapses to `'other'`;\nthe raw value survives under `platformData` (iOS only — see below). CLOSED for 1.x; a member may\nstill be added in 0.x as a MINOR, which breaks exhaustive switches.\n\nEvery member maps to a NON-DEPRECATED constant on BOTH platforms — the full table with raw\nintegers lives in `activity.ts` and is pinned by `tests/fixtures/activity-vectors.json`.\n\n⚠ `indoor` is STORED on iOS and DERIVED on Android. Health Connect's `ExerciseSessionRecord` has\n  no indoor field, so `indoor` survives an Android round-trip only for the four kinds that have a\n  constant PAIR (`running`, `cycling`, `swimming`, `rowing`). For `walking`, `hiking`, `strength`,\n  `wheelchair` and `other` it is written nowhere on Android and reads back `undefined`.\n⚠ The escape hatch is asymmetric. On iOS an unmapped `HKWorkoutActivityType` arrives intact in\n  `platformData.ios.activityTypeRaw`. On Android the value is already destroyed before it reaches\n  us — Health Connect collapses any unmapped int to 0 on BOTH the read and the write IPC path —\n  so `platformData.android.exerciseType` reads 0 and `'other'` is all the information that exists."
        },
        {
          "name": "WORKOUT_METRIC_SCOPES",
          "slug": "workout-metric-scopes",
          "kind": "constant",
          "declaration": "WORKOUT_METRIC_SCOPES: {\n    readonly distanceM: \"distance\";\n    readonly activeEnergyKcal: \"activeEnergy\";\n    readonly elevationGainM: \"elevation\";\n    readonly heartRate: \"heartRate\";\n    readonly steps: \"steps\";\n}",
          "sourceDocumentation": "The single table that ties every optional `Workout` metric to the ONE scope that gates it. The\n`satisfies` clause is a live guard, not decoration: a key that is not a `WorkoutBase` field and a\nvalue that is not a `Scope` are BOTH compile errors, so this table cannot drift away from\n`Workout` or from `Scope`.\n\n`routeState` is deliberately absent: it is per-workout and recomputed on every read, so it can\nnever be answered from an `AuthorizationState` snapshot."
        },
        {
          "name": "WORKOUT_TOTALS_SCOPES",
          "slug": "workout-totals-scopes",
          "kind": "constant",
          "declaration": "WORKOUT_TOTALS_SCOPES: readonly [\n    \"workouts\",\n    \"distance\",\n    \"activeEnergy\",\n    \"elevation\"\n]",
          "sourceDocumentation": "The coarse form of owner decision ②, in ONE token: the session plus every total the common\n`Workout` model carries. Spread it in place —\n\n```ts\nawait requestAuthorization({ read: [...WORKOUT_TOTALS_SCOPES, 'routes'] });\n```\n\nThis is the recipe to copy unless you have a reason not to. Naming the members individually is\nthe NARROW case and should be a deliberate act.\n\nIt DELIBERATELY EXCLUDES `'routes'`: a convenience constant must never hide a non-requestable\nscope inside itself, or it lies about what the permission dialog will show.\n\n⚠ Spread it; do not `.concat()` it (TS2769), and do not park it in an un-annotated intermediate\n  (`string[]`, then TS2322 at the use site). Add `satisfies readonly Scope[]` if you need a\n  variable."
        },
        {
          "name": "WorkoutBase",
          "slug": "workout-base",
          "kind": "interface",
          "declaration": "/** The fields both platforms share. Never used directly — see `Workout`. */\ninterface WorkoutBase {\n    /** The PLATFORM id: HKWorkout.uuid / ExerciseSessionRecord.metadata.id. Pass this to `getRoute`. */\n    readonly id: string;\n    /**\n     * The id the WRITING app used (HKMetadataKeySyncIdentifier / clientRecordId), when present.\n     * It is the STABLE upsert key for own writes: on iOS `id` changes when a workout is replaced while\n     * `clientId` does not. It is visible cross-app, so never put anything sensitive in it.\n     */\n    readonly clientId?: string | undefined;\n    /** True when this app wrote it. Nothing is filtered on your behalf — the sync loop needs to see\n     *  its own echo to reconcile native ids. Filter on this yourself. */\n    readonly isOwn: boolean;\n    readonly kind: WorkoutKind;\n    /**\n     * `undefined` when the platform cannot tell. iOS raw `locationType` 3 means \"outdoor OR unknown\",\n     * so an absent HKIndoorWorkout metadata key leaves this undefined rather than `false`.\n     *\n     * ⚠ **Platform-asymmetric, by construction.** On iOS this is STORED, so it round-trips for every\n     *   `kind`. On Android it is DERIVED from `exerciseType` alone, so it survives only for the four\n     *   kinds with a constant pair (`running`, `cycling`, `swimming`, `rowing`) and reads back\n     *   `undefined` for the other five. On those four paired kinds the opposite rounding happens:\n     *   `indoor: undefined` normalizes to `false` after an Android round-trip.\n     */\n    readonly indoor?: boolean | undefined;\n    readonly startMs: number;\n    readonly endMs: number;\n    /**\n     * Active seconds. iOS: the store's own `duration`, which honours the writer's explicit value and\n     * can differ from `endMs - startMs`. Android: `(endMs - startMs)` minus every PAUSE segment.\n     */\n    readonly activeDurationS: number;\n    /** Minutes east of UTC at the workout's start. Use this for day bucketing. */\n    readonly utcOffsetMin?: number | undefined;\n    readonly source: WorkoutSource;\n    /**\n     * Metres. `undefined` means UNKNOWN — never 0.\n     * ⚠ Populated only when the `'distance'` read scope is granted. With `read: ['workouts']` alone\n     *   this field is `undefined` on EVERY workout. `unpopulatedWorkoutMetrics(state)` answers \"which\n     *   fields can never be filled with the permissions I hold\" without a device.\n     */\n    readonly distanceM?: number | undefined;\n    readonly distanceProvenance?: MetricProvenance | undefined;\n    /**\n     * Active kcal, never total/BMR-inclusive. `undefined` means UNKNOWN — never 0.\n     * ⚠ Populated only when the `'activeEnergy'` read scope is granted.\n     */\n    readonly activeEnergyKcal?: number | undefined;\n    readonly activeEnergyProvenance?: MetricProvenance | undefined;\n    /**\n     * Metres of cumulative ascent. ⚠ Populated only when the `'elevation'` read scope is granted.\n     * On iOS that scope maps to the EMPTY HealthKit set and therefore aliases `'workouts'`.\n     */\n    readonly elevationGainM?: number | undefined;\n    /** ⚠ Populated only when the `'heartRate'` read scope is granted. */\n    readonly heartRate?: WorkoutHeartRateSummary | undefined;\n    /** ⚠ Populated only when the `'steps'` read scope is granted. */\n    readonly steps?: number | undefined;\n    /** Explicit pause segments only. */\n    readonly pauses: readonly Pause[];\n    readonly laps: readonly Lap[];\n    readonly routeState: RouteState;\n    readonly lastModifiedMs?: number | undefined;\n}",
          "sourceDocumentation": "The fields both platforms share. Never used directly — see `Workout`."
        },
        {
          "name": "WorkoutDto",
          "slug": "workout-dto",
          "kind": "interface",
          "declaration": "/**\n * 한 워크아웃의 평면 DTO.\n *\n * ⚠ `kind`가 없다 — **활동 매핑은 `./core`가 한다.** 네이티브는 raw 정수(`activityTypeRaw`)만\n *   보내고 `activity.ts`가 `WorkoutKind`로 접는다. 그래야 매핑표가 Node에서 fuzz된다.\n * ⚠ `indoor`도 마찬가지로 iOS에서만 채워진다(메타데이터 사다리의 결과). Android는 `null`이고\n *   `./core`가 `exerciseType`에서 파생한다.\n */\ninterface WorkoutDto {\n    readonly platform: WorkoutsPlatform;\n    readonly id: string;\n    readonly clientId?: string | null | undefined;\n    readonly isOwn: boolean;\n    /** HKWorkoutActivityType (iOS) / ExerciseSessionRecord.exerciseType (Android). */\n    readonly activityTypeRaw: number;\n    /** iOS only. Android sends `null` — `./core` derives it from `activityTypeRaw`. */\n    readonly indoor?: boolean | null | undefined;\n    readonly startMs: number;\n    readonly endMs: number;\n    readonly activeDurationS: number;\n    readonly utcOffsetMin?: number | null | undefined;\n    readonly source: SourceDto;\n    readonly distanceM?: number | null | undefined;\n    readonly distanceProvenance?: MetricProvenance | null | undefined;\n    readonly activeEnergyKcal?: number | null | undefined;\n    readonly activeEnergyProvenance?: MetricProvenance | null | undefined;\n    readonly elevationGainM?: number | null | undefined;\n    readonly heartRate?: HeartRateSummaryDto | null | undefined;\n    readonly steps?: number | null | undefined;\n    readonly pauses: readonly PauseDto[];\n    readonly laps: readonly LapDto[];\n    readonly routeState: RouteState;\n    readonly lastModifiedMs?: number | null | undefined;\n    /** Exactly one of these is present, matching `platform`. */\n    readonly ios?: IosWorkoutData | null | undefined;\n    readonly android?: AndroidWorkoutData | null | undefined;\n}",
          "sourceDocumentation": "한 워크아웃의 평면 DTO.\n\n⚠ `kind`가 없다 — **활동 매핑은 `./core`가 한다.** 네이티브는 raw 정수(`activityTypeRaw`)만\n  보내고 `activity.ts`가 `WorkoutKind`로 접는다. 그래야 매핑표가 Node에서 fuzz된다.\n⚠ `indoor`도 마찬가지로 iOS에서만 채워진다(메타데이터 사다리의 결과). Android는 `null`이고\n  `./core`가 `exerciseType`에서 파생한다."
        },
        {
          "name": "WorkoutHeartRateSummary",
          "slug": "workout-heart-rate-summary",
          "kind": "interface",
          "declaration": "interface WorkoutHeartRateSummary {\n    readonly avgBpm?: number | undefined;\n    readonly minBpm?: number | undefined;\n    readonly maxBpm?: number | undefined;\n}"
        },
        {
          "name": "WorkoutKind",
          "slug": "workout-kind",
          "kind": "type",
          "declaration": "type WorkoutKind = (typeof WORKOUT_KINDS)[number];"
        },
        {
          "name": "WorkoutMetricField",
          "slug": "workout-metric-field",
          "kind": "type",
          "declaration": "type WorkoutMetricField = keyof typeof WORKOUT_METRIC_SCOPES;"
        },
        {
          "name": "WorkoutPage",
          "slug": "workout-page",
          "kind": "interface",
          "declaration": "interface WorkoutPage {\n    /** DESCENDING by start instant — most recent first. The order is part of the contract, because it\n     *  is what makes a multi-launch backfill resumable. */\n    readonly items: readonly Workout[];\n    /** Absent = last page. */\n    readonly nextPageToken?: WorkoutsPageToken | undefined;\n}"
        },
        {
          "name": "WorkoutPageDto",
          "slug": "workout-page-dto",
          "kind": "interface",
          "declaration": "interface WorkoutPageDto {\n    readonly items: readonly WorkoutDto[];\n    /** The platform's own opaque token. `./core` wraps it in the `gjp1.` page-token magic. */\n    readonly nextPageToken?: string | null | undefined;\n}"
        },
        {
          "name": "WorkoutRef",
          "slug": "workout-ref",
          "kind": "type",
          "declaration": "/**\n * Identify a workout without ambiguity. Two id spaces exist and both are UUIDs, so no runtime\n * heuristic can tell them apart — the type makes the choice unmissable.\n * The `?: never` members are load-bearing: a bare `{a} | {b}` union ACCEPTS both keys together.\n */\ntype WorkoutRef = \n/** The platform id from `Workout.id`. */\n{\n    readonly nativeId: string;\n    readonly clientId?: never;\n}\n/** Your own `WorkoutWrite.id`. */\n | {\n    readonly clientId: string;\n    readonly nativeId?: never;\n};",
          "sourceDocumentation": "Identify a workout without ambiguity. Two id spaces exist and both are UUIDs, so no runtime\nheuristic can tell them apart — the type makes the choice unmissable.\nThe `?: never` members are load-bearing: a bare `{a} | {b}` union ACCEPTS both keys together."
        },
        {
          "name": "workouts",
          "slug": "workouts",
          "kind": "constant",
          "declaration": "workouts: WorkoutsApi",
          "sourceDocumentation": "The same twelve functions as one object, so an app can inject `WorkoutsApi` and substitute\n`createFakeWorkouts().api` in tests. It is not a second implementation — the twelve named exports\nbelow are literally this object's destructured properties."
        },
        {
          "name": "WORKOUTS_ERROR_CODES",
          "slug": "workouts-error-codes",
          "kind": "constant",
          "declaration": "WORKOUTS_ERROR_CODES: readonly [\n    \"unavailable\",\n    \"updateRequired\",\n    \"notAuthorized\",\n    \"consentRequired\",\n    \"historyRequired\",\n    \"rateLimited\",\n    \"busy\",\n    \"invalidArgument\",\n    \"routeTooLarge\",\n    \"staleVersion\",\n    \"storeLocked\",\n    \"cancelled\",\n    \"io\",\n    \"internal\"\n]"
        },
        {
          "name": "WorkoutsApi",
          "slug": "workouts-api",
          "kind": "interface",
          "declaration": "/**\n * Every function of `.`, as one interface. `.`'s `workouts` and `./testing`'s `api` are both\n * instances of it, produced by the SAME factory.\n */\ninterface WorkoutsApi {\n    getAvailability(): Promise<Availability>;\n    requestAuthorization(request: AuthorizationRequest): Promise<AuthorizationResult>;\n    getAuthorizationState(): Promise<AuthorizationState>;\n    listWorkouts(query: ListQuery): Promise<WorkoutPage>;\n    syncWorkouts(cursor: WorkoutsSyncCursor | null): Promise<SyncResult>;\n    getRoute(workoutId: string, options?: GetRouteOptions): AsyncIterable<readonly RoutePoint[]>;\n    readHeartRate(window: TimeWindow): Promise<readonly HeartRateSample[]>;\n    readSteps(window: TimeWindow): Promise<StepTotal>;\n    saveWorkout(workout: WorkoutWrite): Promise<SaveResult>;\n    deleteWorkout(ref: WorkoutRef): Promise<DeleteResult>;\n    openSettings(): Promise<void>;\n    openStoreListing(): Promise<void>;\n}",
          "sourceDocumentation": "Every function of `.`, as one interface. `.`'s `workouts` and `./testing`'s `api` are both\ninstances of it, produced by the SAME factory."
        },
        {
          "name": "WorkoutsError",
          "slug": "workouts-error",
          "kind": "class",
          "declaration": "declare class WorkoutsError extends Error {\n    readonly code: WorkoutsErrorCode;\n    readonly retryAfterMs?: number | undefined;\n    readonly nativeMessage?: string | undefined;\n    /** 사본 인식 태그 — `isWorkoutsError`의 유일한 판정 근거다. */\n    readonly [WORKOUTS_ERROR_TAG]: true;\n    constructor(code: WorkoutsErrorCode, message: string, options?: WorkoutsErrorOptions);\n}"
        },
        {
          "name": "workoutsErrorCode",
          "slug": "workouts-error-code",
          "kind": "function",
          "declaration": "/** `null` for anything that is not one of ours. */\ndeclare function workoutsErrorCode(error: unknown): WorkoutsErrorCode | null;",
          "sourceDocumentation": "`null` for anything that is not one of ours."
        },
        {
          "name": "WorkoutsErrorCode",
          "slug": "workouts-error-code--type",
          "kind": "type",
          "declaration": "type WorkoutsErrorCode = (typeof WORKOUTS_ERROR_CODES)[number];"
        },
        {
          "name": "WorkoutsErrorOptions",
          "slug": "workouts-error-options",
          "kind": "interface",
          "declaration": "interface WorkoutsErrorOptions {\n    readonly cause?: unknown;\n    /** Only meaningful with code 'rateLimited'. */\n    readonly retryAfterMs?: number | undefined;\n    /**\n     * A short, TEMPLATE-BUILT diagnostic string from the native layer: exception class name, platform\n     * error code, and a bounded reason token. NEVER coordinates, heart rates, distances, energies,\n     * step counts, titles or notes — a source-scan guard enforces this.\n     */\n    readonly nativeMessage?: string | undefined;\n}"
        },
        {
          "name": "workoutsExceptionClassName",
          "slug": "workouts-exception-class-name",
          "kind": "function",
          "declaration": "/** `'routeTooLarge'` -> `'WorkoutsRouteTooLargeException'` (Swift와 Kotlin에서 동일한 이름). */\ndeclare function workoutsExceptionClassName(code: WorkoutsErrorCode): string;",
          "sourceDocumentation": "`'routeTooLarge'` -> `'WorkoutsRouteTooLargeException'` (Swift와 Kotlin에서 동일한 이름)."
        },
        {
          "name": "WorkoutSource",
          "slug": "workout-source",
          "kind": "interface",
          "declaration": "interface WorkoutSource {\n    /** iOS bundle identifier / Android package name. Apple Watch first-party reads as\n     *  `com.apple.health.<UUID>`. */\n    readonly id: string;\n    /** iOS only — Android's DataOrigin carries a package name and nothing else. */\n    readonly name?: string | undefined;\n    readonly version?: string | undefined;\n    readonly deviceModel?: string | undefined;\n}"
        },
        {
          "name": "WorkoutsPageToken",
          "slug": "workouts-page-token",
          "kind": "type",
          "declaration": "/** Opaque, and NOT interchangeable with a sync cursor — the two carry different magic prefixes. */\ntype WorkoutsPageToken = string;",
          "sourceDocumentation": "Opaque, and NOT interchangeable with a sync cursor — the two carry different magic prefixes."
        },
        {
          "name": "WorkoutsPlatform",
          "slug": "workouts-platform",
          "kind": "type",
          "declaration": "/** The health store a workout came from. Also the discriminator of the `Workout` union. */\ntype WorkoutsPlatform = 'ios' | 'android';",
          "sourceDocumentation": "The health store a workout came from. Also the discriminator of the `Workout` union."
        },
        {
          "name": "WorkoutsSyncCursor",
          "slug": "workouts-sync-cursor",
          "kind": "type",
          "declaration": "/** Opaque. Persist it verbatim; never parse, compare or construct one. */\ntype WorkoutsSyncCursor = string;",
          "sourceDocumentation": "Opaque. Persist it verbatim; never parse, compare or construct one."
        },
        {
          "name": "WorkoutWrite",
          "slug": "workout-write",
          "kind": "interface",
          "declaration": "/**\n * Full-state input for `saveWorkout`. There is NO partial-update path: on Android an upsert that\n * omits the route DELETES the stored route, so the only safe contract is \"send everything\".\n */\ninterface WorkoutWrite {\n    /**\n     * A stable id this app owns — the idempotency key. Becomes HKMetadataKeySyncIdentifier /\n     * Health Connect `clientRecordId`.\n     * ⚠ Other apps CAN read this value on Android. Use an opaque UUID.\n     * Must match `/^[A-Za-z0-9._:-]{1,120}$/`.\n     */\n    readonly id: string;\n    /**\n     * A safe integer >= 1, non-decreasing per `id`, that increases whenever the content changes.\n     * Derive it from your own record's `updatedAt` (epoch ms) or from an edit counter.\n     * ⚠ NEVER `Date.now()` at call time: a crash retry would write a fresh version and, on iOS, mint a\n     *   second workout object and orphan the first one's samples and route.\n     * An EQUAL version replaces the stored workout; a LOWER one throws `staleVersion` and writes nothing.\n     */\n    readonly version: number;\n    /**\n     * Nine members since owner decision ③. `'other'` is the documented lossy sink — it stores\n     * OTHER_WORKOUT(0) / `.other`(3000) and the original activity is not recoverable.\n     */\n    readonly kind: WorkoutKind;\n    /**\n     * Drives the platform activity constant on write.\n     * ⚠ On Android it is only representable for `running`, `cycling`, `swimming` and `rowing`; for\n     *   every other kind it is silently dropped and reads back `undefined`. On iOS it is written to\n     *   `HKMetadataKeyIndoorWorkout` for every kind — but only when you actually set it: leaving it\n     *   `undefined` OMITS the key rather than writing `@NO`.\n     */\n    readonly indoor?: boolean | undefined;\n    readonly startMs: number;\n    /** Must be > `startMs` and <= now. */\n    readonly endMs: number;\n    readonly utcOffsetMin?: number | undefined;\n    /** IANA zone id (e.g. `'Asia/Seoul'`). iOS metadata only; Android stores only the offset. */\n    readonly timeZoneId?: string | undefined;\n    readonly pauses?: readonly Pause[] | undefined;\n    readonly laps?: readonly Lap[] | undefined;\n    readonly distanceM?: number | undefined;\n    readonly activeEnergyKcal?: number | undefined;\n    readonly elevationGainM?: number | undefined;\n    /** Omitted from the write when <= 0 — Health Connect throws on `StepsRecord(count = 0)`. */\n    readonly steps?: number | undefined;\n    /** Samples outside 1..300 bpm or outside `[startMs, endMs)` are dropped before writing. */\n    readonly heartRate?: readonly HeartRateSample[] | undefined;\n    /**\n     * REQUIRED, and `'none'` is not the same call shape as an empty array.\n     *\n     * ⚠ This is the one place where forgetting a field DESTROYS user data: an Android upsert that\n     *   omits the route while holding the route write scope DELETES the stored route. Making the field\n     *   required turns that silent, irreversible mistake into a compile error, and `'none'` forces the\n     *   intent to be stated out loud.\n     * An empty array is `invalidArgument` — say `'none'`.\n     */\n    readonly route: readonly RoutePoint[] | 'none';\n}",
          "sourceDocumentation": "Full-state input for `saveWorkout`. There is NO partial-update path: on Android an upsert that\nomits the route DELETES the stored route, so the only safe contract is \"send everything\"."
        },
        {
          "name": "WorkoutWriteDto",
          "slug": "workout-write-dto",
          "kind": "interface",
          "declaration": "interface WorkoutWriteDto {\n    readonly clientId: string;\n    readonly version: number;\n    /** 이미 매핑된 플랫폼 정수 — `activity.ts`가 계산한다. */\n    readonly activityTypeRaw: number;\n    /** `undefined`면 키를 아예 쓰지 않는다(iOS). Android는 정수 선택에 이미 반영돼 있다. */\n    readonly indoor?: boolean | undefined;\n    readonly startMs: number;\n    readonly endMs: number;\n    readonly utcOffsetMin?: number | undefined;\n    readonly timeZoneId?: string | undefined;\n    readonly pauses: readonly PauseDto[];\n    readonly laps: readonly LapDto[];\n    readonly distanceM?: number | undefined;\n    readonly activeEnergyKcal?: number | undefined;\n    readonly elevationGainM?: number | undefined;\n    /** `<= 0`이면 `./core`가 이미 제거했다 — Health Connect가 0-count StepsRecord에 throw한다. */\n    readonly steps?: number | undefined;\n    readonly heartRate: readonly HeartRateDto[];\n    /** 이미 위생을 통과한 점들. 빈 배열은 \"route 없음\"을 뜻한다. */\n    readonly route: readonly RoutePointDto[];\n}"
        }
      ]
    },
    {
      "subpath": "./core",
      "id": "core",
      "declarationTarget": "./dist/core.d.mts",
      "symbols": [
        {
          "name": "activeDurationS",
          "slug": "active-duration-s",
          "kind": "function",
          "declaration": "/**\n * `(endMs - startMs - Σ pause overlap) / 1000`, clamped at 0. Overlapping pauses are merged, so\n * double-counting is not expressible.\n *\n * ⚠ This is how ANDROID derives it. iOS reports the store's own `duration`, which honours the\n *   writer's explicit argument and can differ; `Workout.activeDurationS` carries whichever the\n *   platform gave.\n */\ndeclare function activeDurationS(startMs: number, endMs: number, pauses: readonly Pause[]): number;",
          "sourceDocumentation": "`(endMs - startMs - Σ pause overlap) / 1000`, clamped at 0. Overlapping pauses are merged, so\ndouble-counting is not expressible.\n\n⚠ This is how ANDROID derives it. iOS reports the store's own `duration`, which honours the\n  writer's explicit argument and can differ; `Workout.activeDurationS` carries whichever the\n  platform gave."
        },
        {
          "name": "ANDROID_HISTORY_PERMISSION",
          "slug": "android-history-permission",
          "kind": "constant",
          "declaration": "ANDROID_HISTORY_PERMISSION = \"android.permission.health.READ_HEALTH_DATA_HISTORY\"",
          "sourceDocumentation": "D10. 매니페스트와 런타임 요청 양쪽에 필요한 history 권한."
        },
        {
          "name": "ANDROID_HISTORY_WINDOW_MS",
          "slug": "android-history-window-ms",
          "kind": "constant",
          "declaration": "ANDROID_HISTORY_WINDOW_MS = 2592000000",
          "sourceDocumentation": "30 days in ms — Health Connect's history wall without READ_HEALTH_DATA_HISTORY (D10)."
        },
        {
          "name": "ANDROID_READ_PERMISSIONS",
          "slug": "android-read-permissions",
          "kind": "constant",
          "declaration": "ANDROID_READ_PERMISSIONS: Readonly<Record<Scope, string>>",
          "sourceDocumentation": "Health Connect READ 권한. `routes`는 **매니페스트 전용**이며 런타임 요청 집합에 넣지 않는다(f110)."
        },
        {
          "name": "ANDROID_WRITE_PERMISSIONS",
          "slug": "android-write-permissions",
          "kind": "constant",
          "declaration": "ANDROID_WRITE_PERMISSIONS: Readonly<Record<Scope, string>>",
          "sourceDocumentation": "Health Connect WRITE 권한. `routes`만 단수형(`WRITE_EXERCISE_ROUTE`)이다."
        },
        {
          "name": "androidExerciseTypeFromKind",
          "slug": "android-exercise-type-from-kind",
          "kind": "function",
          "declaration": "/**\n * WRITE direction, Android. `indoor` selects between the constant PAIR where one exists\n * (running / cycling / swimming / rowing) and is otherwise silently dropped — Health Connect has\n * nowhere to store it.\n * ⚠ `kind: 'other'` writes OTHER_WORKOUT(0) and is NOT recoverable on read.\n */\ndeclare function androidExerciseTypeFromKind(kind: WorkoutKind, indoor?: boolean | undefined): number;",
          "sourceDocumentation": "WRITE direction, Android. `indoor` selects between the constant PAIR where one exists\n(running / cycling / swimming / rowing) and is otherwise silently dropped — Health Connect has\nnowhere to store it.\n⚠ `kind: 'other'` writes OTHER_WORKOUT(0) and is NOT recoverable on read."
        },
        {
          "name": "androidRequestPermissions",
          "slug": "android-request-permissions",
          "kind": "function",
          "declaration": "/**\n * Android: scope -> `android.permission.health.*`, 방향별로.\n * `'routes'`의 READ는 **절대 포함하지 않는다** — 플랫폼이 조용히 걸러내고 설정 화면이나 per-route\n * 다이얼로그에서만 부여된다(f110, f121). `history`는 READ 쪽에 실린다(D10).\n */\ndeclare function androidRequestPermissions(request: AuthorizationRequest): DirectedPermissions;",
          "sourceDocumentation": "Android: scope -> `android.permission.health.*`, 방향별로.\n`'routes'`의 READ는 **절대 포함하지 않는다** — 플랫폼이 조용히 걸러내고 설정 화면이나 per-route\n다이얼로그에서만 부여된다(f110, f121). `history`는 READ 쪽에 실린다(D10)."
        },
        {
          "name": "androidRuntimeRequestPermissions",
          "slug": "android-runtime-request-permissions",
          "kind": "function",
          "declaration": "/**\n * 런타임 요청 집합. `'routes'`의 READ는 **절대 포함하지 않는다** — 플랫폼이 조용히 걸러내고\n * 설정 화면이나 per-route 다이얼로그에서만 부여된다(f110, f121).\n */\ndeclare function androidRuntimeRequestPermissions(request: AuthorizationRequest): readonly string[];",
          "sourceDocumentation": "런타임 요청 집합. `'routes'`의 READ는 **절대 포함하지 않는다** — 플랫폼이 조용히 걸러내고\n설정 화면이나 per-route 다이얼로그에서만 부여된다(f110, f121)."
        },
        {
          "name": "AndroidWorkout",
          "slug": "android-workout",
          "kind": "interface",
          "declaration": "interface AndroidWorkout extends WorkoutBase {\n    readonly platform: 'android';\n    readonly platformData: AndroidWorkoutData;\n}"
        },
        {
          "name": "AndroidWorkoutData",
          "slug": "android-workout-data",
          "kind": "interface",
          "declaration": "/** Raw Android values the common model deliberately does not model. */\ninterface AndroidWorkoutData {\n    /** Raw ExerciseSessionRecord.exerciseType. */\n    readonly exerciseType: number;\n    readonly packageName: string;\n    readonly recordingMethod: number;\n    readonly deviceType?: number | undefined;\n    /**\n     * The writer's own client record id. Foreign apps' values ARE visible here — treat it as PUBLIC\n     * data, never as a private namespace.\n     */\n    readonly clientRecordId?: string | undefined;\n    readonly clientRecordVersion?: number | undefined;\n    readonly endUtcOffsetMin?: number | undefined;\n    /** Foreign-app authored text. This library never writes a title or notes. */\n    readonly title?: string | undefined;\n    readonly notes?: string | undefined;\n    /** Every segment, including REST (44), which `pauses` deliberately excludes. PAUSE is 39. */\n    readonly segments: readonly {\n        readonly type: number;\n        readonly startMs: number;\n        readonly endMs: number;\n    }[];\n}",
          "sourceDocumentation": "Raw Android values the common model deliberately does not model."
        },
        {
          "name": "assertNeverWorkoutsCode",
          "slug": "assert-never-workouts-code",
          "kind": "function",
          "declaration": "/**\n * Call it from a `switch` default so a future code becomes a compile error for you.\n * ⚠ This is only honest because the code union is CLOSED for 1.x: adding a code is a major.\n */\ndeclare function assertNeverWorkoutsCode(code: never): never;",
          "sourceDocumentation": "Call it from a `switch` default so a future code becomes a compile error for you.\n⚠ This is only honest because the code union is CLOSED for 1.x: adding a code is a major."
        },
        {
          "name": "authorizationAdvice",
          "slug": "authorization-advice",
          "kind": "function",
          "declaration": "/**\n * Our opinion about what a settings screen should render, as a PURE function rather than a field on\n * `AuthorizationState`: if the platform's permission UI changes, an opinion baked into the contract\n * would be wrong in a way the raw facts would not have been. Adopt it or re-implement it.\n *\n * The one rule that matters: `'unknown'` NEVER produces `'openSettings'`. Every iOS read scope is\n * permanently `'unknown'`, so treating it as a problem would show every iOS user \"go check\n * Settings\" forever.\n */\ndeclare function authorizationAdvice(facts: AuthorizationFacts): AuthorizationAdvice;",
          "sourceDocumentation": "Our opinion about what a settings screen should render, as a PURE function rather than a field on\n`AuthorizationState`: if the platform's permission UI changes, an opinion baked into the contract\nwould be wrong in a way the raw facts would not have been. Adopt it or re-implement it.\n\nThe one rule that matters: `'unknown'` NEVER produces `'openSettings'`. Every iOS read scope is\npermanently `'unknown'`, so treating it as a problem would show every iOS user \"go check\nSettings\" forever."
        },
        {
          "name": "AuthorizationAdvice",
          "slug": "authorization-advice--type",
          "kind": "type",
          "declaration": "/** What a settings screen should do next. */\ntype AuthorizationAdvice = 'ready' | 'requestable' | 'openSettings' | 'openStoreListing' | 'unsupported';",
          "sourceDocumentation": "What a settings screen should do next."
        },
        {
          "name": "AuthorizationDerivationFacts",
          "slug": "authorization-derivation-facts",
          "kind": "interface",
          "declaration": "/** 스냅샷에서 상태를 도출할 때 필요한 부가 사실. 전부 optional이며 없으면 보수적으로 판정한다. */\ninterface AuthorizationDerivationFacts {\n    /**\n     * 방금 끝난 **결론적인** 요청에서 사용자가 실제로 거부한 플랫폼 문자열. `before`/`after` 비교의\n     * 결과이며(f120이 강제하는 유일한 정직한 판정 근거), 요청이 `conclusive: false`였다면 **비어\n     * 있어야 한다** — 그때 scope 상태는 불변이다.\n     */\n    readonly denied?: readonly string[] | undefined;\n}",
          "sourceDocumentation": "스냅샷에서 상태를 도출할 때 필요한 부가 사실. 전부 optional이며 없으면 보수적으로 판정한다."
        },
        {
          "name": "AuthorizationFacts",
          "slug": "authorization-facts",
          "kind": "interface",
          "declaration": "/** Input for the pure derivation, so a second consumer can re-derive it from facts it stored earlier. */\ninterface AuthorizationFacts {\n    readonly state: AuthorizationState;\n    /** The scopes THIS screen actually needs — may be narrower than everything the build declares. */\n    readonly requiredRead: readonly Scope[];\n    readonly requiredWrite?: readonly Scope[] | undefined;\n    readonly requiresHistory?: boolean | undefined;\n}",
          "sourceDocumentation": "Input for the pure derivation, so a second consumer can re-derive it from facts it stored earlier."
        },
        {
          "name": "AuthorizationRequest",
          "slug": "authorization-request",
          "kind": "interface",
          "declaration": "interface AuthorizationRequest {\n    /**\n     * Coarse form — one token, and the recipe to copy:\n     * `read: [...WORKOUT_TOTALS_SCOPES, 'routes']`.\n     * Fine form — name the members: `read: ['workouts', 'heartRate']`.\n     *\n     * ⚠ A metric scope without `'workouts'` is `invalidArgument`. `read: ['distance']` alone is a\n     *   100 % mistake — no API in this library reads distance except through a workout — so `./core`\n     *   rejects it before any platform call.\n     */\n    readonly read: readonly Scope[];\n    readonly write?: readonly Scope[] | undefined;\n    /**\n     * D10, opt-in. Android only. Without it, reads are walled to the last 30 days and a wider window\n     * throws `historyRequired`. It ALSO needs the config-plugin `history: true` prop; requesting it\n     * without the manifest entry throws `invalidArgument` naming the missing prop.\n     */\n    readonly history?: boolean | undefined;\n}"
        },
        {
          "name": "AuthorizationResult",
          "slug": "authorization-result",
          "kind": "type",
          "declaration": "/**\n * `requestAuthorization`'s result: the state afterwards, plus whether we can attribute it to the\n * user. `conclusive: false` means the OS returned an answer we cannot attribute — on Android,\n * bouncing off Health Connect's first-run onboarding with \"Go back\" returns an EMPTY permission set\n * after ~20 s, byte-identical to denying everything. Treat it as \"ask again later\", NEVER as denial.\n */\ntype AuthorizationResult = AuthorizationState & {\n    readonly conclusive: boolean;\n};",
          "sourceDocumentation": "`requestAuthorization`'s result: the state afterwards, plus whether we can attribute it to the\nuser. `conclusive: false` means the OS returned an answer we cannot attribute — on Android,\nbouncing off Health Connect's first-run onboarding with \"Go back\" returns an EMPTY permission set\nafter ~20 s, byte-identical to denying everything. Treat it as \"ask again later\", NEVER as denial."
        },
        {
          "name": "AuthorizationSnapshotDto",
          "slug": "authorization-snapshot-dto",
          "kind": "interface",
          "declaration": "/**\n * 인가 스냅샷. **판정은 하지 않는다** — 원시 사실만 넘긴다.\n * iOS: `authorizationStatus`(공유) + `statusForAuthorizationRequest`(시트 여부).\n * Android: `getGrantedPermissions()` + `processImportance()` + `declaredPermissions()`.\n */\ninterface AuthorizationSnapshotDto {\n    readonly platform: WorkoutsPlatform;\n    readonly availability: AvailabilityDto;\n    /** Android: granted permission strings. iOS: share-authorized HK type identifiers. */\n    readonly granted: readonly string[];\n    /**\n     * iOS only, and the reason `write.*` can say `'denied'` rather than a permanent `'undetermined'`:\n     * `HKHealthStore.authorizationStatus(for:)` per DECLARED type identifier, already reduced to our\n     * vocabulary (`sharingAuthorized` -> `'granted'`, `sharingDenied` -> `'denied'`,\n     * `notDetermined` -> `'undetermined'`). It is a SHARE-side fact only — HealthKit never reports a\n     * read status, which is exactly why every iOS read scope is permanently `'unknown'`.\n     * `null` on Android, where the direction is encoded in the permission string and `granted` is\n     * already the whole truth.\n     */\n    readonly statuses?: Readonly<Record<string, 'granted' | 'denied' | 'undetermined'>> | null | undefined;\n    /** Manifest / Info.plist 선언 집합. 선언되지 않은 scope 요청은 `invalidArgument`가 된다. */\n    readonly declared: readonly string[];\n    /** iOS only: a sheet would still appear for at least one requested type. */\n    readonly wouldPrompt: boolean;\n    /** Android only: the process is at IMPORTANCE_FOREGROUND (a hard precondition for foreign routes). */\n    readonly foreground: boolean;\n    /** AOSP `getExerciseRouteReadAccessType`, already reduced to our vocabulary. iOS: always `'all'`. */\n    readonly routeAccess: RouteAccess;\n    /** Android READ_HEALTH_DATA_HISTORY. `null` on iOS — that platform has no wall. */\n    readonly history: boolean | null;\n}",
          "sourceDocumentation": "인가 스냅샷. **판정은 하지 않는다** — 원시 사실만 넘긴다.\niOS: `authorizationStatus`(공유) + `statusForAuthorizationRequest`(시트 여부).\nAndroid: `getGrantedPermissions()` + `processImportance()` + `declaredPermissions()`."
        },
        {
          "name": "AuthorizationState",
          "slug": "authorization-state",
          "kind": "type",
          "declaration": "/**\n * Availability and authorization fused into ONE union. Reading a scope's status on a platform that\n * has no usable health store is **unrepresentable** rather than merely discouraged.\n */\ntype AuthorizationState = {\n    readonly availability: 'unavailable';\n    readonly reason: 'platformTooOld' | 'notSupported';\n} | {\n    readonly availability: 'updateRequired';\n} | {\n    readonly availability: 'available';\n    /** Every scope is always present — no `undefined` holes to guard. */\n    readonly read: Readonly<Record<Scope, ScopeStatus>>;\n    readonly write: Readonly<Record<Scope, ScopeStatus>>;\n    /** Android READ_HEALTH_DATA_HISTORY. Always `'unknown'` on iOS — that platform has no wall,\n     *  and reporting a grant the user never gave would be a lying field. */\n    readonly history: ScopeStatus;\n    readonly routeAccess: RouteAccess;\n};",
          "sourceDocumentation": "Availability and authorization fused into ONE union. Reading a scope's status on a platform that\nhas no usable health store is **unrepresentable** rather than merely discouraged."
        },
        {
          "name": "Availability",
          "slug": "availability",
          "kind": "type",
          "declaration": "type Availability = {\n    readonly status: 'available';\n} | {\n    readonly status: 'unavailable';\n    readonly reason: 'platformTooOld' | 'notSupported';\n}\n/** Android 9–13 without the Play Health Connect provider. Pair with `openStoreListing()`. */\n | {\n    readonly status: 'updateRequired';\n};"
        },
        {
          "name": "AvailabilityDto",
          "slug": "availability-dto",
          "kind": "type",
          "declaration": "type AvailabilityDto = {\n    readonly status: 'available';\n} | {\n    readonly status: 'unavailable';\n    readonly reason: 'platformTooOld' | 'notSupported';\n} | {\n    readonly status: 'updateRequired';\n};"
        },
        {
          "name": "collectRoute",
          "slug": "collect-route",
          "kind": "function",
          "declaration": "/**\n * Concatenate a `getRoute()` stream into one array. Convenience only: a 36 000-point route costs\n * ~15 MB of JS heap, which is why the stream is the default and this is the opt-in.\n */\ndeclare function collectRoute(chunks: AsyncIterable<readonly RoutePoint[]>): Promise<RoutePoint[]>;",
          "sourceDocumentation": "Concatenate a `getRoute()` stream into one array. Convenience only: a 36 000-point route costs\n~15 MB of JS heap, which is why the stream is the default and this is the opt-in."
        },
        {
          "name": "createWorkoutsApi",
          "slug": "create-workouts-api",
          "kind": "function",
          "declaration": "/**\n * The ONLY implementation of the twelve functions.\n *\n * With `native === null` every function rejects with `unavailable` except `getAvailability()`,\n * which resolves to `{ status: 'unavailable', reason: 'notSupported' }`. That is the whole\n * difference between the two `.` branches — the surfaces are structurally identical, which is what\n * `export-parity-guard` locks down.\n */\ndeclare function createWorkoutsApi(native: NativeWorkoutsModule | null, options?: CreateWorkoutsApiOptions): WorkoutsApi;",
          "sourceDocumentation": "The ONLY implementation of the twelve functions.\n\nWith `native === null` every function rejects with `unavailable` except `getAvailability()`,\nwhich resolves to `{ status: 'unavailable', reason: 'notSupported' }`. That is the whole\ndifference between the two `.` branches — the surfaces are structurally identical, which is what\n`export-parity-guard` locks down."
        },
        {
          "name": "CreateWorkoutsApiOptions",
          "slug": "create-workouts-api-options",
          "kind": "interface",
          "declaration": "/** `createWorkoutsApi`의 주입 지점. 전부 테스트가 시간·예산을 소유하기 위한 것이다. */\ninterface CreateWorkoutsApiOptions {\n    readonly now?: (() => number) | undefined;\n    /**\n     * 클라이언트측 읽기 페이서. 기본값은 **Android에서만** 켜진 `ReadBudget` 하나이고\n     * (f102의 계수는 Health Connect의 것이며 HealthKit에는 대응물이 없다), `null`이면 끈다.\n     */\n    readonly budget?: ReadBudget | null | undefined;\n    /** per-route 동의 다이얼로그 상한 (f104). 테스트가 짧은 값으로 hang을 재현한다. */\n    readonly routeConsentTimeoutMs?: number | undefined;\n}",
          "sourceDocumentation": "`createWorkoutsApi`의 주입 지점. 전부 테스트가 시간·예산을 소유하기 위한 것이다."
        },
        {
          "name": "CURSOR_FORMAT_VERSION",
          "slug": "cursor-format-version",
          "kind": "constant",
          "declaration": "CURSOR_FORMAT_VERSION = 1",
          "sourceDocumentation": "OUR format version, not the platform token's."
        },
        {
          "name": "CursorInfo",
          "slug": "cursor-info",
          "kind": "interface",
          "declaration": "interface CursorInfo {\n    /** OUR format version, not the platform token's. */\n    readonly formatVersion: number;\n    readonly platform: WorkoutsPlatform;\n    readonly issuedAtMs: number;\n}"
        },
        {
          "name": "CursorResetReason",
          "slug": "cursor-reset-reason",
          "kind": "type",
          "declaration": "type CursorResetReason = \n/** `cursor === null` — a fresh start. */\n'noCursor'\n/** Bad magic / bad base64url / bad JSON / failed shape validation. */\n | 'malformed'\n/** Magic ok, format version not in `READABLE_CURSOR_VERSIONS`. */\n | 'formatUnsupported'\n/** Minted on the other platform (server-synced cursor, device switch, restore). */\n | 'platformMismatch'\n/** Android: `ChangesResponse.changesTokenExpired === true` (30-day idle). */\n | 'expired'\n/** The granted-scope fingerprint differs from the one baked into the cursor. */\n | 'scopesChanged';"
        },
        {
          "name": "DeleteRefDto",
          "slug": "delete-ref-dto",
          "kind": "interface",
          "declaration": "interface DeleteRefDto {\n    readonly nativeId?: string | null | undefined;\n    readonly clientId?: string | null | undefined;\n}"
        },
        {
          "name": "DeleteResult",
          "slug": "delete-result",
          "kind": "interface",
          "declaration": "interface DeleteResult {\n    /** `false` for an id that was not there. Deleting something absent is never an error. */\n    readonly deleted: boolean;\n}"
        },
        {
          "name": "deniedFromOutcome",
          "slug": "denied-from-outcome",
          "kind": "function",
          "declaration": "/**\n * f120's rule, as a function: a request is only evidence of DENIAL when the platform actually\n * answered. An empty returned set after the Android onboarding \"Go back\" is byte-identical to\n * denying everything, so it must never flip a scope to `'denied'`.\n *\n * Returns the permission strings we asked for and did not get back, or `[]` when the outcome was\n * inconclusive.\n */\ndeclare function deniedFromOutcome(requested: readonly string[], outcome: PermissionOutcomeDto): readonly string[];",
          "sourceDocumentation": "f120's rule, as a function: a request is only evidence of DENIAL when the platform actually\nanswered. An empty returned set after the Android onboarding \"Go back\" is byte-identical to\ndenying everything, so it must never flip a scope to `'denied'`.\n\nReturns the permission strings we asked for and did not get back, or `[]` when the outcome was\ninconclusive."
        },
        {
          "name": "deriveAuthorizationState",
          "slug": "derive-authorization-state",
          "kind": "function",
          "declaration": "/**\n * `AuthorizationSnapshotDto` -> `AuthorizationState`. The ONE place the platform's raw facts become\n * our vocabulary (design §8.8 + the iOS \"read is permanently `unknown`\" rule + the before/after\n * comparison that f120 makes the only honest source of `'denied'`).\n *\n * Every scope is always present in both records — there are no `undefined` holes to guard.\n */\ndeclare function deriveAuthorizationState(snapshot: AuthorizationSnapshotDto, facts?: AuthorizationDerivationFacts | undefined): AuthorizationState;",
          "sourceDocumentation": "`AuthorizationSnapshotDto` -> `AuthorizationState`. The ONE place the platform's raw facts become\nour vocabulary (design §8.8 + the iOS \"read is permanently `unknown`\" rule + the before/after\ncomparison that f120 makes the only honest source of `'denied'`).\n\nEvery scope is always present in both records — there are no `undefined` holes to guard."
        },
        {
          "name": "derivePauses",
          "slug": "derive-pauses",
          "kind": "function",
          "declaration": "/**\n * Gaps of at least `minGapMs` between consecutive points, as pauses. `minGapMs` is required — the\n * threshold that separates \"a GPS fix was late\" from \"the user stopped\" is the caller's domain.\n *\n * `auto` is left `undefined`: these are DERIVED by you, not reported by the platform.\n */\ndeclare function derivePauses(points: readonly RoutePoint[], minGapMs: number): Pause[];",
          "sourceDocumentation": "Gaps of at least `minGapMs` between consecutive points, as pauses. `minGapMs` is required — the\nthreshold that separates \"a GPS fix was late\" from \"the user stopped\" is the caller's domain.\n\n`auto` is left `undefined`: these are DERIVED by you, not reported by the platform."
        },
        {
          "name": "describeCursor",
          "slug": "describe-cursor",
          "kind": "function",
          "declaration": "/**\n * Inspect a cursor for diagnostics and progress UI. Returns `null` for anything this build cannot\n * read - it NEVER throws.\n *\n * It NEVER returns the platform token (HKQueryAnchor / Health Connect changes token): an app that\n * stores its cursor on a server would otherwise be storing the platform's own token. A guard test\n * asserts no substring of the encoded token appears in the returned object.\n */\ndeclare function describeCursor(cursor: string): CursorInfo | null;",
          "sourceDocumentation": "Inspect a cursor for diagnostics and progress UI. Returns `null` for anything this build cannot\nread - it NEVER throws.\n\nIt NEVER returns the platform token (HKQueryAnchor / Health Connect changes token): an app that\nstores its cursor on a server would otherwise be storing the platform's own token. A guard test\nasserts no substring of the encoded token appears in the returned object."
        },
        {
          "name": "DirectedPermissions",
          "slug": "directed-permissions",
          "kind": "interface",
          "declaration": "/** `read`/`write` 두 방향의 플랫폼 문자열. `native-contract.ts`의 `PermissionRequestDto`와 같은 모양. */\ninterface DirectedPermissions {\n    readonly read: readonly string[];\n    readonly write: readonly string[];\n}",
          "sourceDocumentation": "`read`/`write` 두 방향의 플랫폼 문자열. `native-contract.ts`의 `PermissionRequestDto`와 같은 모양."
        },
        {
          "name": "DrainBatchDto",
          "slug": "drain-batch-dto",
          "kind": "interface",
          "declaration": "/** 드레인 한 배치. `checkpoint`는 **이 배치를 만들기 전에** 잡힌 값이다. */\ninterface DrainBatchDto {\n    readonly added: readonly WorkoutDto[];\n    readonly removed: readonly RemovedDto[];\n    readonly checkpoint: string;\n    readonly hasMore: boolean;\n    /** Android `ChangesResponse.changesTokenExpired`. */\n    readonly expired: boolean;\n}",
          "sourceDocumentation": "드레인 한 배치. `checkpoint`는 **이 배치를 만들기 전에** 잡힌 값이다."
        },
        {
          "name": "EPOCH_MS_FLOOR",
          "slug": "epoch-ms-floor",
          "kind": "constant",
          "declaration": "EPOCH_MS_FLOOR = 100000000000",
          "sourceDocumentation": "Every epoch-millisecond input in this library is validated against this floor.\n`1e11` ms is 1973-03-03. A \"now\" expressed in SECONDS is ~1.79e9, which is far below it, while no\nreal workout predates 1973 — so `0 < value < EPOCH_MS_FLOOR` is exactly the seconds-in-a-\nmilliseconds-field mistake and nothing else. It is rejected with `invalidArgument`.\nThis is the one unit accident types cannot catch and the library therefore catches at runtime."
        },
        {
          "name": "estimateAndroidRecordBytes",
          "slug": "estimate-android-record-bytes",
          "kind": "function",
          "declaration": "/**\n * Exact Health Connect record-size model, fitted with residual 0 over six failure samples:\n *   `bytes = 160 + 48·routePoints + 2·(title + notes + clientRecordId chars) + 24·(segments + laps)`\n *\n * The optional route fields are FREE — a 21 000-point route serialises to the byte-identical size\n * with and without altitude and accuracies.\n *\n * Pinned boundary (f99): a 13-char title + 13-char clientRecordId gives `bytes = 212 + 48·points`,\n * and 20 829 points is exactly 1 000 004 B — the first failing size.\n *\n * ⚠ One Mainline build's parcel encoding. A safety margin, not a contract.\n */\ndeclare function estimateAndroidRecordBytes(input: {\n    readonly routePoints: number;\n    readonly clientRecordIdLength: number;\n    readonly titleLength?: number | undefined;\n    readonly notesLength?: number | undefined;\n    readonly segments?: number | undefined;\n    readonly laps?: number | undefined;\n}): number;",
          "sourceDocumentation": "Exact Health Connect record-size model, fitted with residual 0 over six failure samples:\n  `bytes = 160 + 48·routePoints + 2·(title + notes + clientRecordId chars) + 24·(segments + laps)`\n\nThe optional route fields are FREE — a 21 000-point route serialises to the byte-identical size\nwith and without altitude and accuracies.\n\nPinned boundary (f99): a 13-char title + 13-char clientRecordId gives `bytes = 212 + 48·points`,\nand 20 829 points is exactly 1 000 004 B — the first failing size.\n\n⚠ One Mainline build's parcel encoding. A safety margin, not a contract."
        },
        {
          "name": "ExistingWorkoutDto",
          "slug": "existing-workout-dto",
          "kind": "interface",
          "declaration": "interface ExistingWorkoutDto {\n    readonly nativeId: string;\n    readonly version: number;\n}"
        },
        {
          "name": "GetRouteOptions",
          "slug": "get-route-options",
          "kind": "interface",
          "declaration": "interface GetRouteOptions {\n    /**\n     * What to do when `routeState === 'consentRequired'` (Android only — HealthKit has no per-route\n     * consent).\n     * - `'skip'` (default) — throw `consentRequired`. Never shows UI, never blocks.\n     * - `'prompt'`         — show the platform's per-route dialog and, if the user allows, stream the\n     *   route from that same call. Can block for tens of seconds, so it must be driven by an explicit\n     *   user gesture. Only one prompt may be in flight per process; a concurrent call throws `busy`.\n     */\n    readonly consent?: 'skip' | 'prompt' | undefined;\n}"
        },
        {
          "name": "hasAndroidIndoorPair",
          "slug": "has-android-indoor-pair",
          "kind": "function",
          "declaration": "/** 이 kind가 Android에서 `indoor`를 왕복시키는가 (= 상수 쌍이 있는가). */\ndeclare function hasAndroidIndoorPair(kind: WorkoutKind): boolean;",
          "sourceDocumentation": "이 kind가 Android에서 `indoor`를 왕복시키는가 (= 상수 쌍이 있는가)."
        },
        {
          "name": "HeartRateDto",
          "slug": "heart-rate-dto",
          "kind": "interface",
          "declaration": "interface HeartRateDto {\n    readonly t: number;\n    readonly bpm: number;\n}"
        },
        {
          "name": "HeartRateSample",
          "slug": "heart-rate-sample",
          "kind": "interface",
          "declaration": "/** One heart-rate reading. The same shape on read and on write. */\ninterface HeartRateSample {\n    /** Epoch MILLISECONDS. */\n    readonly t: number;\n    /** Integer beats per minute, 1..300. Samples outside that range are dropped on write. */\n    readonly bpm: number;\n}",
          "sourceDocumentation": "One heart-rate reading. The same shape on read and on write."
        },
        {
          "name": "HeartRateSummaryDto",
          "slug": "heart-rate-summary-dto",
          "kind": "interface",
          "declaration": "interface HeartRateSummaryDto {\n    readonly avgBpm?: number | null | undefined;\n    readonly minBpm?: number | null | undefined;\n    readonly maxBpm?: number | null | undefined;\n}"
        },
        {
          "name": "Interval",
          "slug": "interval",
          "kind": "interface",
          "declaration": "interface Interval {\n    readonly startMs: number;\n    readonly endMs: number;\n}"
        },
        {
          "name": "IOS_SCOPE_TYPES",
          "slug": "ios-scope-types",
          "kind": "constant",
          "declaration": "IOS_SCOPE_TYPES: Readonly<Record<Scope, readonly string[]>>",
          "sourceDocumentation": "HealthKit 타입 식별자. `elevation`이 **빈 집합**인 것이 이 표의 핵심이다."
        },
        {
          "name": "iosActivityTypeFromKind",
          "slug": "ios-activity-type-from-kind",
          "kind": "function",
          "declaration": "/**\n * WRITE direction, iOS. `indoor` is NOT part of the integer choice on this platform — it is written\n * separately to `HKMetadataKeyIndoorWorkout` (and `HKMetadataKeySwimmingLocationType` for swimming),\n * and OMITTED entirely when `undefined` so the read side can keep telling \"outdoor\" and \"unknown\"\n * apart.\n * ⚠ `kind: 'other'` writes `.other`(3000) and is NOT recoverable on read.\n * ⚠ Never emits 20 or 71 — those are read-aliases only.\n */\ndeclare function iosActivityTypeFromKind(kind: WorkoutKind, indoor?: boolean | undefined): number;",
          "sourceDocumentation": "WRITE direction, iOS. `indoor` is NOT part of the integer choice on this platform — it is written\nseparately to `HKMetadataKeyIndoorWorkout` (and `HKMetadataKeySwimmingLocationType` for swimming),\nand OMITTED entirely when `undefined` so the read side can keep telling \"outdoor\" and \"unknown\"\napart.\n⚠ `kind: 'other'` writes `.other`(3000) and is NOT recoverable on read.\n⚠ Never emits 20 or 71 — those are read-aliases only."
        },
        {
          "name": "iosRequestIdentifiers",
          "slug": "ios-request-identifiers",
          "kind": "function",
          "declaration": "/**\n * iOS: scope -> HK 타입 식별자, **방향별로**. 같은 식별자가 양쪽에 나오는 것이 정상이다 —\n * HealthKit은 하나의 타입에 대해 read와 share를 따로 인가한다.\n * `'elevation'`은 빈 집합이므로 어느 쪽에도 아무것도 더하지 않는다(§8.8).\n */\ndeclare function iosRequestIdentifiers(request: AuthorizationRequest): DirectedPermissions;",
          "sourceDocumentation": "iOS: scope -> HK 타입 식별자, **방향별로**. 같은 식별자가 양쪽에 나오는 것이 정상이다 —\nHealthKit은 하나의 타입에 대해 read와 share를 따로 인가한다.\n`'elevation'`은 빈 집합이므로 어느 쪽에도 아무것도 더하지 않는다(§8.8)."
        },
        {
          "name": "IosWorkout",
          "slug": "ios-workout",
          "kind": "interface",
          "declaration": "interface IosWorkout extends WorkoutBase {\n    readonly platform: 'ios';\n    readonly platformData: IosWorkoutData;\n}"
        },
        {
          "name": "IosWorkoutData",
          "slug": "ios-workout-data",
          "kind": "interface",
          "declaration": "/** Raw iOS values the common model deliberately does not model. */\ninterface IosWorkoutData {\n    /** Raw HKWorkoutActivityType — the escape hatch for everything D11 collapses into 'other'. */\n    readonly activityTypeRaw: number;\n    readonly bundleIdentifier: string;\n    readonly productType?: string | undefined;\n    readonly osVersion?: string | undefined;\n    /** IANA identifier from HKMetadataKeyTimeZone, present only when the writer supplied one. */\n    readonly timeZoneId?: string | undefined;\n    readonly elevationDescendedM?: number | undefined;\n    /** `(endMs - startMs) / 1000`. Differs from `activeDurationS`, which honours the writer's own\n     *  `duration` argument. */\n    readonly wallClockS: number;\n    readonly syncIdentifier?: string | undefined;\n    readonly syncVersion?: number | undefined;\n    /** Number of HKWorkoutActivity entries (multi-sport workouts). */\n    readonly activityCount: number;\n    /** Whether the HKIndoorWorkout metadata key was present — the only honest indoor discriminator. */\n    readonly hasIndoorMetadataKey: boolean;\n    readonly routeSampleCount: number;\n}",
          "sourceDocumentation": "Raw iOS values the common model deliberately does not model."
        },
        {
          "name": "isWorkoutsError",
          "slug": "is-workouts-error",
          "kind": "function",
          "declaration": "/**\n * `instanceof` is unreliable across entries (see the file header). This guard uses the\n * `Symbol.for('gj-kit.workouts.error')` tag, which every copy of the class shares.\n */\ndeclare function isWorkoutsError(error: unknown): error is WorkoutsError;",
          "sourceDocumentation": "`instanceof` is unreliable across entries (see the file header). This guard uses the\n`Symbol.for('gj-kit.workouts.error')` tag, which every copy of the class shares."
        },
        {
          "name": "kindFromAndroidExerciseType",
          "slug": "kind-from-android-exercise-type",
          "kind": "function",
          "declaration": "/**\n * Health Connect exerciseType → WorkoutKind + indoor. TOTAL over `number`, same contract as above.\n *\n * ⚠ **The raw value is already destroyed before it reaches us.** Health Connect's\n *   `IntDefMappingsKt` collapses any unmapped int to 0 (`EXERCISE_TYPE_OTHER_WORKOUT`) on BOTH the\n *   read and the write IPC path. So for a future activity `platformData.android.exerciseType` reads\n *   0, not the real value, and `'other'` is all the information that exists.\n *\n * `indoor` is only decidable for the four kinds with a constant PAIR; for the other five it is\n * `undefined` because Health Connect stores the fact nowhere.\n *\n * Android has no read-aliases: every Health Connect constant we accept, we also emit.\n */\ndeclare function kindFromAndroidExerciseType(raw: number): {\n    kind: WorkoutKind;\n    indoor?: boolean | undefined;\n};",
          "sourceDocumentation": "Health Connect exerciseType → WorkoutKind + indoor. TOTAL over `number`, same contract as above.\n\n⚠ **The raw value is already destroyed before it reaches us.** Health Connect's\n  `IntDefMappingsKt` collapses any unmapped int to 0 (`EXERCISE_TYPE_OTHER_WORKOUT`) on BOTH the\n  read and the write IPC path. So for a future activity `platformData.android.exerciseType` reads\n  0, not the real value, and `'other'` is all the information that exists.\n\n`indoor` is only decidable for the four kinds with a constant PAIR; for the other five it is\n`undefined` because Health Connect stores the fact nowhere.\n\nAndroid has no read-aliases: every Health Connect constant we accept, we also emit."
        },
        {
          "name": "kindFromIosActivityType",
          "slug": "kind-from-ios-activity-type",
          "kind": "function",
          "declaration": "/**\n * Raw HKWorkoutActivityType → WorkoutKind. TOTAL over `number`: anything not in the table —\n * including negative, non-integer and huge values that only the JS boundary can produce — returns\n * `{ kind: 'other' }` with `indoor` left `undefined`.\n *\n * ⚠ **`indoor` never comes from this function on iOS.** HealthKit carries it in the\n *   `HKIndoorWorkout` metadata key (and, for swimming, in `HKMetadataKeySwimmingLocationType`),\n *   orthogonally to the activity type. The return shape keeps `indoor` for symmetry with the\n *   Android mapper and is always `undefined` here.\n *\n * ⚠ **Nothing collapses on iOS.** `HKWorkoutActivityType` is a plain `NSUInteger`, so an unknown\n *   value (e.g. 16 = Elliptical) arrives intact and IS preserved in\n *   `platformData.ios.activityTypeRaw` — an app can recover it. Contrast\n *   `kindFromAndroidExerciseType`.\n *\n * READ-ALIASES: 20 (FunctionalStrengthTraining) → `'strength'` and 71 (WheelchairRunPace) →\n * `'wheelchair'` map INTO kinds the write direction never emits, so the two mapper directions are\n * NOT literal inverses. The asserted property is write-then-read only.\n */\ndeclare function kindFromIosActivityType(raw: number): {\n    kind: WorkoutKind;\n    indoor?: boolean | undefined;\n};",
          "sourceDocumentation": "Raw HKWorkoutActivityType → WorkoutKind. TOTAL over `number`: anything not in the table —\nincluding negative, non-integer and huge values that only the JS boundary can produce — returns\n`{ kind: 'other' }` with `indoor` left `undefined`.\n\n⚠ **`indoor` never comes from this function on iOS.** HealthKit carries it in the\n  `HKIndoorWorkout` metadata key (and, for swimming, in `HKMetadataKeySwimmingLocationType`),\n  orthogonally to the activity type. The return shape keeps `indoor` for symmetry with the\n  Android mapper and is always `undefined` here.\n\n⚠ **Nothing collapses on iOS.** `HKWorkoutActivityType` is a plain `NSUInteger`, so an unknown\n  value (e.g. 16 = Elliptical) arrives intact and IS preserved in\n  `platformData.ios.activityTypeRaw` — an app can recover it. Contrast\n  `kindFromAndroidExerciseType`.\n\nREAD-ALIASES: 20 (FunctionalStrengthTraining) → `'strength'` and 71 (WheelchairRunPace) →\n`'wheelchair'` map INTO kinds the write direction never emits, so the two mapper directions are\nNOT literal inverses. The asserted property is write-then-read only."
        },
        {
          "name": "Lap",
          "slug": "lap",
          "kind": "interface",
          "declaration": "interface Lap extends Interval {\n    readonly distanceM?: number | undefined;\n}"
        },
        {
          "name": "LapDto",
          "slug": "lap-dto",
          "kind": "interface",
          "declaration": "interface LapDto {\n    readonly startMs: number;\n    readonly endMs: number;\n    readonly distanceM?: number | null | undefined;\n}"
        },
        {
          "name": "ListQuery",
          "slug": "list-query",
          "kind": "interface",
          "declaration": "interface ListQuery extends TimeWindow {\n    /** From a previous page's `nextPageToken`. **NOT a sync cursor** — the two carry different magic. */\n    readonly pageToken?: WorkoutsPageToken | undefined;\n}"
        },
        {
          "name": "MAX_ANDROID_ROUTE_POINTS",
          "slug": "max-android-route-points",
          "kind": "constant",
          "declaration": "MAX_ANDROID_ROUTE_POINTS = 20000",
          "sourceDocumentation": "The largest route this library will write **on Android**. Health Connect's record ceiling is\nexactly 1 000 000 bytes at `160 + 48·points + 2·chars + 24·(segments+laps)` (20 828 points OK /\n20 829 FAIL, and the optional point fields are FREE). 20 000 leaves ~40 KB of headroom for a\nMainline encoding change.\n\n⚠ This guard does NOT run on iOS. HealthKit was measured storing and streaming a 36 000-point\n  route with no leak and no ceiling. Discarding a user's 8-hour 1 Hz hike on iOS to mirror an\n  Android parcel limit is not defensible. Portability-conscious apps call\n  `estimateAndroidRecordBytes()` themselves."
        },
        {
          "name": "MAX_HEART_RATE_WINDOW_MS",
          "slug": "max-heart-rate-window-ms",
          "kind": "constant",
          "declaration": "MAX_HEART_RATE_WINDOW_MS = 86400000",
          "sourceDocumentation": "24 h. `readHeartRate` refuses wider windows so one call cannot return an unbounded array.\n⚠ This bounds the WINDOW, not the density: a 1 Hz watch still returns ~86 400 samples."
        },
        {
          "name": "MetricProvenance",
          "slug": "metric-provenance",
          "kind": "type",
          "declaration": "/**\n * Where a distance/energy number came from.\n * - 'associated' — summed from samples explicitly associated with the workout.\n * - 'total'      — a total the writer stated but did not back with samples (iOS legacy workouts).\n * - 'derived'    — summed over the workout's window from whatever samples were there.\n *                  **May include other sources.** Treat `derived` as a hint, never as the workout's\n *                  own number.\n */\ntype MetricProvenance = 'associated' | 'total' | 'derived';",
          "sourceDocumentation": "Where a distance/energy number came from.\n- 'associated' — summed from samples explicitly associated with the workout.\n- 'total'      — a total the writer stated but did not back with samples (iOS legacy workouts).\n- 'derived'    — summed over the workout's window from whatever samples were there.\n                 **May include other sources.** Treat `derived` as a hint, never as the workout's\n                 own number."
        },
        {
          "name": "MetricRowDto",
          "slug": "metric-row-dto",
          "kind": "interface",
          "declaration": "/** 한 메트릭 레코드 행. 세션당이 아니라 **페이지 창당** 한 번 읽는다(§8.4). */\ninterface MetricRowDto {\n    readonly type: MetricTypeDto;\n    readonly startMs: number;\n    readonly endMs: number;\n    readonly value: number;\n    /** `dataOrigin.packageName` — 소스별 합산을 `./core`가 하기 위해 필요하다. */\n    readonly origin: string;\n}",
          "sourceDocumentation": "한 메트릭 레코드 행. 세션당이 아니라 **페이지 창당** 한 번 읽는다(§8.4)."
        },
        {
          "name": "MetricTypeDto",
          "slug": "metric-type-dto",
          "kind": "type",
          "declaration": "type MetricTypeDto = 'distance' | 'activeEnergy' | 'elevation' | 'steps';"
        },
        {
          "name": "missingDeclarations",
          "slug": "missing-declarations",
          "kind": "function",
          "declaration": "/**\n * 이 빌드가 **선언한** 집합(`declared`) 밖의 것을 요청했는지. 반환값은 소비자가 고쳐야 할\n * **config-plugin prop 이름**이다 (§5.7 58행: 메시지가 빠진 prop 이름을 말해야 한다).\n *\n * `'elevation'`은 iOS에서 빈 집합이라 선언할 것이 없으므로 여기서 절대 걸리지 않는다.\n */\ndeclare function missingDeclarations(request: AuthorizationRequest, platform: WorkoutsPlatform, declared: readonly string[]): readonly string[];",
          "sourceDocumentation": "이 빌드가 **선언한** 집합(`declared`) 밖의 것을 요청했는지. 반환값은 소비자가 고쳐야 할\n**config-plugin prop 이름**이다 (§5.7 58행: 메시지가 빠진 prop 이름을 말해야 한다).\n\n`'elevation'`은 iOS에서 빈 집합이라 선언할 것이 없으므로 여기서 절대 걸리지 않는다."
        },
        {
          "name": "NATIVE_ERROR_CODES",
          "slug": "native-error-codes",
          "kind": "constant",
          "declaration": "NATIVE_ERROR_CODES: Readonly<Record<string, WorkoutsErrorCode>>",
          "sourceDocumentation": "`ERR_WORKOUTS_*` -> 공개 코드. 14종 전수, 다른 것은 없다."
        },
        {
          "name": "nativeErrorCodeFor",
          "slug": "native-error-code-for",
          "kind": "function",
          "declaration": "/** `'routeTooLarge'` -> `'ERR_WORKOUTS_ROUTE_TOO_LARGE'`. */\ndeclare function nativeErrorCodeFor(code: WorkoutsErrorCode): string;",
          "sourceDocumentation": "`'routeTooLarge'` -> `'ERR_WORKOUTS_ROUTE_TOO_LARGE'`."
        },
        {
          "name": "NativePayloadDto",
          "slug": "native-payload-dto",
          "kind": "interface",
          "declaration": "/** 네이티브가 던지는 예외의 평면 표현 — `mapErrors.ts`의 입력이다. */\ninterface NativePayloadDto {\n    /** `ERR_WORKOUTS_*` — Expo 런타임이 예외 클래스명에서 만든 코드. */\n    readonly code?: string | null | undefined;\n    /** 템플릿으로 만든 짧은 진단 문자열. 좌표·건강값·제목·메모는 절대 들어가지 않는다. */\n    readonly message?: string | null | undefined;\n    /** Health Connect `HealthConnectException` errorCode / HKError code. */\n    readonly platformCode?: number | null | undefined;\n    readonly exceptionClass?: string | null | undefined;\n}",
          "sourceDocumentation": "네이티브가 던지는 예외의 평면 표현 — `mapErrors.ts`의 입력이다."
        },
        {
          "name": "NativeWorkoutsModule",
          "slug": "native-workouts-module",
          "kind": "interface",
          "declaration": "/**\n * 네이티브 모듈 계약. `./testing`의 `createFakeNativeWorkouts()`가 이것을 인메모리로 구현하고,\n * 실물은 `requireOptionalNativeModule('GjKitWorkouts')`가 돌려준다.\n */\ninterface NativeWorkoutsModule {\n    availability(): Promise<AvailabilityDto>;\n    authorizationSnapshot(): Promise<AuthorizationSnapshotDto>;\n    /**\n     * No internal timeout (f120, f122). Returns the raw before/after granted sets.\n     *\n     * ⚠ **Phase 3 correction (design defect found by running the example app).** This member used to\n     *   take ONE flat `readonly string[]`. That is lossless on Android — the direction lives in the\n     *   permission string itself (`READ_EXERCISE` vs `WRITE_EXERCISE`) — but on iOS the SAME type\n     *   identifier serves both directions, so a flat array cannot express what\n     *   `HKHealthStore.requestAuthorization(toShare:read:)` requires: the iOS lane could only either\n     *   over-request share access or silently drop it. The two sets are now explicit.\n     *   `read`/`write` are Android permission strings on Android and HK type identifiers on iOS.\n     *   `history` is Android's `READ_HEALTH_DATA_HISTORY` and rides in `read`.\n     */\n    requestPermissions(request: PermissionRequestDto): Promise<PermissionOutcomeDto>;\n    /** 커서의 `g` 지문을 만드는 원시 연산. 정렬된 granted 권한 문자열 목록을 그대로 준다. */\n    grantedScopeFingerprint(): Promise<string>;\n    /** Start instant in `[fromMs, toMs)`. iOS `.strictStartDate`, Android `TimeRangeFilter.between`. */\n    readWorkoutPage(query: WindowDto & {\n        readonly pageSize: number;\n        readonly pageToken?: string | undefined;\n    }): Promise<WorkoutPageDto>;\n    /** One call per metric type per PAGE WINDOW - never per session (§8.4). Never `aggregate()` (f109). */\n    readMetricRecords(query: WindowDto & {\n        readonly type: MetricTypeDto;\n        readonly origins: readonly string[];\n    }): Promise<readonly MetricRowDto[]>;\n    readHeartRateSamples(query: WindowDto): Promise<readonly HeartRateDto[]>;\n    /** iOS provenance discriminator required by RESULTS 206 / f71. */\n    hasAssociatedSamples(nativeId: string, quantity: QuantityKindDto): Promise<boolean>;\n    takeCheckpoint(): Promise<string>;\n    drainCheckpoint(checkpoint: string, limit: number): Promise<DrainBatchDto>;\n    openRoute(nativeId: string, consent: 'skip' | 'prompt'): Promise<RouteHandleDto>;\n    readRouteChunk(handle: string, maxPoints: number): Promise<readonly RoutePointDto[] | null>;\n    closeRoute(handle: string): Promise<void>;\n    /** iOS: look the workout up by sync identifier BEFORE writing (idx f26). Android: `null`. */\n    findBySyncIdentifier(clientId: string): Promise<ExistingWorkoutDto | null>;\n    saveWorkout(spec: WorkoutWriteDto): Promise<SaveOutcomeDto>;\n    /** Android only, ALWAYS called after a save (f93, f94). `null` when nothing was found. */\n    readBackVersion(clientId: string): Promise<number | null>;\n    deleteWorkout(ref: DeleteRefDto): Promise<boolean>;\n    openSettings(): Promise<void>;\n    openStoreListing(): Promise<void>;\n}",
          "sourceDocumentation": "네이티브 모듈 계약. `./testing`의 `createFakeNativeWorkouts()`가 이것을 인메모리로 구현하고,\n실물은 `requireOptionalNativeModule('GjKitWorkouts')`가 돌려준다."
        },
        {
          "name": "normalizeRouteForWrite",
          "slug": "normalize-route-for-write",
          "kind": "function",
          "declaration": "/**\n * Apply the write-side hygiene the library performs, so you can see what will happen before you call\n * `saveWorkout`. Throws `invalidArgument` for out-of-range coordinates. The rules and their order\n * are §8.2's and are pinned by `tests/fixtures/route-vectors.json`, which also drives the Swift and\n * Kotlin tests.\n *\n * Order (do not reorder — the platforms disagree and this order is what makes them agree):\n *  1. an EMPTY array is `invalidArgument` — say `route: 'none'` instead;\n *  2. non-finite `t`/`lat`/`lon`, or `lat` outside ±90 / `lon` outside ±180, is `invalidArgument`\n *     (rejected, NOT dropped — silently discarding hides a data-corruption signal);\n *  3. points with `hAccM < 0` or `hAccM > 50` are DROPPED (`hAccM === undefined` is kept);\n *  4. points outside `[window.startMs, window.endMs)` are DROPPED — deliberately not clamped:\n *     clamping piles every out-of-window point onto one boundary instant and rule 5 then collapses\n *     them into a single point, i.e. it fabricates timestamps AND destroys more data;\n *  5. sorted ascending by `t`, and for equal `t` the LAST point in input order wins — matching what\n *     HealthKit silently does, so that both platforms agree instead of one throwing.\n *\n * Zero survivors is not an error here: `saveWorkout` reports it as `route: 'dropped'`.\n */\ndeclare function normalizeRouteForWrite(points: readonly RoutePoint[], window: Interval): readonly RoutePoint[];",
          "sourceDocumentation": "Apply the write-side hygiene the library performs, so you can see what will happen before you call\n`saveWorkout`. Throws `invalidArgument` for out-of-range coordinates. The rules and their order\nare §8.2's and are pinned by `tests/fixtures/route-vectors.json`, which also drives the Swift and\nKotlin tests.\n\nOrder (do not reorder — the platforms disagree and this order is what makes them agree):\n 1. an EMPTY array is `invalidArgument` — say `route: 'none'` instead;\n 2. non-finite `t`/`lat`/`lon`, or `lat` outside ±90 / `lon` outside ±180, is `invalidArgument`\n    (rejected, NOT dropped — silently discarding hides a data-corruption signal);\n 3. points with `hAccM < 0` or `hAccM > 50` are DROPPED (`hAccM === undefined` is kept);\n 4. points outside `[window.startMs, window.endMs)` are DROPPED — deliberately not clamped:\n    clamping piles every out-of-window point onto one boundary instant and rule 5 then collapses\n    them into a single point, i.e. it fabricates timestamps AND destroys more data;\n 5. sorted ascending by `t`, and for equal `t` the LAST point in input order wins — matching what\n    HealthKit silently does, so that both platforms agree instead of one throwing.\n\nZero survivors is not an error here: `saveWorkout` reports it as `route: 'dropped'`."
        },
        {
          "name": "Pause",
          "slug": "pause",
          "kind": "interface",
          "declaration": "/** `auto` is true for platform-detected pauses (HK motionPaused / HC REST-flagged segments). */\ninterface Pause extends Interval {\n    readonly auto?: boolean | undefined;\n}",
          "sourceDocumentation": "`auto` is true for platform-detected pauses (HK motionPaused / HC REST-flagged segments)."
        },
        {
          "name": "PauseDto",
          "slug": "pause-dto",
          "kind": "interface",
          "declaration": "interface PauseDto {\n    readonly startMs: number;\n    readonly endMs: number;\n    readonly auto?: boolean | null | undefined;\n}"
        },
        {
          "name": "PermissionOutcomeDto",
          "slug": "permission-outcome-dto",
          "kind": "interface",
          "declaration": "interface PermissionOutcomeDto {\n    readonly before: readonly string[];\n    readonly after: readonly string[];\n    readonly conclusive: boolean;\n}"
        },
        {
          "name": "PermissionRequestDto",
          "slug": "permission-request-dto",
          "kind": "interface",
          "declaration": "/**\n * 권한 요청의 원시 결과. **before/after 비교가 판정의 유일한 근거다** — contract의 반환 집합을\n * \"사용자가 방금 부여한 것\"으로 읽지 않는다.\n * `conclusive: false`는 플랫폼이 아무것도 돌려주지 않았다는 뜻이며(f120), 그때 scope 상태는 불변이다.\n */\n/**\n * 권한 요청의 **방향이 있는** 입력 (Phase 3 결함 B). 플랫폼별 문자열 어휘:\n *  - Android — `android.permission.health.READ_*` / `…WRITE_*`. `read`와 `write`의 합집합이\n *    contract 집합이며, `READ_EXERCISE_ROUTES`는 **절대 여기 들어오지 않는다**(f110).\n *  - iOS — HealthKit 타입 식별자. `read`는 `requestAuthorization(read:)`, `write`는 `toShare:`다.\n *    같은 식별자가 양쪽에 동시에 나타나는 것이 정상이다.\n */\ninterface PermissionRequestDto {\n    readonly read: readonly string[];\n    readonly write: readonly string[];\n}",
          "sourceDocumentation": "권한 요청의 **방향이 있는** 입력 (Phase 3 결함 B). 플랫폼별 문자열 어휘:\n - Android — `android.permission.health.READ_*` / `…WRITE_*`. `read`와 `write`의 합집합이\n   contract 집합이며, `READ_EXERCISE_ROUTES`는 **절대 여기 들어오지 않는다**(f110).\n - iOS — HealthKit 타입 식별자. `read`는 `requestAuthorization(read:)`, `write`는 `toShare:`다.\n   같은 식별자가 양쪽에 동시에 나타나는 것이 정상이다."
        },
        {
          "name": "QuantityKindDto",
          "slug": "quantity-kind-dto",
          "kind": "type",
          "declaration": "/** iOS provenance 판별에 쓰이는 두 종류 (RESULTS 206 / f71). */\ntype QuantityKindDto = 'distance' | 'activeEnergy';",
          "sourceDocumentation": "iOS provenance 판별에 쓰이는 두 종류 (RESULTS 206 / f71)."
        },
        {
          "name": "READABLE_CURSOR_VERSIONS",
          "slug": "readable-cursor-versions",
          "kind": "constant",
          "declaration": "READABLE_CURSOR_VERSIONS: readonly number[]",
          "sourceDocumentation": "Every version this build can still READ.\n주의: 이 목록을 줄이는 것은 BREAKING change다 — 기존 사용자 전부가 전체 백필로 되돌아간다.\nCHANGELOG에 그렇게 명시해야 한다."
        },
        {
          "name": "ReadBudget",
          "slug": "read-budget",
          "kind": "class",
          "declaration": "/**\n * The client-side read pacer.\n *\n * Budgets: 900 / 15 min and 4 500 / 24 h — 10 % under the measured device constants (1000 / 5000),\n * because those are server-pushed and a Mainline update can move them.\n *\n * It NEVER blocks: it refuses an over-budget call BEFORE the platform call with `rateLimited` and a\n * computed `retryAfterMs`. Sleeping inside a 60-second poll would stall the consumer's whole\n * single-flight pipeline.\n *\n * ⚠ Process-local and blind to another health library in the same app: Health Connect's own limiter\n *   is per-uid, so during a migration off another library our accounting is optimistic.\n */\ndeclare class ReadBudget {\n    private readonly now;\n    private readonly spends;\n    constructor(options?: ReadBudgetOptions);\n    private prune;\n    private countSince;\n    /**\n     * `null` when `count` more reads fit right now; otherwise the milliseconds to wait before the\n     * oldest blocking spend leaves its window. NEVER blocks and NEVER throws.\n     */\n    retryAfterMs(count?: number): number | null;\n    /**\n     * Charge `count` reads, or refuse with `rateLimited` + `retryAfterMs` BEFORE the platform call.\n     * Nothing is charged when it refuses.\n     */\n    spend(count?: number): void;\n    /** 진단용 — 두 창의 현재 사용량. */\n    usage(): {\n        readonly shortWindow: number;\n        readonly longWindow: number;\n    };\n}",
          "sourceDocumentation": "The client-side read pacer.\n\nBudgets: 900 / 15 min and 4 500 / 24 h — 10 % under the measured device constants (1000 / 5000),\nbecause those are server-pushed and a Mainline update can move them.\n\nIt NEVER blocks: it refuses an over-budget call BEFORE the platform call with `rateLimited` and a\ncomputed `retryAfterMs`. Sleeping inside a 60-second poll would stall the consumer's whole\nsingle-flight pipeline.\n\n⚠ Process-local and blind to another health library in the same app: Health Connect's own limiter\n  is per-uid, so during a migration off another library our accounting is optimistic."
        },
        {
          "name": "ReadBudgetOptions",
          "slug": "read-budget-options",
          "kind": "interface",
          "declaration": "interface ReadBudgetOptions {\n    /** 주입 클록. 테스트가 시간을 소유한다. */\n    readonly now?: (() => number) | undefined;\n}"
        },
        {
          "name": "reconcileSyncPage",
          "slug": "reconcile-sync-page",
          "kind": "function",
          "declaration": "/**\n * Split one sync page into the three operations a local store actually performs.\n *\n * - `rekeys` come from `removed[].replaced === true` matched against the same page's `added` —\n *   iOS replaces a workout's native id when the same sync identifier is re-saved. Apply them as an\n *   UPDATE of the primary key, NEVER as DELETE + INSERT, or you lose your local join data (server\n *   ids, upload state, notes).\n * - `deletes` are the genuinely-gone ids. Applying one for an id you never held must be a no-op.\n *\n * ⚠ Matching heuristic, stated out loud: `RemovedWorkout` deliberately does not carry a\n *   `replacedById` field, so a replaced removal is paired, in order, with the same page's own\n *   writes (`isOwn && clientId != null`). A batch that carries several replacements at once pairs\n *   them positionally, which is what the platform emits. A replaced removal that finds no partner\n *   is NEVER turned into a delete — `replaced: true` means the workout still exists, and deleting\n *   it would destroy the caller's join data.\n */\ndeclare function reconcileSyncPage(page: Pick<SyncPage, 'added' | 'removed'>): {\n    readonly upserts: readonly Workout[];\n    readonly deletes: readonly string[];\n    readonly rekeys: readonly {\n        readonly fromId: string;\n        readonly toId: string;\n    }[];\n};",
          "sourceDocumentation": "Split one sync page into the three operations a local store actually performs.\n\n- `rekeys` come from `removed[].replaced === true` matched against the same page's `added` —\n  iOS replaces a workout's native id when the same sync identifier is re-saved. Apply them as an\n  UPDATE of the primary key, NEVER as DELETE + INSERT, or you lose your local join data (server\n  ids, upload state, notes).\n- `deletes` are the genuinely-gone ids. Applying one for an id you never held must be a no-op.\n\n⚠ Matching heuristic, stated out loud: `RemovedWorkout` deliberately does not carry a\n  `replacedById` field, so a replaced removal is paired, in order, with the same page's own\n  writes (`isOwn && clientId != null`). A batch that carries several replacements at once pairs\n  them positionally, which is what the platform emits. A replaced removal that finds no partner\n  is NEVER turned into a delete — `replaced: true` means the workout still exists, and deleting\n  it would destroy the caller's join data."
        },
        {
          "name": "RemovedDto",
          "slug": "removed-dto",
          "kind": "interface",
          "declaration": "interface RemovedDto {\n    readonly id: string;\n    readonly replaced: boolean;\n}"
        },
        {
          "name": "RemovedWorkout",
          "slug": "removed-workout",
          "kind": "interface",
          "declaration": "interface RemovedWorkout {\n    readonly id: string;\n    /**\n     * `true` only with POSITIVE evidence that the same logical workout still exists under a different\n     * native id. **Always `false` on Android** — an upsert there keeps the same deterministic UUID.\n     *\n     * `false` does NOT mean \"definitely and permanently deleted\": HealthKit may purge deletion records\n     * before we ever see them, so a workout can vanish with no `removed` entry at all.\n     */\n    readonly replaced: boolean;\n}"
        },
        {
          "name": "requiredWriteScopes",
          "slug": "required-write-scopes",
          "kind": "function",
          "declaration": "/**\n * WRITE-side pre-flight. Derives, from the fields a `WorkoutWrite` ACTUALLY CARRIES, which write\n * scopes its single `insertRecords` transaction will need. A workout with no `distanceM` needs no\n * `'distance'` scope, so nothing is over-demanded.\n *\n * `saveWorkout` calls this BEFORE touching the store and throws `notAuthorized` naming the missing\n * scope. Without it, owner decision ②'s split would ship a hard REGRESSION: Android writes the\n * session and all five metric records in ONE transaction, so a missing `WRITE_DISTANCE` fails the\n * whole transaction and the workout is not saved at all.\n *\n * `'routes'` is the documented exception and is NEVER a throw: a missing route scope stays the\n * established non-fatal path (`SaveResult.route === 'notPermitted'`).\n *\n * `steps <= 0` deliberately does NOT demand `'steps'`: Health Connect throws on a zero-count\n * `StepsRecord`, so the library never writes one.\n */\ndeclare function requiredWriteScopes(workout: WorkoutWrite): readonly Scope[];",
          "sourceDocumentation": "WRITE-side pre-flight. Derives, from the fields a `WorkoutWrite` ACTUALLY CARRIES, which write\nscopes its single `insertRecords` transaction will need. A workout with no `distanceM` needs no\n`'distance'` scope, so nothing is over-demanded.\n\n`saveWorkout` calls this BEFORE touching the store and throws `notAuthorized` naming the missing\nscope. Without it, owner decision ②'s split would ship a hard REGRESSION: Android writes the\nsession and all five metric records in ONE transaction, so a missing `WRITE_DISTANCE` fails the\nwhole transaction and the workout is not saved at all.\n\n`'routes'` is the documented exception and is NEVER a throw: a missing route scope stays the\nestablished non-fatal path (`SaveResult.route === 'notPermitted'`).\n\n`steps <= 0` deliberately does NOT demand `'steps'`: Health Connect throws on a zero-count\n`StepsRecord`, so the library never writes one."
        },
        {
          "name": "RouteAccess",
          "slug": "route-access",
          "kind": "type",
          "declaration": "/**\n * How far route reads reach right now.\n * - 'all'      — the route permission is held AND the app is in the foreground.\n * - 'own'      — only routes this app wrote read inline.\n * - 'perRoute' — nothing reads inline; each route needs `getRoute(id, { consent: 'prompt' })`.\n *\n * ⚠ On iOS this is always `'all'` and is NOT evidence of anything — read it together with\n *   `read.routes === 'unknown'`.\n * ⚠ On Android `'all'` does NOT guarantee a route read succeeds: Health Connect's first-run\n *   onboarding is an undocumented further precondition. `'all'` + `getRoute` throwing\n *   `consentRequired` is the signature of incomplete onboarding — send the user to `openSettings()`.\n * CLOSED for 1.x.\n */\ntype RouteAccess = 'all' | 'own' | 'perRoute';",
          "sourceDocumentation": "How far route reads reach right now.\n- 'all'      — the route permission is held AND the app is in the foreground.\n- 'own'      — only routes this app wrote read inline.\n- 'perRoute' — nothing reads inline; each route needs `getRoute(id, { consent: 'prompt' })`.\n\n⚠ On iOS this is always `'all'` and is NOT evidence of anything — read it together with\n  `read.routes === 'unknown'`.\n⚠ On Android `'all'` does NOT guarantee a route read succeeds: Health Connect's first-run\n  onboarding is an undocumented further precondition. `'all'` + `getRoute` throwing\n  `consentRequired` is the signature of incomplete onboarding — send the user to `openSettings()`.\nCLOSED for 1.x."
        },
        {
          "name": "routeDistanceM",
          "slug": "route-distance-m",
          "kind": "function",
          "declaration": "/** Great-circle length of a route, metres. Ignores altitude. */\ndeclare function routeDistanceM(points: readonly RoutePoint[]): number;",
          "sourceDocumentation": "Great-circle length of a route, metres. Ignores altitude."
        },
        {
          "name": "routeElevationGainM",
          "slug": "route-elevation-gain-m",
          "kind": "function",
          "declaration": "/**\n * Cumulative ascent, metres, with hysteresis: only rises of at least `minRiseM` count.\n * Required on purpose — \"what counts as a climb\" differs between hiking and cycling apps and there\n * is no defensible default.\n *\n * Points without `altM` are skipped; they neither break nor extend a rise.\n */\ndeclare function routeElevationGainM(points: readonly RoutePoint[], minRiseM: number): number;",
          "sourceDocumentation": "Cumulative ascent, metres, with hysteresis: only rises of at least `minRiseM` count.\nRequired on purpose — \"what counts as a climb\" differs between hiking and cycling apps and there\nis no defensible default.\n\nPoints without `altM` are skipped; they neither break nor extend a rise."
        },
        {
          "name": "RouteHandleDto",
          "slug": "route-handle-dto",
          "kind": "interface",
          "declaration": "interface RouteHandleDto {\n    /** 핸들 문자열. `closeRoute(handle)`에 그대로 되돌려준다. */\n    readonly handle: string;\n    /** 이 워크아웃의 route 상태 — **매 읽기마다 재계산된 값**이다(f114, 절대 캐시 금지). */\n    readonly state: RouteState;\n}"
        },
        {
          "name": "RoutePoint",
          "slug": "route-point",
          "kind": "interface",
          "declaration": "/**\n * One GPS fix. SI units, unit in the field name.\n *\n * Negative CoreLocation sentinels (`-1`) are mapped to `undefined` for `hAccM`, `vAccM`, `speedMps`\n * and `courseDeg`; an explicit `0` is PRESERVED as `0`, because HealthKit preserves it.\n * `altM` is passed through verbatim — a negative altitude is a legal value (Dead Sea), not a\n * sentinel; `vAccM` is the actual validity flag for it.\n */\ninterface RoutePoint {\n    /** Epoch MILLISECONDS. Strictly increasing after our normalisation. */\n    readonly t: number;\n    /** WGS84 degrees, -90..90. Out of range is `invalidArgument` on BOTH platforms. */\n    readonly lat: number;\n    /** WGS84 degrees, -180..180. */\n    readonly lon: number;\n    readonly altM?: number | undefined;\n    /** Horizontal accuracy, metres. */\n    readonly hAccM?: number | undefined;\n    /** Vertical accuracy, metres. */\n    readonly vAccM?: number | undefined;\n    /** iOS only — Health Connect's `ExerciseRoute.Location` has no speed field. */\n    readonly speedMps?: number | undefined;\n    /** iOS only. */\n    readonly courseDeg?: number | undefined;\n}",
          "sourceDocumentation": "One GPS fix. SI units, unit in the field name.\n\nNegative CoreLocation sentinels (`-1`) are mapped to `undefined` for `hAccM`, `vAccM`, `speedMps`\nand `courseDeg`; an explicit `0` is PRESERVED as `0`, because HealthKit preserves it.\n`altM` is passed through verbatim — a negative altitude is a legal value (Dead Sea), not a\nsentinel; `vAccM` is the actual validity flag for it."
        },
        {
          "name": "RoutePointDto",
          "slug": "route-point-dto",
          "kind": "interface",
          "declaration": "interface RoutePointDto {\n    readonly t: number;\n    readonly lat: number;\n    readonly lon: number;\n    readonly altM?: number | null | undefined;\n    readonly hAccM?: number | null | undefined;\n    readonly vAccM?: number | null | undefined;\n    readonly speedMps?: number | null | undefined;\n    readonly courseDeg?: number | null | undefined;\n}"
        },
        {
          "name": "RouteState",
          "slug": "route-state",
          "kind": "type",
          "declaration": "/**\n * Whether a GPS route can be read for a workout, RECOMPUTED ON EVERY READ.\n * Never cache this across app sessions: on Android an app can lose read access to routes it wrote\n * itself once both route scopes are revoked.\n *\n * - 'available'       — the route can be streamed with `getRoute()` right now.\n * - 'consentRequired' — a route EXISTS but is not readable. **Never collapse this to 'none'.**\n * - 'none'            — there is no route at all. On iOS this is also what a denied read looks like.\n * CLOSED for 1.x.\n */\ntype RouteState = 'available' | 'consentRequired' | 'none';",
          "sourceDocumentation": "Whether a GPS route can be read for a workout, RECOMPUTED ON EVERY READ.\nNever cache this across app sessions: on Android an app can lose read access to routes it wrote\nitself once both route scopes are revoked.\n\n- 'available'       — the route can be streamed with `getRoute()` right now.\n- 'consentRequired' — a route EXISTS but is not readable. **Never collapse this to 'none'.**\n- 'none'            — there is no route at all. On iOS this is also what a denied read looks like.\nCLOSED for 1.x."
        },
        {
          "name": "RouteWriteOutcome",
          "slug": "route-write-outcome",
          "kind": "type",
          "declaration": "/**\n * What happened to `route`.\n *  - 'stored'       — written and readable.\n *  - 'none'         — you passed `'none'`.\n *  - 'dropped'      — you passed points but NOTHING survived hygiene; the workout was still saved.\n *  - 'notPermitted' — Android: WRITE_EXERCISE_ROUTE is not granted; the workout was still saved.\n *                     ⚠ On a re-save this means the previously stored route is now GONE.\n *  - 'deferred'     — `status === 'pendingUnlock'`; the retry will attach it.\n */\ntype RouteWriteOutcome = 'stored' | 'none' | 'dropped' | 'notPermitted' | 'deferred';",
          "sourceDocumentation": "What happened to `route`.\n - 'stored'       — written and readable.\n - 'none'         — you passed `'none'`.\n - 'dropped'      — you passed points but NOTHING survived hygiene; the workout was still saved.\n - 'notPermitted' — Android: WRITE_EXERCISE_ROUTE is not granted; the workout was still saved.\n                    ⚠ On a re-save this means the previously stored route is now GONE.\n - 'deferred'     — `status === 'pendingUnlock'`; the retry will attach it."
        },
        {
          "name": "SaveOutcomeDto",
          "slug": "save-outcome-dto",
          "kind": "interface",
          "declaration": "interface SaveOutcomeDto {\n    readonly status: 'saved' | 'pendingUnlock';\n    readonly nativeId?: string | null | undefined;\n    readonly route: RouteWriteOutcome;\n    readonly routePointsWritten: number;\n}"
        },
        {
          "name": "SaveResult",
          "slug": "save-result",
          "kind": "type",
          "declaration": "/**\n * A discriminated union, so `nativeId` does not EXIST on the `pendingUnlock` branch. That branch\n * only ever appears on a locked device, i.e. never during development, so a type that merely made\n * `nativeId` optional would be forgotten by everyone.\n */\ntype SaveResult = {\n    readonly status: 'saved';\n    /** Echo of `WorkoutWrite.id`. */\n    readonly id: string;\n    /** The platform's own id for the stored workout. */\n    readonly nativeId: string;\n    readonly route: Exclude<RouteWriteOutcome, 'deferred'>;\n    /** How many points actually reached the store. Compare it against what you sent to see how\n     *  much our mandatory hygiene removed. */\n    readonly routePointsWritten: number;\n} | {\n    /**\n     * The store accepted the workout but cannot confirm it while the device is locked.\n     * **Do not re-save blindly.** Call `saveWorkout` again with the SAME `id` and `version` once\n     * the device is unlocked; that call is idempotent and completes the route.\n     */\n    readonly status: 'pendingUnlock';\n    readonly id: string;\n    readonly route: 'deferred';\n    readonly routePointsWritten: 0;\n};",
          "sourceDocumentation": "A discriminated union, so `nativeId` does not EXIST on the `pendingUnlock` branch. That branch\nonly ever appears on a locked device, i.e. never during development, so a type that merely made\n`nativeId` optional would be forgotten by everyone."
        },
        {
          "name": "Scope",
          "slug": "scope",
          "kind": "type",
          "declaration": "type Scope = (typeof SCOPES)[number];"
        },
        {
          "name": "SCOPES",
          "slug": "scopes",
          "kind": "constant",
          "declaration": "SCOPES: readonly [\n    \"workouts\",\n    \"distance\",\n    \"activeEnergy\",\n    \"elevation\",\n    \"routes\",\n    \"heartRate\",\n    \"steps\"\n]",
          "sourceDocumentation": "One authorization vocabulary for both platforms. Read calls have NO `include` flags — capability\nis chosen once, at authorization time. CLOSED for 1.x.\n\nOwner decision ② (2026-08-22) split this union from four members to seven so the consuming\ndeveloper chooses the granularity. Use `WORKOUT_TOTALS_SCOPES` for the coarse form; name the\nmembers individually for the fine form.\n\n⚠ **`'workouts'` no longer implies totals.** `read: ['workouts']` is valid code before and after\n  this change and means something materially different after: ONE Android permission row instead\n  of four, and `distanceM` / `activeEnergyKcal` / `elevationGainM` `undefined` on EVERY workout.\n\n- `workouts`     — the exercise SESSION and its intrinsic fields only.\n- `distance`     — gates `Workout.distanceM` + `distanceProvenance`. iOS requests BOTH\n                   `.distanceWalkingRunning` AND `.distanceCycling`, always both.\n- `activeEnergy` — gates `Workout.activeEnergyKcal` + `activeEnergyProvenance`. Named\n                   `activeEnergy` and NOT `energy`: `TotalCaloriesBurnedRecord` is forbidden as a\n                   fallback because it silently mixes in BMR.\n- `elevation`    — gates `Workout.elevationGainM`. ⚠ On iOS this maps to the EMPTY HealthKit set\n                   and therefore ALIASES `workouts`.\n- `routes`       — read maps to READ_EXERCISE_ROUTES, which is manifest-declared and NEVER\n                   requestable at runtime; write maps to WRITE_EXERCISE_ROUTE (singular).\n- `heartRate` / `steps` — the READ_/WRITE_ pair for that type; each also gates its own top-level\n                   read function."
        },
        {
          "name": "ScopeStatus",
          "slug": "scope-status",
          "kind": "type",
          "declaration": "/**\n * - 'granted'      — proceed.\n * - 'denied'       — the user said no. `openSettings()`; asking again will not help.\n * - 'undetermined' — never asked, OR the last request was inconclusive. Call `requestAuthorization()`.\n * - 'unknown'      — unknowable by platform design. EVERY iOS read scope that has already been asked\n *                    about reports this, permanently. Proceed, and treat an empty result as\n *                    ambiguous rather than as \"no data\".\n * CLOSED for 1.x.\n */\ntype ScopeStatus = 'granted' | 'denied' | 'undetermined' | 'unknown';",
          "sourceDocumentation": "- 'granted'      — proceed.\n- 'denied'       — the user said no. `openSettings()`; asking again will not help.\n- 'undetermined' — never asked, OR the last request was inconclusive. Call `requestAuthorization()`.\n- 'unknown'      — unknowable by platform design. EVERY iOS read scope that has already been asked\n                   about reports this, permanently. Proceed, and treat an empty result as\n                   ambiguous rather than as \"no data\".\nCLOSED for 1.x."
        },
        {
          "name": "SourceDto",
          "slug": "source-dto",
          "kind": "interface",
          "declaration": "interface SourceDto {\n    readonly id: string;\n    readonly name?: string | null | undefined;\n    readonly version?: string | null | undefined;\n    readonly deviceModel?: string | null | undefined;\n}"
        },
        {
          "name": "StepTotal",
          "slug": "step-total",
          "kind": "interface",
          "declaration": "interface StepTotal {\n    /**\n     * Steps in the window. `0` is a real answer.\n     * When several apps wrote steps over the window this is the LARGEST SINGLE-`dataOrigin` total, not\n     * the sum — a phone + watch device is never double-counted. It will therefore disagree with the\n     * number Health Connect's own UI shows, which merges by an app-priority list we cannot read.\n     * On iOS a denied read scope is indistinguishable from no data, so `0` can also mean \"not granted\".\n     */\n    readonly count: number;\n}"
        },
        {
          "name": "SyncPage",
          "slug": "sync-page",
          "kind": "interface",
          "declaration": "interface SyncPage {\n    /**\n     * An idempotent UPSERT SET keyed by `id` (or by `clientId` for own writes), never a delta append.\n     * The same workout may legitimately appear in two consecutive results. Health Connect emits an\n     * upsertion change even for a write that changed nothing, so the presence of a workout here is not\n     * a claim that it changed.\n     */\n    readonly added: readonly Workout[];\n    /** May contain ids this app never held. `remove(unknown id)` MUST be a no-op. */\n    readonly removed: readonly RemovedWorkout[];\n    /**\n     * Persist this together with `added`/`removed` **IN ONE TRANSACTION**. Persisting the cursor\n     * without the items loses those workouts permanently and the library cannot prevent it.\n     */\n    readonly cursor: WorkoutsSyncCursor;\n    /** `true` = call `syncWorkouts(result.cursor)` again immediately. */\n    readonly hasMore: boolean;\n}"
        },
        {
          "name": "SyncResult",
          "slug": "sync-result",
          "kind": "type",
          "declaration": "/**\n * Discriminated on `reset`, so `resetReason` is unreachable without narrowing and unforgettable when\n * present. `const b: boolean = result.reset` still compiles, so this stays read-compatible with the\n * mission's `reset: boolean` sketch at every call site.\n */\ntype SyncResult = (SyncPage & {\n    readonly reset: false;\n}) | (SyncPage & {\n    readonly added: readonly [\n    ];\n    readonly removed: readonly [\n    ];\n    readonly hasMore: false;\n    readonly reset: true;\n    readonly resetReason: CursorResetReason;\n});",
          "sourceDocumentation": "Discriminated on `reset`, so `resetReason` is unreachable without narrowing and unforgettable when\npresent. `const b: boolean = result.reset` still compiles, so this stays read-compatible with the\nmission's `reset: boolean` sketch at every call site."
        },
        {
          "name": "TimeWindow",
          "slug": "time-window",
          "kind": "interface",
          "declaration": "/**\n * Epoch-ms half-open window. Everywhere in this library it means: the record's **START instant** in\n * `[fromMs, toMs)`. There is no overlap variant and no local-day variant — day bucketing is your\n * job, done afterwards from `utcOffsetMin`.\n *\n * Both bounds are validated against `EPOCH_MS_FLOOR`: a value in `(0, 1e11)` is rejected with\n * `invalidArgument` because it is a seconds timestamp in a milliseconds field.\n */\ninterface TimeWindow {\n    /** Inclusive. Epoch MILLISECONDS, integer. */\n    readonly fromMs: number;\n    /** EXCLUSIVE. Epoch MILLISECONDS, integer, > `fromMs`. */\n    readonly toMs: number;\n}",
          "sourceDocumentation": "Epoch-ms half-open window. Everywhere in this library it means: the record's **START instant** in\n`[fromMs, toMs)`. There is no overlap variant and no local-day variant — day bucketing is your\njob, done afterwards from `utcOffsetMin`.\n\nBoth bounds are validated against `EPOCH_MS_FLOOR`: a value in `(0, 1e11)` is rejected with\n`invalidArgument` because it is a seconds timestamp in a milliseconds field."
        },
        {
          "name": "unpopulatedWorkoutMetrics",
          "slug": "unpopulated-workout-metrics",
          "kind": "function",
          "declaration": "/**\n * READ-side answer to \"why is this field `undefined` on every workout?\", without a device.\n *\n * Returns the `Workout` FIELD names — not scope names — whose gating read scope is `'denied'` or\n * `'undetermined'`. `'undetermined'` is the load-bearing half: it is the exact shape of the\n * `read: ['workouts']` trap (never asked), and an implementation that only looked for `'denied'`\n * would miss the trap entirely.\n *\n * It returns ONLY what we positively know:\n * - `'unknown'` NEVER produces an accusation, so on iOS this always returns `[]`.\n * - On an unavailable / updateRequired platform it returns `[]`.\n */\ndeclare function unpopulatedWorkoutMetrics(state: AuthorizationState): readonly WorkoutMetricField[];",
          "sourceDocumentation": "READ-side answer to \"why is this field `undefined` on every workout?\", without a device.\n\nReturns the `Workout` FIELD names — not scope names — whose gating read scope is `'denied'` or\n`'undetermined'`. `'undetermined'` is the load-bearing half: it is the exact shape of the\n`read: ['workouts']` trap (never asked), and an implementation that only looked for `'denied'`\nwould miss the trap entirely.\n\nIt returns ONLY what we positively know:\n- `'unknown'` NEVER produces an accusation, so on iOS this always returns `[]`.\n- On an unavailable / updateRequired platform it returns `[]`."
        },
        {
          "name": "WindowDto",
          "slug": "window-dto",
          "kind": "interface",
          "declaration": "interface WindowDto {\n    readonly fromMs: number;\n    readonly toMs: number;\n}"
        },
        {
          "name": "Workout",
          "slug": "workout",
          "kind": "type",
          "declaration": "/** Discriminated by `platform` — `if (w.platform === 'ios')` narrows `platformData` with ZERO casts. */\ntype Workout = IosWorkout | AndroidWorkout;",
          "sourceDocumentation": "Discriminated by `platform` — `if (w.platform === 'ios')` narrows `platformData` with ZERO casts."
        },
        {
          "name": "WORKOUT_KINDS",
          "slug": "workout-kinds",
          "kind": "constant",
          "declaration": "WORKOUT_KINDS: readonly [\n    \"running\",\n    \"walking\",\n    \"hiking\",\n    \"cycling\",\n    \"swimming\",\n    \"rowing\",\n    \"strength\",\n    \"wheelchair\",\n    \"other\"\n]",
          "sourceDocumentation": "D11, as amended by the product owner on 2026-08-22 (the original D11 named five members:\nrunning | walking | hiking | cycling | other).\n\nAnything the platform reports that is not one of the eight named kinds collapses to `'other'`;\nthe raw value survives under `platformData` (iOS only — see below). CLOSED for 1.x; a member may\nstill be added in 0.x as a MINOR, which breaks exhaustive switches.\n\nEvery member maps to a NON-DEPRECATED constant on BOTH platforms — the full table with raw\nintegers lives in `activity.ts` and is pinned by `tests/fixtures/activity-vectors.json`.\n\n⚠ `indoor` is STORED on iOS and DERIVED on Android. Health Connect's `ExerciseSessionRecord` has\n  no indoor field, so `indoor` survives an Android round-trip only for the four kinds that have a\n  constant PAIR (`running`, `cycling`, `swimming`, `rowing`). For `walking`, `hiking`, `strength`,\n  `wheelchair` and `other` it is written nowhere on Android and reads back `undefined`.\n⚠ The escape hatch is asymmetric. On iOS an unmapped `HKWorkoutActivityType` arrives intact in\n  `platformData.ios.activityTypeRaw`. On Android the value is already destroyed before it reaches\n  us — Health Connect collapses any unmapped int to 0 on BOTH the read and the write IPC path —\n  so `platformData.android.exerciseType` reads 0 and `'other'` is all the information that exists."
        },
        {
          "name": "WORKOUT_METRIC_SCOPES",
          "slug": "workout-metric-scopes",
          "kind": "constant",
          "declaration": "WORKOUT_METRIC_SCOPES: {\n    readonly distanceM: \"distance\";\n    readonly activeEnergyKcal: \"activeEnergy\";\n    readonly elevationGainM: \"elevation\";\n    readonly heartRate: \"heartRate\";\n    readonly steps: \"steps\";\n}",
          "sourceDocumentation": "The single table that ties every optional `Workout` metric to the ONE scope that gates it. The\n`satisfies` clause is a live guard, not decoration: a key that is not a `WorkoutBase` field and a\nvalue that is not a `Scope` are BOTH compile errors, so this table cannot drift away from\n`Workout` or from `Scope`.\n\n`routeState` is deliberately absent: it is per-workout and recomputed on every read, so it can\nnever be answered from an `AuthorizationState` snapshot."
        },
        {
          "name": "WORKOUT_TOTALS_SCOPES",
          "slug": "workout-totals-scopes",
          "kind": "constant",
          "declaration": "WORKOUT_TOTALS_SCOPES: readonly [\n    \"workouts\",\n    \"distance\",\n    \"activeEnergy\",\n    \"elevation\"\n]",
          "sourceDocumentation": "The coarse form of owner decision ②, in ONE token: the session plus every total the common\n`Workout` model carries. Spread it in place —\n\n```ts\nawait requestAuthorization({ read: [...WORKOUT_TOTALS_SCOPES, 'routes'] });\n```\n\nThis is the recipe to copy unless you have a reason not to. Naming the members individually is\nthe NARROW case and should be a deliberate act.\n\nIt DELIBERATELY EXCLUDES `'routes'`: a convenience constant must never hide a non-requestable\nscope inside itself, or it lies about what the permission dialog will show.\n\n⚠ Spread it; do not `.concat()` it (TS2769), and do not park it in an un-annotated intermediate\n  (`string[]`, then TS2322 at the use site). Add `satisfies readonly Scope[]` if you need a\n  variable."
        },
        {
          "name": "WorkoutBase",
          "slug": "workout-base",
          "kind": "interface",
          "declaration": "/** The fields both platforms share. Never used directly — see `Workout`. */\ninterface WorkoutBase {\n    /** The PLATFORM id: HKWorkout.uuid / ExerciseSessionRecord.metadata.id. Pass this to `getRoute`. */\n    readonly id: string;\n    /**\n     * The id the WRITING app used (HKMetadataKeySyncIdentifier / clientRecordId), when present.\n     * It is the STABLE upsert key for own writes: on iOS `id` changes when a workout is replaced while\n     * `clientId` does not. It is visible cross-app, so never put anything sensitive in it.\n     */\n    readonly clientId?: string | undefined;\n    /** True when this app wrote it. Nothing is filtered on your behalf — the sync loop needs to see\n     *  its own echo to reconcile native ids. Filter on this yourself. */\n    readonly isOwn: boolean;\n    readonly kind: WorkoutKind;\n    /**\n     * `undefined` when the platform cannot tell. iOS raw `locationType` 3 means \"outdoor OR unknown\",\n     * so an absent HKIndoorWorkout metadata key leaves this undefined rather than `false`.\n     *\n     * ⚠ **Platform-asymmetric, by construction.** On iOS this is STORED, so it round-trips for every\n     *   `kind`. On Android it is DERIVED from `exerciseType` alone, so it survives only for the four\n     *   kinds with a constant pair (`running`, `cycling`, `swimming`, `rowing`) and reads back\n     *   `undefined` for the other five. On those four paired kinds the opposite rounding happens:\n     *   `indoor: undefined` normalizes to `false` after an Android round-trip.\n     */\n    readonly indoor?: boolean | undefined;\n    readonly startMs: number;\n    readonly endMs: number;\n    /**\n     * Active seconds. iOS: the store's own `duration`, which honours the writer's explicit value and\n     * can differ from `endMs - startMs`. Android: `(endMs - startMs)` minus every PAUSE segment.\n     */\n    readonly activeDurationS: number;\n    /** Minutes east of UTC at the workout's start. Use this for day bucketing. */\n    readonly utcOffsetMin?: number | undefined;\n    readonly source: WorkoutSource;\n    /**\n     * Metres. `undefined` means UNKNOWN — never 0.\n     * ⚠ Populated only when the `'distance'` read scope is granted. With `read: ['workouts']` alone\n     *   this field is `undefined` on EVERY workout. `unpopulatedWorkoutMetrics(state)` answers \"which\n     *   fields can never be filled with the permissions I hold\" without a device.\n     */\n    readonly distanceM?: number | undefined;\n    readonly distanceProvenance?: MetricProvenance | undefined;\n    /**\n     * Active kcal, never total/BMR-inclusive. `undefined` means UNKNOWN — never 0.\n     * ⚠ Populated only when the `'activeEnergy'` read scope is granted.\n     */\n    readonly activeEnergyKcal?: number | undefined;\n    readonly activeEnergyProvenance?: MetricProvenance | undefined;\n    /**\n     * Metres of cumulative ascent. ⚠ Populated only when the `'elevation'` read scope is granted.\n     * On iOS that scope maps to the EMPTY HealthKit set and therefore aliases `'workouts'`.\n     */\n    readonly elevationGainM?: number | undefined;\n    /** ⚠ Populated only when the `'heartRate'` read scope is granted. */\n    readonly heartRate?: WorkoutHeartRateSummary | undefined;\n    /** ⚠ Populated only when the `'steps'` read scope is granted. */\n    readonly steps?: number | undefined;\n    /** Explicit pause segments only. */\n    readonly pauses: readonly Pause[];\n    readonly laps: readonly Lap[];\n    readonly routeState: RouteState;\n    readonly lastModifiedMs?: number | undefined;\n}",
          "sourceDocumentation": "The fields both platforms share. Never used directly — see `Workout`."
        },
        {
          "name": "WorkoutDto",
          "slug": "workout-dto",
          "kind": "interface",
          "declaration": "/**\n * 한 워크아웃의 평면 DTO.\n *\n * ⚠ `kind`가 없다 — **활동 매핑은 `./core`가 한다.** 네이티브는 raw 정수(`activityTypeRaw`)만\n *   보내고 `activity.ts`가 `WorkoutKind`로 접는다. 그래야 매핑표가 Node에서 fuzz된다.\n * ⚠ `indoor`도 마찬가지로 iOS에서만 채워진다(메타데이터 사다리의 결과). Android는 `null`이고\n *   `./core`가 `exerciseType`에서 파생한다.\n */\ninterface WorkoutDto {\n    readonly platform: WorkoutsPlatform;\n    readonly id: string;\n    readonly clientId?: string | null | undefined;\n    readonly isOwn: boolean;\n    /** HKWorkoutActivityType (iOS) / ExerciseSessionRecord.exerciseType (Android). */\n    readonly activityTypeRaw: number;\n    /** iOS only. Android sends `null` — `./core` derives it from `activityTypeRaw`. */\n    readonly indoor?: boolean | null | undefined;\n    readonly startMs: number;\n    readonly endMs: number;\n    readonly activeDurationS: number;\n    readonly utcOffsetMin?: number | null | undefined;\n    readonly source: SourceDto;\n    readonly distanceM?: number | null | undefined;\n    readonly distanceProvenance?: MetricProvenance | null | undefined;\n    readonly activeEnergyKcal?: number | null | undefined;\n    readonly activeEnergyProvenance?: MetricProvenance | null | undefined;\n    readonly elevationGainM?: number | null | undefined;\n    readonly heartRate?: HeartRateSummaryDto | null | undefined;\n    readonly steps?: number | null | undefined;\n    readonly pauses: readonly PauseDto[];\n    readonly laps: readonly LapDto[];\n    readonly routeState: RouteState;\n    readonly lastModifiedMs?: number | null | undefined;\n    /** Exactly one of these is present, matching `platform`. */\n    readonly ios?: IosWorkoutData | null | undefined;\n    readonly android?: AndroidWorkoutData | null | undefined;\n}",
          "sourceDocumentation": "한 워크아웃의 평면 DTO.\n\n⚠ `kind`가 없다 — **활동 매핑은 `./core`가 한다.** 네이티브는 raw 정수(`activityTypeRaw`)만\n  보내고 `activity.ts`가 `WorkoutKind`로 접는다. 그래야 매핑표가 Node에서 fuzz된다.\n⚠ `indoor`도 마찬가지로 iOS에서만 채워진다(메타데이터 사다리의 결과). Android는 `null`이고\n  `./core`가 `exerciseType`에서 파생한다."
        },
        {
          "name": "WorkoutHeartRateSummary",
          "slug": "workout-heart-rate-summary",
          "kind": "interface",
          "declaration": "interface WorkoutHeartRateSummary {\n    readonly avgBpm?: number | undefined;\n    readonly minBpm?: number | undefined;\n    readonly maxBpm?: number | undefined;\n}"
        },
        {
          "name": "WorkoutKind",
          "slug": "workout-kind",
          "kind": "type",
          "declaration": "type WorkoutKind = (typeof WORKOUT_KINDS)[number];"
        },
        {
          "name": "WorkoutMetricField",
          "slug": "workout-metric-field",
          "kind": "type",
          "declaration": "type WorkoutMetricField = keyof typeof WORKOUT_METRIC_SCOPES;"
        },
        {
          "name": "WorkoutPage",
          "slug": "workout-page",
          "kind": "interface",
          "declaration": "interface WorkoutPage {\n    /** DESCENDING by start instant — most recent first. The order is part of the contract, because it\n     *  is what makes a multi-launch backfill resumable. */\n    readonly items: readonly Workout[];\n    /** Absent = last page. */\n    readonly nextPageToken?: WorkoutsPageToken | undefined;\n}"
        },
        {
          "name": "WorkoutPageDto",
          "slug": "workout-page-dto",
          "kind": "interface",
          "declaration": "interface WorkoutPageDto {\n    readonly items: readonly WorkoutDto[];\n    /** The platform's own opaque token. `./core` wraps it in the `gjp1.` page-token magic. */\n    readonly nextPageToken?: string | null | undefined;\n}"
        },
        {
          "name": "WorkoutRef",
          "slug": "workout-ref",
          "kind": "type",
          "declaration": "/**\n * Identify a workout without ambiguity. Two id spaces exist and both are UUIDs, so no runtime\n * heuristic can tell them apart — the type makes the choice unmissable.\n * The `?: never` members are load-bearing: a bare `{a} | {b}` union ACCEPTS both keys together.\n */\ntype WorkoutRef = \n/** The platform id from `Workout.id`. */\n{\n    readonly nativeId: string;\n    readonly clientId?: never;\n}\n/** Your own `WorkoutWrite.id`. */\n | {\n    readonly clientId: string;\n    readonly nativeId?: never;\n};",
          "sourceDocumentation": "Identify a workout without ambiguity. Two id spaces exist and both are UUIDs, so no runtime\nheuristic can tell them apart — the type makes the choice unmissable.\nThe `?: never` members are load-bearing: a bare `{a} | {b}` union ACCEPTS both keys together."
        },
        {
          "name": "WORKOUTS_ERROR_CODES",
          "slug": "workouts-error-codes",
          "kind": "constant",
          "declaration": "WORKOUTS_ERROR_CODES: readonly [\n    \"unavailable\",\n    \"updateRequired\",\n    \"notAuthorized\",\n    \"consentRequired\",\n    \"historyRequired\",\n    \"rateLimited\",\n    \"busy\",\n    \"invalidArgument\",\n    \"routeTooLarge\",\n    \"staleVersion\",\n    \"storeLocked\",\n    \"cancelled\",\n    \"io\",\n    \"internal\"\n]"
        },
        {
          "name": "WorkoutsApi",
          "slug": "workouts-api",
          "kind": "interface",
          "declaration": "/**\n * Every function of `.`, as one interface. `.`'s `workouts` and `./testing`'s `api` are both\n * instances of it, produced by the SAME factory.\n */\ninterface WorkoutsApi {\n    getAvailability(): Promise<Availability>;\n    requestAuthorization(request: AuthorizationRequest): Promise<AuthorizationResult>;\n    getAuthorizationState(): Promise<AuthorizationState>;\n    listWorkouts(query: ListQuery): Promise<WorkoutPage>;\n    syncWorkouts(cursor: WorkoutsSyncCursor | null): Promise<SyncResult>;\n    getRoute(workoutId: string, options?: GetRouteOptions): AsyncIterable<readonly RoutePoint[]>;\n    readHeartRate(window: TimeWindow): Promise<readonly HeartRateSample[]>;\n    readSteps(window: TimeWindow): Promise<StepTotal>;\n    saveWorkout(workout: WorkoutWrite): Promise<SaveResult>;\n    deleteWorkout(ref: WorkoutRef): Promise<DeleteResult>;\n    openSettings(): Promise<void>;\n    openStoreListing(): Promise<void>;\n}",
          "sourceDocumentation": "Every function of `.`, as one interface. `.`'s `workouts` and `./testing`'s `api` are both\ninstances of it, produced by the SAME factory."
        },
        {
          "name": "WorkoutsError",
          "slug": "workouts-error",
          "kind": "class",
          "declaration": "declare class WorkoutsError extends Error {\n    readonly code: WorkoutsErrorCode;\n    readonly retryAfterMs?: number | undefined;\n    readonly nativeMessage?: string | undefined;\n    /** 사본 인식 태그 — `isWorkoutsError`의 유일한 판정 근거다. */\n    readonly [WORKOUTS_ERROR_TAG]: true;\n    constructor(code: WorkoutsErrorCode, message: string, options?: WorkoutsErrorOptions);\n}"
        },
        {
          "name": "workoutsErrorCode",
          "slug": "workouts-error-code",
          "kind": "function",
          "declaration": "/** `null` for anything that is not one of ours. */\ndeclare function workoutsErrorCode(error: unknown): WorkoutsErrorCode | null;",
          "sourceDocumentation": "`null` for anything that is not one of ours."
        },
        {
          "name": "WorkoutsErrorCode",
          "slug": "workouts-error-code--type",
          "kind": "type",
          "declaration": "type WorkoutsErrorCode = (typeof WORKOUTS_ERROR_CODES)[number];"
        },
        {
          "name": "WorkoutsErrorOptions",
          "slug": "workouts-error-options",
          "kind": "interface",
          "declaration": "interface WorkoutsErrorOptions {\n    readonly cause?: unknown;\n    /** Only meaningful with code 'rateLimited'. */\n    readonly retryAfterMs?: number | undefined;\n    /**\n     * A short, TEMPLATE-BUILT diagnostic string from the native layer: exception class name, platform\n     * error code, and a bounded reason token. NEVER coordinates, heart rates, distances, energies,\n     * step counts, titles or notes — a source-scan guard enforces this.\n     */\n    readonly nativeMessage?: string | undefined;\n}"
        },
        {
          "name": "workoutsExceptionClassName",
          "slug": "workouts-exception-class-name",
          "kind": "function",
          "declaration": "/** `'routeTooLarge'` -> `'WorkoutsRouteTooLargeException'` (Swift와 Kotlin에서 동일한 이름). */\ndeclare function workoutsExceptionClassName(code: WorkoutsErrorCode): string;",
          "sourceDocumentation": "`'routeTooLarge'` -> `'WorkoutsRouteTooLargeException'` (Swift와 Kotlin에서 동일한 이름)."
        },
        {
          "name": "WorkoutSource",
          "slug": "workout-source",
          "kind": "interface",
          "declaration": "interface WorkoutSource {\n    /** iOS bundle identifier / Android package name. Apple Watch first-party reads as\n     *  `com.apple.health.<UUID>`. */\n    readonly id: string;\n    /** iOS only — Android's DataOrigin carries a package name and nothing else. */\n    readonly name?: string | undefined;\n    readonly version?: string | undefined;\n    readonly deviceModel?: string | undefined;\n}"
        },
        {
          "name": "WorkoutsPageToken",
          "slug": "workouts-page-token",
          "kind": "type",
          "declaration": "/** Opaque, and NOT interchangeable with a sync cursor — the two carry different magic prefixes. */\ntype WorkoutsPageToken = string;",
          "sourceDocumentation": "Opaque, and NOT interchangeable with a sync cursor — the two carry different magic prefixes."
        },
        {
          "name": "WorkoutsPlatform",
          "slug": "workouts-platform",
          "kind": "type",
          "declaration": "/** The health store a workout came from. Also the discriminator of the `Workout` union. */\ntype WorkoutsPlatform = 'ios' | 'android';",
          "sourceDocumentation": "The health store a workout came from. Also the discriminator of the `Workout` union."
        },
        {
          "name": "WorkoutsSyncCursor",
          "slug": "workouts-sync-cursor",
          "kind": "type",
          "declaration": "/** Opaque. Persist it verbatim; never parse, compare or construct one. */\ntype WorkoutsSyncCursor = string;",
          "sourceDocumentation": "Opaque. Persist it verbatim; never parse, compare or construct one."
        },
        {
          "name": "WorkoutWrite",
          "slug": "workout-write",
          "kind": "interface",
          "declaration": "/**\n * Full-state input for `saveWorkout`. There is NO partial-update path: on Android an upsert that\n * omits the route DELETES the stored route, so the only safe contract is \"send everything\".\n */\ninterface WorkoutWrite {\n    /**\n     * A stable id this app owns — the idempotency key. Becomes HKMetadataKeySyncIdentifier /\n     * Health Connect `clientRecordId`.\n     * ⚠ Other apps CAN read this value on Android. Use an opaque UUID.\n     * Must match `/^[A-Za-z0-9._:-]{1,120}$/`.\n     */\n    readonly id: string;\n    /**\n     * A safe integer >= 1, non-decreasing per `id`, that increases whenever the content changes.\n     * Derive it from your own record's `updatedAt` (epoch ms) or from an edit counter.\n     * ⚠ NEVER `Date.now()` at call time: a crash retry would write a fresh version and, on iOS, mint a\n     *   second workout object and orphan the first one's samples and route.\n     * An EQUAL version replaces the stored workout; a LOWER one throws `staleVersion` and writes nothing.\n     */\n    readonly version: number;\n    /**\n     * Nine members since owner decision ③. `'other'` is the documented lossy sink — it stores\n     * OTHER_WORKOUT(0) / `.other`(3000) and the original activity is not recoverable.\n     */\n    readonly kind: WorkoutKind;\n    /**\n     * Drives the platform activity constant on write.\n     * ⚠ On Android it is only representable for `running`, `cycling`, `swimming` and `rowing`; for\n     *   every other kind it is silently dropped and reads back `undefined`. On iOS it is written to\n     *   `HKMetadataKeyIndoorWorkout` for every kind — but only when you actually set it: leaving it\n     *   `undefined` OMITS the key rather than writing `@NO`.\n     */\n    readonly indoor?: boolean | undefined;\n    readonly startMs: number;\n    /** Must be > `startMs` and <= now. */\n    readonly endMs: number;\n    readonly utcOffsetMin?: number | undefined;\n    /** IANA zone id (e.g. `'Asia/Seoul'`). iOS metadata only; Android stores only the offset. */\n    readonly timeZoneId?: string | undefined;\n    readonly pauses?: readonly Pause[] | undefined;\n    readonly laps?: readonly Lap[] | undefined;\n    readonly distanceM?: number | undefined;\n    readonly activeEnergyKcal?: number | undefined;\n    readonly elevationGainM?: number | undefined;\n    /** Omitted from the write when <= 0 — Health Connect throws on `StepsRecord(count = 0)`. */\n    readonly steps?: number | undefined;\n    /** Samples outside 1..300 bpm or outside `[startMs, endMs)` are dropped before writing. */\n    readonly heartRate?: readonly HeartRateSample[] | undefined;\n    /**\n     * REQUIRED, and `'none'` is not the same call shape as an empty array.\n     *\n     * ⚠ This is the one place where forgetting a field DESTROYS user data: an Android upsert that\n     *   omits the route while holding the route write scope DELETES the stored route. Making the field\n     *   required turns that silent, irreversible mistake into a compile error, and `'none'` forces the\n     *   intent to be stated out loud.\n     * An empty array is `invalidArgument` — say `'none'`.\n     */\n    readonly route: readonly RoutePoint[] | 'none';\n}",
          "sourceDocumentation": "Full-state input for `saveWorkout`. There is NO partial-update path: on Android an upsert that\nomits the route DELETES the stored route, so the only safe contract is \"send everything\"."
        },
        {
          "name": "WorkoutWriteDto",
          "slug": "workout-write-dto",
          "kind": "interface",
          "declaration": "interface WorkoutWriteDto {\n    readonly clientId: string;\n    readonly version: number;\n    /** 이미 매핑된 플랫폼 정수 — `activity.ts`가 계산한다. */\n    readonly activityTypeRaw: number;\n    /** `undefined`면 키를 아예 쓰지 않는다(iOS). Android는 정수 선택에 이미 반영돼 있다. */\n    readonly indoor?: boolean | undefined;\n    readonly startMs: number;\n    readonly endMs: number;\n    readonly utcOffsetMin?: number | undefined;\n    readonly timeZoneId?: string | undefined;\n    readonly pauses: readonly PauseDto[];\n    readonly laps: readonly LapDto[];\n    readonly distanceM?: number | undefined;\n    readonly activeEnergyKcal?: number | undefined;\n    readonly elevationGainM?: number | undefined;\n    /** `<= 0`이면 `./core`가 이미 제거했다 — Health Connect가 0-count StepsRecord에 throw한다. */\n    readonly steps?: number | undefined;\n    readonly heartRate: readonly HeartRateDto[];\n    /** 이미 위생을 통과한 점들. 빈 배열은 \"route 없음\"을 뜻한다. */\n    readonly route: readonly RoutePointDto[];\n}"
        }
      ]
    },
    {
      "subpath": "./plugin",
      "id": "plugin",
      "declarationTarget": "./dist/plugin.d.mts",
      "symbols": [
        {
          "name": "GjKitWorkoutsPluginProps",
          "slug": "gj-kit-workouts-plugin-props",
          "kind": "interface",
          "declaration": "interface GjKitWorkoutsPluginProps {\n    /**\n     * The scopes this app will ever ask for. Drives the iOS entitlement and every Android\n     * `<uses-permission>` line. `'routes'` in either list additionally emits the manifest-only\n     * READ_EXERCISE_ROUTES entry, which is MANDATORY: undeclared, route requests silently return\n     * nothing with no error at all.\n     *\n     * ⚠ Since owner decision ② the vocabulary is SEVEN scopes and `'workouts'` means the session\n     *   ALONE. `read: ['workouts']` in `app.json` now emits ONE `<uses-permission>` line instead of\n     *   four, and the failure shows up at runtime as `undefined` totals — far from the file that\n     *   caused it. For the old (coarse) behaviour write the four members out, or import\n     *   `WORKOUT_TOTALS_SCOPES` from `@gj-kit/expo-workouts/core` in an `app.config.ts`.\n     *   `./core` has zero peers, so importing it from a config file is safe.\n     */\n    readonly read?: readonly Scope[] | undefined;\n    readonly write?: readonly Scope[] | undefined;\n    /** D10. Adds READ_HEALTH_DATA_HISTORY. Default false — the 30-day wall is the default reality. */\n    readonly history?: boolean | undefined;\n    /**\n     * REQUIRED. Android 14+ launches `VIEW_PERMISSION_USAGE` + category `HEALTH_PERMISSIONS` at the\n     * app when the user taps \"privacy policy\" in the permission dialog, and the activity-alias this\n     * plugin registers needs somewhere to go. A dead link there is a user-visible defect, and Play's\n     * Health apps declaration requires a policy URL anyway — so this is not optional.\n     */\n    readonly privacyPolicyUrl: string;\n    readonly ios?: {\n        /**\n         * NSHealthShareUsageDescription. An English default is supplied; localise via\n         * `ios.infoPlist`/locales.\n         * ⚠ A missing usage string CRASHES at `requestAuthorization` — the plugin makes that\n         *   unreachable.\n         */\n        readonly shareUsageDescription?: string | undefined;\n        /** NSHealthUpdateUsageDescription. */\n        readonly updateUsageDescription?: string | undefined;\n    } | undefined;\n}"
        }
      ]
    },
    {
      "subpath": "./testing",
      "id": "testing",
      "declarationTarget": "./dist/testing.d.mts",
      "symbols": [
        {
          "name": "corruptCursor",
          "slug": "corrupt-cursor",
          "kind": "function",
          "declaration": "/**\n * Produce a cursor that forces one specific `CursorResetReason`, so a test can reach all SIX\n * without hand-rolling cursor strings. Three of them (`expired`, `scopesChanged`, `noCursor`) are\n * facts about the store or the call rather than about the string — for those this returns the\n * cursor unchanged (`null` for `noCursor`) and `expireCursor()` is the control that produces them.\n */\ndeclare function corruptCursor(cursor: WorkoutsSyncCursor, reason: CursorResetReason): WorkoutsSyncCursor | null;",
          "sourceDocumentation": "Produce a cursor that forces one specific `CursorResetReason`, so a test can reach all SIX\nwithout hand-rolling cursor strings. Three of them (`expired`, `scopesChanged`, `noCursor`) are\nfacts about the store or the call rather than about the string — for those this returns the\ncursor unchanged (`null` for `noCursor`) and `expireCursor()` is the control that produces them."
        },
        {
          "name": "createFakeNativeWorkouts",
          "slug": "create-fake-native-workouts",
          "kind": "function",
          "declaration": "/**\n * An in-memory `NativeWorkoutsModule`.\n *\n * ⚠ **The fake HONOURS SCOPES**: seed a fully-populated workout, authorize with only `['workouts']`,\n * and `listWorkouts` hands back `distanceM: undefined`. The read trap becomes reproducible in\n * `pnpm test`, on Node, in the CONSUMER'S OWN suite. The write side is symmetric: saving a workout\n * whose `requiredWriteScopes()` are not all granted rejects with `notAuthorized` naming the missing\n * scope, exactly as Android would.\n *\n * **States this fake still cannot reach** (they are platform facts with no seam representation):\n *  - iOS `pendingUnlock` caused by `finishRoute` failing while locked — the seam reports one\n *    `SaveOutcomeDto`, so the fake can produce `pendingUnlock`, but not the sub-case where the\n *    workout landed and only the route builder failed (f70 was never reproduced on a device either).\n *  - Health Connect's 30-day history wall silently TRUNCATING a large read (f46 in §5.7) — the\n *    platform gives no signal, so nothing downstream can be asserted.\n *  - `FLAG_PERMISSION_USER_FIXED` (\"routes never work again, silently\") — Phase 0 never reached it,\n *    so there is no measured behaviour to imitate.\n */\ndeclare function createFakeNativeWorkouts(seed?: FakeSeed): FakeNativeWorkouts;",
          "sourceDocumentation": "An in-memory `NativeWorkoutsModule`.\n\n⚠ **The fake HONOURS SCOPES**: seed a fully-populated workout, authorize with only `['workouts']`,\nand `listWorkouts` hands back `distanceM: undefined`. The read trap becomes reproducible in\n`pnpm test`, on Node, in the CONSUMER'S OWN suite. The write side is symmetric: saving a workout\nwhose `requiredWriteScopes()` are not all granted rejects with `notAuthorized` naming the missing\nscope, exactly as Android would.\n\n**States this fake still cannot reach** (they are platform facts with no seam representation):\n - iOS `pendingUnlock` caused by `finishRoute` failing while locked — the seam reports one\n   `SaveOutcomeDto`, so the fake can produce `pendingUnlock`, but not the sub-case where the\n   workout landed and only the route builder failed (f70 was never reproduced on a device either).\n - Health Connect's 30-day history wall silently TRUNCATING a large read (f46 in §5.7) — the\n   platform gives no signal, so nothing downstream can be asserted.\n - `FLAG_PERMISSION_USER_FIXED` (\"routes never work again, silently\") — Phase 0 never reached it,\n   so there is no measured behaviour to imitate."
        },
        {
          "name": "createFakeWorkouts",
          "slug": "create-fake-workouts",
          "kind": "function",
          "declaration": "/** Convenience wrapper: `createWorkoutsApi(native)` plus the same controls. */\ndeclare function createFakeWorkouts(seed?: FakeSeed): FakeWorkouts;",
          "sourceDocumentation": "Convenience wrapper: `createWorkoutsApi(native)` plus the same controls."
        },
        {
          "name": "drainSync",
          "slug": "drain-sync",
          "kind": "function",
          "declaration": "/**\n * Run the full sync loop to convergence against any `WorkoutsApi`. Test helper only — it holds the\n * whole store in memory and does NOT model the one-transaction rule, so a production app must write\n * the loop itself. `killAfterPages` reproduces a crash and reports whether the cursor was persisted\n * without the items, which is the one failure the library cannot prevent.\n */\ndeclare function drainSync(api: Pick<WorkoutsApi, 'syncWorkouts' | 'listWorkouts'>, opts: {\n    readonly backfillFromMs: number;\n    readonly cursor?: WorkoutsSyncCursor | null | undefined;\n    readonly maxPages?: number | undefined;\n    readonly killAfterPages?: number | undefined;\n}): Promise<{\n    readonly cursor: WorkoutsSyncCursor;\n    readonly store: ReadonlyMap<string, Workout>;\n    readonly pages: number;\n    readonly resets: readonly CursorResetReason[];\n}>;",
          "sourceDocumentation": "Run the full sync loop to convergence against any `WorkoutsApi`. Test helper only — it holds the\nwhole store in memory and does NOT model the one-transaction rule, so a production app must write\nthe loop itself. `killAfterPages` reproduces a crash and reports whether the cursor was persisted\nwithout the items, which is the one failure the library cannot prevent."
        },
        {
          "name": "FakeNativeWorkouts",
          "slug": "fake-native-workouts",
          "kind": "interface",
          "declaration": "/** An in-memory `NativeWorkoutsModule`. THIS is what tests drive; the real JS layer runs on top. */\ninterface FakeNativeWorkouts extends NativeWorkoutsModule {\n    setAvailability(availability: Availability): void;\n    setAuthorization(state: AuthorizationState): void;\n    /** Grant scopes directly — the shape the fake actually stores. */\n    authorize(grants: FakeScopeGrants): void;\n    /** What this build DECLARED. Requesting outside it is `invalidArgument` naming the missing prop. */\n    setDeclared(declared: FakeScopeGrants): void;\n    /** How the NEXT `requestPermissions` ends. `'inconclusive'` is f120's onboarding \"Go back\". */\n    setNextPermissionOutcome(outcome: FakePermissionOutcome): void;\n    /** Returns the native id. */\n    addWorkout(workout: FakeWorkoutInput): string;\n    /** Platform-faithful replacement: iOS mints a NEW native id and emits `removed{replaced:true}`\n     *  in the same drain batch; Android keeps the SAME id and emits only an upsertion change. */\n    replaceWorkout(nativeId: string, patch: Partial<FakeWorkoutInput>): string;\n    removeWorkout(nativeId: string): void;\n    /** HealthKit purging a deletion record before we drain it — the workout vanishes with no `removed`. */\n    purgeDeletion(nativeId: string): void;\n    /** Android: emit an upsertion change carrying an UNCHANGED record — the undetectable stale-version no-op. */\n    emitNoOpUpsertion(nativeId: string): void;\n    /** Force `reset: true` with a chosen reason on the next sync — reaches all six `CursorResetReason`s. */\n    expireCursor(reason?: CursorResetReason | undefined): void;\n    setRouteAccess(access: RouteAccess): void;\n    /** Process importance — the only way to reach the background-route path without a device. */\n    setForeground(foreground: boolean): void;\n    /** Health Connect first-run onboarding: foreign routes read `consentRequired` while this is false\n     *  even with the permission held and the app in the foreground. */\n    setOnboarded(onboarded: boolean): void;\n    /** iOS: protected data unavailable — the `storeLocked` pre-check path. */\n    setStoreLocked(locked: boolean): void;\n    /** iOS: make the next save return (nil workout, nil error) — the case Phase 0 could not reproduce. */\n    nextSaveIsPendingUnlock(): void;\n    /** f102: every read primitive raises Health Connect's overloaded `errorCode 7` while this is on. */\n    setRateLimited(rateLimited: boolean): void;\n    /** f109: `readMetricRecords` returns NOTHING, so every total stays `undefined` — never `0`. */\n    setMetricsMissing(missing: boolean): void;\n    /** Make the next call to one seam primitive throw a platform-shaped payload, so the error MAPPING\n     *  (not just the mapped result) is what the test exercises. */\n    failNext(primitive: keyof NativeWorkoutsModule, payload: NativePayloadDto): void;\n    /**\n     * f104: make the next call to one seam primitive **never settle**. The Intent-overflow failure is\n     * not an error — the ActivityResult callback simply never fires — so the only faithful fake is a\n     * promise that hangs and lets the caller's timeout be the thing under test.\n     */\n    hangNext(primitive: keyof NativeWorkoutsModule): void;\n    /** Unreleased route handles. A test asserts this is 0 after every `for await`. */\n    readonly openRouteHandles: number;\n    readonly calls: readonly {\n        readonly fn: keyof NativeWorkoutsModule;\n        readonly atMs: number;\n    }[];\n}",
          "sourceDocumentation": "An in-memory `NativeWorkoutsModule`. THIS is what tests drive; the real JS layer runs on top."
        },
        {
          "name": "FakePermissionOutcome",
          "slug": "fake-permission-outcome",
          "kind": "type",
          "declaration": "/** 권한 요청이 어떻게 끝나는가 — 셋 다 Phase 0가 실제로 관측한 결말이다. */\ntype FakePermissionOutcome = \n/** 사용자가 요청 집합을 전부 허용한다. */\n'grant'\n/** 사용자가 \"Don't allow\"를 누른다 — 결론적인 거부다. */\n | 'deny'\n/** f120: 온보딩 \"Go back\". 19.6 s 뒤 **빈 집합**, 전면 거부와 API 표면에서 구별 불가. */\n | 'inconclusive';",
          "sourceDocumentation": "권한 요청이 어떻게 끝나는가 — 셋 다 Phase 0가 실제로 관측한 결말이다."
        },
        {
          "name": "FakeScopeGrants",
          "slug": "fake-scope-grants",
          "kind": "interface",
          "declaration": "/** scope 집합을 seed와 컨트롤에서 같은 모양으로 받는다. */\ninterface FakeScopeGrants {\n    readonly read?: readonly Scope[] | undefined;\n    readonly write?: readonly Scope[] | undefined;\n    readonly history?: boolean | undefined;\n}",
          "sourceDocumentation": "scope 집합을 seed와 컨트롤에서 같은 모양으로 받는다."
        },
        {
          "name": "FakeSeed",
          "slug": "fake-seed",
          "kind": "interface",
          "declaration": "interface FakeSeed {\n    readonly platform: WorkoutsPlatform;\n    readonly availability?: Availability | undefined;\n    readonly authorization?: AuthorizationState | undefined;\n    /** 이미 부여된 scope. 생략하면 **전부 부여**돼 있다 — 대부분의 테스트가 인가를 소재로 하지 않는다. */\n    readonly granted?: FakeScopeGrants | undefined;\n    /** 이 빌드가 **선언한** scope (config plugin). 생략하면 전부. 선언 밖 요청은 `invalidArgument`다. */\n    readonly declared?: FakeScopeGrants | undefined;\n    readonly workouts?: readonly FakeWorkoutInput[] | undefined;\n    /** Deterministic clock. Defaults to a fixed instant so snapshots and budget tests are stable. */\n    readonly nowMs?: number | undefined;\n    /** `createFakeWorkouts`가 만드는 API에 그대로 넘어간다. `null`이면 읽기 페이서를 끈다. */\n    readonly budget?: ReadBudget | null | undefined;\n    /** per-route 동의 상한 (f104). 짧은 값 + `hangNext('openRoute')`가 그 상태를 재현한다. */\n    readonly routeConsentTimeoutMs?: number | undefined;\n}"
        },
        {
          "name": "FakeWorkoutInput",
          "slug": "fake-workout-input",
          "kind": "interface",
          "declaration": "interface FakeWorkoutInput {\n    /** 생략하면 페이크가 결정적 UUID를 만든다. */\n    readonly nativeId?: string | undefined;\n    readonly clientId?: string | undefined;\n    readonly isOwn?: boolean | undefined;\n    readonly kind?: WorkoutKind | undefined;\n    readonly indoor?: boolean | undefined;\n    readonly startMs: number;\n    readonly endMs: number;\n    readonly distanceM?: number | undefined;\n    readonly activeEnergyKcal?: number | undefined;\n    readonly elevationGainM?: number | undefined;\n    readonly steps?: number | undefined;\n    readonly heartRate?: readonly HeartRateSample[] | undefined;\n    /** 있으면 `routeState`는 기본적으로 `'available'`이 된다. */\n    readonly route?: readonly RoutePoint[] | undefined;\n    readonly routeState?: RouteState | undefined;\n    readonly sourceId?: string | undefined;\n}"
        },
        {
          "name": "FakeWorkouts",
          "slug": "fake-workouts",
          "kind": "interface",
          "declaration": "/** Convenience wrapper: `createWorkoutsApi(native)` plus the same controls. */\ninterface FakeWorkouts extends FakeNativeWorkouts {\n    readonly api: WorkoutsApi;\n}",
          "sourceDocumentation": "Convenience wrapper: `createWorkoutsApi(native)` plus the same controls."
        }
      ]
    }
  ]
}
