@cariva/vislife-health-sdk-rn
v0.1.3
Published
Cariva exercise SDK
Readme
@cariva/vislife-health-sdk-rn
React Native SDK for reading exercise and activity data from Android Health Connect and iOS HealthKit.
Published package: @cariva/vislife-health-sdk-rn
This release is a package move only. The JavaScript API, native module names, and React Native autolinking behavior stay the same.
Platform Support
| Platform | Status | |----------|--------| | Android | Supported (Health Connect) | | iOS | Supported (HealthKit) |
Requirements
- React Native 0.68+ (New Architecture / Turbo Module compatible)
- Android app
minSdkVersion 24or higher - iOS app
deploymentTarget13.0 or higher - A device or emulator with Health Connect (Android) or HealthKit (iOS) support
- Health Connect installed on Android 13 and lower, or available through Play Services / system components on Android 14+
Architecture
The SDK now follows a Clean/Hexagonal structure:
android/.../domain: core models and algorithm servicesandroid/.../application: use-cases and portsandroid/.../infrastructure: Health Connect/Android adaptersandroid/.../HealthConnectModule.kt: thin React Native bridge/controllersrc/domain,src/application,src/infrastructure: mirrored TypeScript boundary layers
See docs/architecture/clean-hexagonal-migration.md for migration details.
Installation
1. Install the package
# npm
npm install @cariva/vislife-health-sdk-rn
# yarn
yarn add @cariva/vislife-health-sdk-rn2. Android setup
- The library ships its own Health Connect permissions, package visibility queries, and permission rationale activities in the Android library manifest.
- In a standard React Native app, Gradle manifest merge and autolinking pick these up automatically when you rebuild the app after installation.
- On Android 13 and lower, install Health Connect before testing. On Android 14+, ensure the device has the required Play Services / system components available.
- Minimum verification after install:
- Launch the rebuilt app.
- Call
connect({ requestPermission: false, onMissingApp: 'status' }). - If
nextAction === 'GRANT_PERMISSIONS', callconnect({ requestPermission: true, onMissingApp: 'status' })to open the permission flow.
3. iOS setup
- Install native dependencies after adding the package:
cd ios && bundle exec pod install - Enable the HealthKit capability for your app target in Xcode.
- Add
NSHealthShareUsageDescriptiontoInfo.plistwith the user-facing reason your app reads health data. - Build and run on a physical iPhone or iPad with Health data available.
- Minimum verification after install:
- Launch the rebuilt app.
- Call
connect({ requestPermission: true, onMissingApp: 'status' }). - After the HealthKit prompt, call
getHealthData(...)to verify effective read access.
Quick Start
import {
connect,
getHealthData,
setAllowDatasource,
getAllowDatasource,
resetPermissions,
} from '@cariva/vislife-health-sdk-rn';
// 1. Connect and request permissions
const status = await connect({
onMissingApp: 'status',
requestPermission: true,
permissionScope: ['steps', 'calories'],
});
if (!status.ready) {
console.log('Next action required:', status.nextAction);
// e.g. 'INSTALL_HEALTH_CONNECT' | 'GRANT_PERMISSIONS' | ...
return;
}
// 2. Fetch exercise data
const data = await getHealthData({
sinceMillis: Date.now() - 7 * 24 * 60 * 60 * 1000, // last 7 days
untilMillis: Date.now(),
bucketPeriod: 'daily',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
bmr: 1500,
});
console.log('Records:', data.records);
console.log('Active kcal:', data.summary.activeKcal);
console.log('Time zone:', data.meta.timeZone);
// 3. Start the platform reset/manage-permission flow, then re-check
const resetResult = await resetPermissions();
if (resetResult.userActionRequired) {
// Show resetResult.message/instructions before asking the user to continue.
}
await connect({ requestPermission: false, onMissingApp: 'status' });API Reference
connect(options?)
Checks Health Connect availability and optionally requests permissions.
type PermissionScope = 'steps' | 'activetimes' | 'calories' | 'distances';
type ConnectOptions = {
onMissingApp?: 'status' | 'open';
requestPermission?: boolean;
permissionScope?: PermissionScope[];
};
type PermissionVerification = 'verified' | 'unverifiable' | 'not_checked';
type ConnectResult = {
ready: boolean;
healthConnectInstalled: boolean;
healthConnectStatus: 'AVAILABLE' | 'UPDATE_REQUIRED' | 'UNAVAILABLE' | 'UNKNOWN';
hasPermissions: boolean | 'unknown';
permissionStatus: 'granted' | 'partial' | 'denied' | 'unknown';
permissionVerification?: PermissionVerification;
nextAction:
| 'INSTALL_HEALTH_CONNECT'
| 'UPDATE_HEALTH_CONNECT'
| 'GRANT_PERMISSIONS'
| 'OPEN_SETTINGS'
| 'NONE';
grantedPermissions?: string[];
grantedScopes?: PermissionScope[];
missingScopes?: PermissionScope[];
errorCode?: 'USER_CANCELLED' | 'TIMEOUT' | 'NATIVE_ERROR' | 'CONNECT_IN_PROGRESS';
errorMessage?: string;
};| Option | Type | Default | Description |
|--------|------|---------|-------------|
| onMissingApp | 'status' \| 'open' | 'status' | 'open' opens Play Store for the missing app |
| requestPermission | boolean | true | Trigger the Health Connect permission dialog |
| permissionScope | PermissionScope[] | all scopes | Limit which permissions are requested |
Default behavior is split for backward compatibility:
connect()keeps the legacy interactive flow: it may open the Play Store when Health Connect needs installation/update and it requests permissions by default.connect({ ... })defaults missing option fields toonMissingApp: 'status',requestPermission: true, and all permission scopes.
For first-screen or app-start status checks, call:
const status = await connect({
requestPermission: false,
onMissingApp: 'status',
});This never opens the Health Connect permission UI and never opens the Play Store. If permissions are missing on Android, it returns ready: false, hasPermissions: false, permissionVerification: 'verified', and nextAction: 'GRANT_PERMISSIONS'.
On iOS, HealthKit read permission is not reliably inspectable. iOS connect() reports hasPermissions: 'unknown', permissionStatus: 'unknown', and permissionVerification: 'unverifiable' for read-permission success/silent paths; consumers must verify effective read access with getHealthData() behavior in their UX.
activetimes may request both exercise-session and active-calories read permissions so the native fallback reader can still derive active duration when session records are unavailable.
On iOS, HealthKit does not allow apps to inspect read permission grants non-interactively. Use this flow to handle the permission recheck:
// Step 1: Silent status check (no HealthKit prompt)
const status = await connect({
requestPermission: false,
onMissingApp: 'status',
});
if (status.nextAction === 'GRANT_PERMISSIONS') {
// Step 2: Request permission (shows HealthKit UI)
const result = await connect({
requestPermission: true,
onMissingApp: 'status',
});
// Step 3: After no system error, verify via data read
// iOS returns ready: true, hasPermissions: 'unknown' even if granted=false
// because read grants cannot be verified programmatically.
if (result.ready) {
try {
const data = await getHealthData({
sinceMillis: Date.now() - 24 * 60 * 60 * 1000,
untilMillis: Date.now(),
bucketPeriod: 'daily',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
bmr: 1500,
});
// Data fetch succeeded - permissions are effective
} catch (error) {
// Handle authorization failure: guide user to resetPermissions()
console.log('Authorization error:', error);
}
} else if (result.errorCode === 'NATIVE_ERROR' && result.nextAction === 'OPEN_SETTINGS') {
// Native error requires Settings entry (e.g., authorizationDenied)
await resetPermissions();
}
}iOS connect() result mapping:
| Input / Outcome | Prompt Behavior | ready | hasPermissions | permissionStatus | nextAction | Error Fields |
|-----------------|-----------------|--------:|-----------------|------------------|--------------|--------------|
| requestPermission: false and HealthKit available | No prompt | false | "unknown" | "unknown" | "GRANT_PERMISSIONS" | absent |
| requestPermission: true with no native error | Prompt may appear | true | "unknown" | "unknown" | "NONE" | absent |
| requestPermission: true with authorizationDenied | Prompt attempted; denied/system error | false | "unknown" | "unknown" | "OPEN_SETTINGS" | errorCode: "NATIVE_ERROR" |
| requestPermission: true with authorizationNotRequested | Prompt attempted; no request | false | "unknown" | "unknown" | "GRANT_PERMISSIONS" | errorCode: "NATIVE_ERROR" |
| requestPermission: true with other native error | Prompt/system error | false | "unknown" | "unknown" | "NONE" | errorCode: "NATIVE_ERROR" |
resetPermissions()
Starts a platform reset/manage-permission action.
type ResetPermissionsResult = {
success: boolean;
supported: boolean;
platform?: 'android' | 'ios';
resetMode?: 'PROGRAMMATIC_REVOKE' | 'OPEN_SETTINGS' | 'OPEN_HEALTH_APP' | 'UNAVAILABLE';
didReset?: boolean;
openedTarget?: 'HEALTH_CONNECT_SETTINGS' | 'APPLE_HEALTH_APP' | 'APP_SETTINGS' | 'NONE';
verificationRequired?: boolean;
requiresAppRestart?: boolean;
nextAction?: 'RECHECK_PERMISSIONS' | 'OPEN_SETTINGS' | 'CHECK_ACCESS' | 'NONE';
message?: string;
userActionRequired?: boolean;
attemptedTargets?: Array<'APPLE_HEALTH_APP' | 'APP_SETTINGS'>;
};
const status = await resetPermissions();resetPermissions starts a platform-specific reset/manage-permission action. Android behavior is unchanged: it calls Health Connect's programmatic revoke path and returns didReset: true when the revoke request succeeds.
iOS cannot reset, revoke, grant, enable, or disable HealthKit permissions from the SDK. The iOS action is best-effort:
- first attempt: open Apple Health with the unofficial
x-apple-health://scheme; - fallback: open the app's official Settings page with
UIApplicationOpenSettingsURLString; - final fallback: resolve with
resetMode: 'UNAVAILABLE',openedTarget: 'NONE', and manual Health app instructions.
Consumer apps must always show the returned iOS message/instructions when userActionRequired is true, ask the user to navigate to Health app > profile picture > Apps > [This App], then let the user return and tap Check Access. nextAction: 'CHECK_ACCESS' means call connect({ requestPermission: false, onMissingApp: 'status' }); use getHealthData() when actual read verification is required because HealthKit read grants are not fully introspectable.
getHealthData(options)
Reads normalized exercise data for a caller-selected absolute timestamp range.
type GetHealthDataOptions = {
sinceMillis: number;
untilMillis: number;
bucketPeriod: 'hourly' | 'daily';
timeZone: string;
bmr: number;
};
type ValueProvenance =
| 'platform_measured'
| 'platform_inferred'
| 'bmr_estimate'
| 'total_minus_basal_estimate'
| 'active_calorie_duration_estimate'
| 'none';
type ReadOutcome = 'succeeded' | 'partial' | 'denied' | 'unknown';
type EmptyReason =
| 'no_data'
| 'permission_denied'
| 'partial_permission'
| 'filtered_out'
| 'unknown';
type HealthDataBucket = {
steps: number;
totalCalories: number;
totalCaloriesUnit: string;
activeCalories: number;
activeCaloriesUnit: string;
activeCaloriesSource: 'device' | 'app' | 'manual' | 'unknown';
provenance: {
activeCalories: ValueProvenance;
basalCalories: ValueProvenance;
activeTime: ValueProvenance;
};
basalCalories: number;
basalCaloriesUnit: string;
distance: number;
distanceUnit: string;
activeTime: number;
activeTimeUnit: string;
startDate: string;
endDate: string;
};
type HealthDataResult = {
summary: {
steps: number;
activeKcal: number;
distanceMeters: number;
activeTimeMillis: number;
};
meta: {
sinceMillis: number;
untilMillis: number;
timeZone: string;
bucketPeriod: 'hourly' | 'daily';
bucketCount: number;
units: {
calories: 'kcal';
distance: 'km';
activeTime: 'milliseconds';
};
readOutcome: ReadOutcome;
emptyReason?: EmptyReason;
};
records: HealthDataBucket[];
debug?: {
raw?: {
sampleCount: number;
workoutCount: number;
samples: Array<{
metric: string;
value: number;
startDate: string;
endDate: string;
sourceName: string;
sourceBundleId?: string | null;
isManual: boolean;
}>;
workouts: Array<{
startDate: string;
endDate: string;
sourceName: string;
sourceBundleId?: string | null;
isManual: boolean;
}>;
};
};
};Timestamp contract:
sinceMillisanduntilMillisare Unix epoch milliseconds.- The range is
[sinceMillis, untilMillis):sinceMillisis inclusive anduntilMillisis exclusive. untilMillismust be greater thansinceMillis.- Epoch seconds,
NaN,Infinity, invalid timestamps, invalid time zones, invalidbmr, and default-mode ranges over 30 days are rejected. - An exact 30-day range is accepted.
timeZoneis used only for bucket boundaries and date formatting; absolute timestamps are not reinterpreted as local time.
Periods longer than 30 days reject in default mode with error code
E_INVALID_PERIOD.
Health Connect also applies runtime read quotas, with stricter limits for background work. The SDK keeps a short-lived exact-query cache for successful
getHealthData()responses, so repeated identical refreshes within 15 seconds can reuse the last result instead of re-reading Health Connect.
To further reduce quota pressure, the Android reader now paces internal Health Connect sub-reads, including paginated reads and retry attempts, and performs one fixed 5-second cooldown retry when Health Connect reports a rate-limit error. If the retry still fails, the SDK surfaces the existing
E_GET_EXERCISE_DATAerror path.
To maximize cache hits and avoid unnecessary quota use, keep
sinceMillisanduntilMillisstable within a refresh cycle instead of shifting the window on every poll, and let your scheduler or caller back off repeated background sync attempts. A full changelog-based sync strategy is out of scope for this SDK patch.
Preferred response contract:
summary.activeKcalis the active-calorie total for the full query window.summary.distanceMetersandsummary.activeTimeMillisare full-window totals. Bucketdistanceremains kilometers for backward compatibility; summary distance is meters.summaryis always calculated fromrecords[]: steps, active kcal, distance meters, and active time are sums of the emitted buckets.meta.sinceMillis,meta.untilMillis,meta.timeZone,meta.bucketPeriod,meta.bucketCount,meta.units,meta.readOutcome, and optionalmeta.emptyReasondescribe the response shared by every bucket.meta.bucketCount === records.length.recordsis the hourly or daily bucket-by-bucket array returned for the requestedbucketPeriod. Empty buckets are emitted with zero values.records[*].provenancetells consumers whetheractiveCalories,basalCalories, andactiveTimeare measured, inferred, estimated, or absent for that bucket.debugis an optional output-only field. When native returns it, the supported public shape isdebug.rawwith raw sample/workout counts and capped raw sample/workout previews. There is nodebugrequest option.
Recommended field usage:
- Use
summaryfor totals across the full query window. - Use
recordsfor charts, timelines, and bucket-by-bucket UI. - Use
metafor shared response metadata such as range echo fields,timeZone,bucketPeriod,bucketCount, units,readOutcome, andemptyReason. - Use bucket
provenancefor product labels around measured, inferred, estimated, or absent values. Summary totals can mix provenance across buckets; bucket provenance is authoritative. - Treat
debug?.rawas optional diagnostics output; do not use it for product empty-result logic or require it for normal app flows.
setAllowDatasource(config)
Configures which data sources to exclude from results.
type DatasourceType = 'manual' | 'device' | 'app' | 'unknown';
type DatasourceConfig = {
blacklist: DatasourceType[];
};
// Example: exclude manually-entered data
await setAllowDatasource({ blacklist: ['manual'] });getAllowDatasource()
Returns the current datasource blacklist configuration.
const config = await getAllowDatasource();
// => { blacklist: ['manual'] }Troubleshooting
Error code 5: HKErrorCodeAuthorizationDenied (iOS)
This error occurs when HealthKit authorization is denied. Common causes and solutions:
| Cause | Solution |
|-------|----------|
| Missing NSHealthShareUsageDescription in Info.plist | Add the key with a usage description string |
| HealthKit capability not enabled | Enable HealthKit in Xcode project settings |
| User previously denied permissions | Settings > Health > Data Access & Devices > [Your App] > enable permissions |
| App not properly signed | Verify provisioning profile includes HealthKit entitlements |
connect() fails with an error
- Android: Ensure Health Connect is installed (Android 13 and below) or available via Play Services (Android 14+)
- iOS: Verify HealthKit is enabled in Xcode and the app is running on a physical device
Bucket Record Schema
Use data.records for per-bucket results:
type UnifiedIntervalRecord = {
steps: number;
activeCalories: number;
activeCaloriesUnit: 'kcal';
activeCaloriesSource: 'device' | 'app' | 'manual' | 'unknown';
provenance: {
activeCalories: ValueProvenance;
basalCalories: ValueProvenance;
activeTime: ValueProvenance;
};
basalCalories: number;
basalCaloriesUnit: 'kcal';
distance: number;
distanceUnit: 'km';
activeTime: number;
activeTimeUnit: 'milliseconds';
startDate: string; // ISO offset string, e.g. "2026-02-16T08:00:00+07:00"
endDate: string;
};records are grouped by the requested bucketPeriod. The first and last record may be partial buckets when sinceMillis or untilMillis land mid-hour or mid-day.
Bucket behavior:
- Bucket boundaries are built from
sinceMillis,untilMillis,bucketPeriod, andtimeZone. - Platform queries use the absolute
[sinceMillis, untilMillis)range. - Bucket boundaries use local time in the requested
timeZone, including DST transitions; a local day can therefore have 23, 24, or 25 hourly buckets where the time zone observes DST. - A full-day
Asia/Bangkokhourly range produces 24 buckets. - A 30-day hourly range produces about 720 buckets, subject to DST in the selected
timeZone. - Records or platform aggregate intervals that cross request or bucket boundaries are clipped/split locally.
- Data exactly at
untilMillisis outside the range.
activeCaloriesSource classifies the datasource (device, app, manual, or unknown). It is not value provenance. Use record.provenance to distinguish measured, inferred, estimated, and absent values:
platform_measured: direct platform sample for the metric.platform_inferred: inferred from a platform interval such as an exercise session or HealthKit workout.bmr_estimate: calculated from caller-suppliedbmr.total_minus_basal_estimate: active calories estimated from total calories minus basal calories.active_calorie_duration_estimate: active time estimated from active-calorie record duration.none: no contributing source for that bucket field.
Empty or incomplete reads are represented on data.meta:
readOutcome: 'succeeded': the read completed for the platform-known scope.readOutcome: 'partial': returned data may be incomplete, for example because permissions are partial or a platform query partially failed.readOutcome: 'denied': Android detected missing Health Connect read permission.readOutcome: 'unknown': the platform cannot safely distinguish the outcome, including ambiguous iOS HealthKit empty reads.emptyReasonis present only for effectively empty results and can beno_data,permission_denied,partial_permission,filtered_out, orunknown.
data.meta.units still exposes the shared units once per response:
{
calories: 'kcal',
distance: 'km',
activeTime: 'milliseconds'
}The current record shape still includes per-record unit fields and basalCalories for compatibility with existing consumers.
basalCalories reflects the bucketed basal energy for that same interval. In the current Phase 2 contract, callers provide bmr in kcal/day and should treat detailed estimation provenance as a later contract phase.
Example record:
{
"steps": 1300,
"activeCalories": 60.2,
"activeCaloriesUnit": "kcal",
"activeCaloriesSource": "device",
"basalCalories": 42.5,
"basalCaloriesUnit": "kcal",
"distance": 1.0,
"distanceUnit": "km",
"activeTime": 1200000,
"activeTimeUnit": "milliseconds",
"startDate": "2026-02-16T08:00:00+07:00",
"endDate": "2026-02-16T09:00:00+07:00"
}Overlap Handling
The SDK automatically de-duplicates overlapping intervals before assigning active time to buckets. Overlapping intervals such as 09:00–10:00 plus 09:30–10:30 count as 90 minutes, not 120 minutes. Back-to-back intervals do not add overlap.
Default reads are aggregate/statistics-first so a 30-day hourly request does not raw-read every quantity sample or send raw platform records over the React Native bridge.
| Platform | Quantity metrics | Active time | Risk control |
|----------|------------------|-------------|--------------|
| Android Health Connect | Aggregate/bucket APIs for steps, distance, and active calories where possible. | Exercise session intervals are clipped, unioned, and split into SDK buckets. | Raw quantity reads are reserved for source-aware filtering. Summary is derived from emitted buckets only. |
| iOS HealthKit | HKStatisticsCollectionQuery cumulative sums for stepCount, distanceWalkingRunning, and activeEnergyBurned. | HKWorkout intervals are clipped, unioned, and split into SDK buckets. | HealthKit read permission certainty is not claimed; ambiguous empty iOS reads remain readOutcome: 'unknown' and emptyReason: 'unknown'. |
Distance is returned on both platforms: bucket records[].distance is kilometers and summary.distanceMeters is meters. iOS distance comes from HealthKit distanceWalkingRunning.
If datasource/source filtering is configured, the SDK may switch to a slower source-aware path because aggregate/statistics APIs do not always expose enough source metadata. Source-aware raw-sample fallback can have platform-specific dedupe semantics; do not treat it as the performance baseline for normal unfiltered 30-day reads.
Calories Fallback (Total - Basal)
When direct active calories are unavailable or lower quality in some windows, the SDK can derive fallback active calories:
active = max(totalCalories - basalCalories, 0)
Platform Behavior:
Android (Health Connect):
- Basal calories are calculated from BMR supplied via
bmrparameter:bmr × (record duration / day) - If no total calories record exists, basal defaults to 0 (proportional to data coverage)
iOS (HealthKit):
- Basal calories prefer actual
HKQuantityTypeIdentifierBasalEnergyBurneddata when available - Falls back to BMR calculation when no resting energy data exists:
bmr × (bucket duration / day) - This provides more accurate basal values from Apple Watch measurements on iOS
- Direct active-calorie measurement is preferred when present; fallback fills only uncovered windows.
- Merge remains deterministic and avoids double counting across atomic windows.
Optional Debug Output
getHealthData() does not accept a debug request option. Some native debug builds may include optional data.debug.raw output:
type HealthDataDebug = {
raw?: {
sampleCount: number;
workoutCount: number;
samples: RawHealthDataSample[];
workouts: RawHealthDataWorkout[];
};
};Treat debug?.raw as optional diagnostics output. Do not require it for normal app flows, and do not branch product UX on its presence.
Examples
Normal mode
import {
connect,
getHealthData,
setAllowDatasource,
} from '@cariva/vislife-health-sdk-rn';
async function loadHealthData() {
// Exclude manual entries
await setAllowDatasource({ blacklist: ['manual'] });
// Connect with minimal permission scope
const status = await connect({
onMissingApp: 'status',
requestPermission: true,
permissionScope: ['steps', 'calories'],
});
if (!status.ready) {
console.warn('Health Connect not ready. Next action:', status.nextAction);
return null;
}
// Query last 24 hours in hourly buckets
const data = await getHealthData({
sinceMillis: Date.now() - 24 * 60 * 60 * 1000,
untilMillis: Date.now(),
bucketPeriod: 'hourly',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
bmr: 1500,
});
console.log(data.summary.activeKcal);
console.log(data.meta.readOutcome, data.meta.emptyReason);
console.log(data.records[0]?.provenance);
return data;
}Silent status check
const status = await connect({
requestPermission: false,
onMissingApp: 'status',
});
if (status.nextAction === 'GRANT_PERMISSIONS') {
// Show your own UI affordance, then call connect({ requestPermission: true }).
}Optional debug output
const data = await getHealthData({
sinceMillis: Date.now() - 24 * 60 * 60 * 1000,
untilMillis: Date.now(),
bucketPeriod: 'hourly',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
bmr: 1500,
});
console.log(data.debug?.raw?.sampleCount);
console.log(data.debug?.raw?.workoutCount);Migration (2.0.0)
This release introduces a clean-break connect contract:
- Removed
ConnectResult.googleFitInstalled - Removed
nextAction: 'INSTALL_GOOGLE_FIT' connectreadiness flow is now Health Connect-only
If you previously handled Google Fit branches, delete those checks and rely on:
INSTALL_HEALTH_CONNECTUPDATE_HEALTH_CONNECTGRANT_PERMISSIONSNONE
Additive migration notes for getHealthData
- Prefer
summary+meta+recordsfor new integrations. summary.activeKcalis the explicit active-calorie total and corresponds to the legacytotalKcal.includeEmptyBuckets: falsenow filters the projectedrecordsview only; totals still come from the unfiltered aggregation result.recordscontinue to use the existingUnifiedIntervalRecordschema in this release.notices, legacy top-level totals,warnings,integrity, anddiagnosticsnow appear only whendebug: trueis enabled.
Fixed
- Double-count bug in
generateDayToDayQueryRequests: Multi-day requests now split the range into consecutive non-overlapping ≤24h chunks. The previous implementation added all per-day chunks then unconditionally re-added the full original range, causing every day to be bucketed twice for multi-day queries.
