Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The Synheart runtime computes three daily scores:
ScoreWhat it answers
Sleep”How good was last night’s sleep?”
Recovery”How recovered is the user today?”
Readiness”How much strain should the user take today?”
Each scorer takes a typed input bundle, returns a typed result, and emits a JSON shape that’s byte-equivalent across Flutter / Kotlin / Swift. The Synheart Core SDKs expose Dart / Kotlin / Swift data classes that round-trip with the runtime’s wire format.

Sleep score

Three input shapes — pick the one matching your data source:
  • Segmented: stage-by-stage timeline (Apple HealthKit, some Wear OS sources, BLE chest straps)
  • Aggregated: totals only (vendor APIs that don’t expose stages — Whoop summary, basic Health Connect, self-report)
  • Vendor score: a single 0-100 number from a vendor that has its own pipeline (Whoop Recovery, Garmin Body Battery)

Kotlin

val input = SleepScoreInput(
    tonight = NightRaw(
        wakeCalendarDate = epochDayForWake,
        detail = NightInput.Aggregated(
            sessionStartMs = bedtime.toEpochMilli(),
            sessionEndMs = wakeTime.toEpochMilli(),
            totals = AggregatedTotals(
                totalSleepMinutes = 420.0,
                awakeMinutes = 30.0,
                deepSleepMinutes = 90.0,  // null when source can't break stages out
                remSleepMinutes  = 110.0,
            ),
        ),
        avgHrBpm = 58.0,
    ),
    priorsNewestFirst = recentNights,
    pipelineVersion = "v1",
)
// Engine returns wire JSON; parse with SleepScoreResult.fromJsonString

Swift

let input = SleepScoreInput(
    tonight: NightRaw(
        wakeCalendarDate: epochDayForWake,
        detail: .aggregated(
            sessionStartMs: Int64(bedtime.timeIntervalSince1970 * 1000),
            sessionEndMs: Int64(wakeTime.timeIntervalSince1970 * 1000),
            totals: AggregatedTotals(
                totalSleepMinutes: 420,
                deepSleepMinutes: 90,  // nil when source can't break stages out
                remSleepMinutes: 110,
                awakeMinutes: 30
            )
        ),
        avgHrBpm: 58
    ),
    priorsNewestFirst: recentNights,
    pipelineVersion: "v1"
)
SleepScoreResult carries score: Int?, confidence: Double, path: SleepPath (stage / aggregated / vendor_score / proxy), mode: SleepScoreMode (cold_start / short_history / stable), components, adjustments, effectiveWeights, and a reason: SleepScoreReason? for absent/degraded scores.

Recovery score

Three-stage scorer — the engine adapts the formula to the available history:
  • Stage 1 (FirstDay) — 1 night of sleep + (HR or HRV)
  • Stage 2 (ShortHistory) — ≥ 3 nights with HR/HRV trends
  • Stage 3 (Personalized) — ≥ 7 nights + stable wearable baselines
// Kotlin
val input = RecoveryScoreInput(
    tonight = NightSummary(wakeCalendarDate = today, totalSleepMinutes = 420.0),
    priors = recentNights,
    overnight = OvernightPhysiology(hrvRmssdMs = 42.5, overnightHrBpm = 58.0),
    priorsOvernight = recentOvernight,
    baselines = stage3Baselines,  // null until ≥ 7 nights
)
RecoveryScoreResult carries score: Int, stage (first_day / short_history / personalized), mode (estimate / trended / personalized), components, confidence, and a List<RecoveryFactor> explaining the score (hrv_above_baseline, resting_hr_elevated, strong_sleep_quality, etc.).

Readiness score

Layers acute load / fatigue / history context on top of today’s Recovery anchor:
// Kotlin
val input = ReadinessScoreInput(
    recoveryScore = 65,
    recoveryConfidence = 0.85,
    acuteWorkload = AcuteWorkloadContext(acuteChronicRatio = 1.1),
    fatigue = FatigueContext(sleepDebtHours = 2.5, recoverySlopePerDay = -3.0),
    history = HistoryContext(consecutiveHighStrainDays = 2, daysSinceRest = 5),
)
// or minimal:
val minimal = ReadinessScoreInput.fromRecovery(72)
ReadinessScoreResult carries score: Int, band: ReadinessBand (.rest / .light / .normal / .push), recoveryAnchor: Int, components, confidence, and a List<ReadinessFactor> (strong_recovery, acute_load_optimal, sleep_debt, etc.).

Sleep questionnaire

Phase-2 self-report input lane — converts subjective answers into the same AggregatedTotals shape vendor payloads produce, so the engine scores them without special-casing.
// Kotlin
val q = SleepQuestionnaireAnswers(
    bedtime = Instant.parse("2026-01-15T22:00:00Z"),
    wakeTime = Instant.parse("2026-01-16T06:00:00Z"),
    sleepLatencyMinutes = 20,
    awakenings = 4,
    subjectiveQuality = 4,
    feltRested = SleepFeltRested.YES,
)
val payload = q.toIngestPayload()
// → feed payload to Baselines.ingestVendorSleep equivalent
The engine accepts null deep/REM on the aggregated path and falls back to duration + continuity components — that’s deliberate. Don’t invent stage durations from subjective signals; surface subjective quality / rested-feeling in the UI alongside the score instead.

Cross-language parity

All three scorers emit identical snake_case JSON on every platform:
  • Flutter: lib/src/models/{sleep,recovery,readiness}_score.dart + sleep_questionnaire.dart
  • Kotlin: synheart-core/src/main/kotlin/ai/synheart/core/models/{SleepScore,RecoveryScore,ReadinessScore,SleepQuestionnaire}.kt
  • Swift: SynheartCore/Models/{SleepScore,RecoveryScore,ReadinessScore,SleepQuestionnaire}.swift
A Flutter, Android, and iOS app talking to the same backend produce byte-equivalent scorer payloads.