> ## 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.

# Scoring models

> Typed input + result classes for the Sleep / Recovery / Readiness scorers — cross-language byte-equivalent JSON.

## Overview

The Synheart runtime computes three daily scores:

| Score         | What 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

```kotlin theme={null}
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

```swift theme={null}
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 theme={null}
// 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 theme={null}
// 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 theme={null}
// 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.

## Related

* [Baselines](/synheart-core/baselines) — reactive snapshot of latest scores + 7-night recent-scores ring
* [Synheart Core overview](/synheart-core/overview)
* [HSI specification](/synheart-core/hsi-specification)
