diff --git a/README.md b/README.md index baa3c44..5e7f5b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Human Engine -> A system for analyzing training load, estimating athlete state, and supporting training decisions. +> A deterministic system for analyzing training load, estimating athlete state, and supporting training decisions. > > `signal -> load state + recovery state -> readiness -> decision` @@ -17,7 +17,7 @@ It is an engineering system designed to support decisions through explicit, repr - Stores raw source payloads for reproducibility - Builds daily load and recovery state - Calculates readiness and good-day probability -- Supports training load decisions +- Provides deterministic outputs for downstream decision support ## What the System Is @@ -52,12 +52,20 @@ The current backend already includes: - Docker deployment - Public API exposed through a VPS +Implemented model baseline: + +- `LoadState + RecoveryState -> Readiness -> GoodDayProbability` +- HealthKit full-sync endpoint `POST /api/v1/healthkit/full-sync/{user_id}` +- baseline-aware recovery scoring stored in `health_recovery_daily` +- explanation payloads for recovery and readiness +- deterministic storage-backed daily layers for load, recovery, and readiness + ### Current Focus - Deterministic core - Transparent logic - Reproducible results -- Stabilization of model v2 and downstream decision outputs +- Stabilization of model v2 baseline and downstream decision outputs See: [docs/ai/CURRENT_PRIORITIES.md](docs/ai/CURRENT_PRIORITIES.md) @@ -140,6 +148,7 @@ docs/ system documentation - [docs/architecture/ARCHITECTURE.md](docs/architecture/ARCHITECTURE.md) - [backend/ROADMAP.md](backend/ROADMAP.md) - [docs/models/model_v2_architecture.md](docs/models/model_v2_architecture.md) +- [docs/product/CURRENT_STATE.md](docs/product/CURRENT_STATE.md) ### Product and AI Context @@ -155,15 +164,19 @@ docs/ system documentation - Readiness contour: `load_state_daily_v2 + health_recovery_daily -> readiness_daily` - `freshness = fitness - fatigue_total` - `fatigue_total` is a weighted mixture of `fatigue_fast` and `fatigue_slow` +- `recovery_score_simple` is currently produced by a baseline-aware recovery scoring layer - Readiness is not equal to freshness - `good_day_probability` is stored as a separate probability-like output +- `good_day_probability` is currently `readiness_score / 100`, not a statistically calibrated probability ## Short Roadmap Already implemented: - HealthKit ingestion and normalization +- HealthKit full-sync orchestration - Recovery daily aggregation +- Recovery explanation payload - Load model v2 - Readiness model v2 baseline - Good day probability baseline @@ -173,6 +186,7 @@ Next: - activity streams ingestion - feature extraction expansion - readiness / probability calibration +- decision layer / recommendation layer - prediction engine - iOS client integration polish @@ -187,4 +201,4 @@ Next: ## Status -Experimental engineering project with a deterministic product core and an implemented model v2 baseline in backend. +Experimental engineering project with a deterministic product core and an implemented Model V2 baseline in backend. diff --git a/backend/README.md b/backend/README.md index c0d4952..5ed46ae 100644 --- a/backend/README.md +++ b/backend/README.md @@ -98,6 +98,23 @@ FastAPI + PostgreSQL - HRV daily median - latest known weight - `recovery_score_simple` +- `recovery_explanation_json` + +Текущая recovery baseline-логика: + +- использует baseline-aware scoring +- считает `hrv_baseline` и `rhr_baseline` по предыдущему окну +- считает `hrv_dev` и `rhr_dev` +- считает component scores: + - `sleep_score` + - `hrv_score` + - `rhr_score` +- сохраняет breakdown в `recovery_explanation_json` + +Важно: + +- поле по-прежнему называется `recovery_score_simple` для совместимости схемы и API +- по смыслу это уже не purely naive heuristic-only score ### 5. Load model v2 @@ -116,6 +133,12 @@ FastAPI + PostgreSQL - хранит `fatigue_total` как взвешенную смесь fast/slow fatigue - хранит `freshness = fitness - fatigue_total` +Параметры: + +- `tau_fitness = 40` +- `tau_fatigue_fast = 4` +- `tau_fatigue_slow = 9` + ### 6. Readiness layer Реализована таблица: @@ -133,6 +156,14 @@ FastAPI + PostgreSQL - сохраняет `status_text` - сохраняет `explanation_json` +Важно: + +- readiness хранится отдельно от `load_state_daily_v2` +- readiness не равен `freshness` +- текущий `good_day_probability` является baseline probability-like mapping: + - `good_day_probability = readiness_score / 100` + - это не статистически откалиброванная вероятность + ## HealthKit full sync pipeline Текущий orchestration pipeline: @@ -150,7 +181,7 @@ POST /api/v1/healthkit/full-sync/{user_id} - recovery пересчитывается поверх normalized health tables - readiness пересчитывается как отдельный слой -- readiness больше не является просто полем внутри load state +- public API уже работает end-to-end через VPS и Caddy ## Технологический стек @@ -192,7 +223,9 @@ docker compose стек для сервера. Уже реализовано: - HealthKit ingestion и normalization +- HealthKit full-sync orchestration - recovery daily aggregation +- recovery explanation payload - load model v2 baseline - readiness baseline - good day probability baseline @@ -202,6 +235,7 @@ docker compose стек для сервера. - activity streams ingestion - расширение feature extraction - калибровка readiness / probability +- decision layer / recommendation layer - API и UI для user-facing insights - iOS integration polish diff --git a/backend/ROADMAP.md b/backend/ROADMAP.md index b53d7f9..e4f0f15 100644 --- a/backend/ROADMAP.md +++ b/backend/ROADMAP.md @@ -4,18 +4,22 @@ - Strava ingestion baseline - HealthKit raw ingestion +- HealthKit full-sync orchestration - HealthKit normalized tables - `health_recovery_daily` +- baseline-aware recovery scoring +- `recovery_explanation_json` - `load_state_daily_v2` - `readiness_daily` - `good_day_probability` baseline -- HealthKit full-sync orchestration +- end-to-end HealthKit -> recovery -> readiness pipeline ## Next steps - activity streams ingestion - feature extraction expansion - readiness / probability calibration +- personalization - decision outputs and ride briefing integration - performance / prediction model - iOS client integration polish diff --git a/backend/backend/services/health_recovery_daily.py b/backend/backend/services/health_recovery_daily.py index ba80544..4a18add 100644 --- a/backend/backend/services/health_recovery_daily.py +++ b/backend/backend/services/health_recovery_daily.py @@ -6,14 +6,29 @@ from backend.db import get_conn +import json + + +def _clamp(value: float, low: float, high: float) -> float: + """Ограничивает значение диапазоном [low, high].""" + return max(low, min(high, value)) + def _compute_recovery_score_simple( sleep_minutes: float | None, resting_hr_bpm: float | None, hrv_daily_median_ms: float | None, ) -> float | None: - # Простейшая baseline-free версия recovery score. - # Это временный heuristic score, не финальная модель. + """ + Простейшая baseline-free версия recovery score. + + Это временный heuristic score: + - сон выше -> лучше + - resting HR ниже -> лучше + - HRV выше -> лучше + + Шкала результата: 0..100. + """ if sleep_minutes is None and resting_hr_bpm is None and hrv_daily_median_ms is None: return None @@ -31,13 +46,93 @@ def _compute_recovery_score_simple( # Чем выше HRV, тем лучше, без baseline пока очень грубо. score += min(15.0, max(-15.0, (hrv_daily_median_ms - 50.0) * 0.5)) - return max(0.0, min(100.0, round(score, 1))) + return _clamp(round(score, 1), 0.0, 100.0) + + +def _compute_recovery_score_with_baseline( + sleep_minutes: float | None, + hrv_today: float | None, + rhr_today: float | None, + hrv_baseline: float | None, + rhr_baseline: float | None, +) -> tuple[float, dict[str, float | None]]: + """ + Более продвинутая версия recovery score с baseline. + + Возвращает: + - recovery_score + - explanation dict с breakdown по компонентам + """ + hrv_dev = None + rhr_dev = None + + if hrv_today is not None and hrv_baseline not in (None, 0): + hrv_dev = (hrv_today - hrv_baseline) / hrv_baseline + hrv_score = 50.0 + 50.0 * hrv_dev + else: + hrv_score = 50.0 + + if rhr_today is not None and rhr_baseline not in (None, 0): + rhr_dev = (rhr_today - rhr_baseline) / rhr_baseline + rhr_score = 50.0 - 50.0 * rhr_dev + else: + rhr_score = 50.0 + + if sleep_minutes is not None: + sleep_score = min(sleep_minutes / 480.0, 1.0) * 100.0 + else: + sleep_score = 50.0 + + hrv_score = _clamp(hrv_score, 0.0, 100.0) + rhr_score = _clamp(rhr_score, 0.0, 100.0) + sleep_score = _clamp(sleep_score, 0.0, 100.0) + + recovery_score = ( + 0.4 * hrv_score + + 0.3 * rhr_score + + 0.3 * sleep_score + ) + recovery_score = round(_clamp(recovery_score, 0.0, 100.0), 1) + + explanation = { + "method": "baseline_v2", + "sleep_minutes": sleep_minutes, + "hrv_today": hrv_today, + "rhr_today": rhr_today, + "hrv_baseline": hrv_baseline, + "rhr_baseline": rhr_baseline, + "hrv_dev": round(hrv_dev, 4) if hrv_dev is not None else None, + "rhr_dev": round(rhr_dev, 4) if rhr_dev is not None else None, + "sleep_score": round(sleep_score, 1), + "hrv_score": round(hrv_score, 1), + "rhr_score": round(rhr_score, 1), + "weights": { + "hrv_score": 0.4, + "rhr_score": 0.3, + "sleep_score": 0.3, + }, + "recovery_score_simple": recovery_score, + } + + return recovery_score, explanation def recompute_health_recovery_daily_for_date(user_id: str, target_date: str) -> dict[str, Any]: + """ + Пересчитывает daily recovery snapshot для заданного пользователя и даты. + + Источники: + - health_sleep_night + - health_resting_hr_daily + - health_hrv_sample + - health_weight_measurement + + Результат сохраняется в: + - health_recovery_daily + """ with get_conn() as conn: with conn.cursor() as cur: - # Sleep for target_date + # 1. Забираем sleep aggregate для target_date. cur.execute( """ select @@ -53,7 +148,7 @@ def recompute_health_recovery_daily_for_date(user_id: str, target_date: str) -> ) sleep_row = cur.fetchone() - # Resting HR for target_date + # 2. Забираем resting HR за target_date. cur.execute( """ select bpm @@ -65,7 +160,7 @@ def recompute_health_recovery_daily_for_date(user_id: str, target_date: str) -> ) resting_hr_row = cur.fetchone() - # HRV median for target_date + # 3. Считаем median HRV за target_date. cur.execute( """ select percentile_cont(0.5) within group (order by value_ms) @@ -77,7 +172,7 @@ def recompute_health_recovery_daily_for_date(user_id: str, target_date: str) -> ) hrv_row = cur.fetchone() - # Latest weight on or before target_date + # 4. Забираем последний известный вес на target_date или раньше. cur.execute( """ select kilograms @@ -91,6 +186,7 @@ def recompute_health_recovery_daily_for_date(user_id: str, target_date: str) -> ) weight_row = cur.fetchone() + # 5. Собираем daily values. sleep_minutes = None awake_minutes = None rem_minutes = None @@ -103,6 +199,7 @@ def recompute_health_recovery_daily_for_date(user_id: str, target_date: str) -> hrv_daily_median_ms = hrv_row[0] if hrv_row and hrv_row[0] is not None else None weight_kg = weight_row[0] if weight_row else None + # 6. Если вообще нет health data, отдаём 404. if ( sleep_minutes is None and resting_hr_bpm is None @@ -114,56 +211,111 @@ def recompute_health_recovery_daily_for_date(user_id: str, target_date: str) -> detail=f"no health data found for user_id={user_id} date={target_date}", ) - recovery_score_simple = _compute_recovery_score_simple( - sleep_minutes=sleep_minutes, - resting_hr_bpm=resting_hr_bpm, - hrv_daily_median_ms=hrv_daily_median_ms, + # 7. Baseline HRV: median по предыдущему окну, исключая target_date. + cur.execute( + """ + select percentile_cont(0.5) within group (order by value_ms) + from health_hrv_sample + where user_id = %s + and sample_start_at::date < %s::date + and sample_start_at::date >= (%s::date - interval '14 days'); + """, + (user_id, target_date, target_date), ) + hrv_baseline_row = cur.fetchone() + hrv_baseline = hrv_baseline_row[0] if hrv_baseline_row and hrv_baseline_row[0] is not None else None + # 8. Baseline resting HR: median по предыдущему окну, исключая target_date. cur.execute( """ - insert into health_recovery_daily ( - user_id, - date, - sleep_minutes, - awake_minutes, - rem_minutes, - deep_minutes, - resting_hr_bpm, - hrv_daily_median_ms, - weight_kg, - recovery_score_simple, - updated_at - ) - values ( - %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, now() - ) - on conflict (user_id, date) do update set - sleep_minutes = excluded.sleep_minutes, - awake_minutes = excluded.awake_minutes, - rem_minutes = excluded.rem_minutes, - deep_minutes = excluded.deep_minutes, - resting_hr_bpm = excluded.resting_hr_bpm, - hrv_daily_median_ms = excluded.hrv_daily_median_ms, - weight_kg = excluded.weight_kg, - recovery_score_simple = excluded.recovery_score_simple, - updated_at = now(); + select percentile_cont(0.5) within group (order by bpm) + from health_resting_hr_daily + where user_id = %s + and date < %s::date + and date >= (%s::date - interval '14 days'); """, - ( - user_id, - target_date, - sleep_minutes, - awake_minutes, - rem_minutes, - deep_minutes, - resting_hr_bpm, - hrv_daily_median_ms, - weight_kg, - recovery_score_simple, - ), + (user_id, target_date, target_date), + ) + rhr_baseline_row = cur.fetchone() + rhr_baseline = rhr_baseline_row[0] if rhr_baseline_row and rhr_baseline_row[0] is not None else None + + # 9. Считаем recovery score. + # Пока сохраняем его в колонку recovery_score_simple для совместимости схемы и API. + recovery_score_simple, recovery_explanation = _compute_recovery_score_with_baseline( + sleep_minutes=sleep_minutes, + hrv_today=hrv_daily_median_ms, + rhr_today=resting_hr_bpm, + hrv_baseline=hrv_baseline, + rhr_baseline=rhr_baseline, ) + + # Если по какой-то причине baseline-based score не получился, + # используем старый простой fallback. + if recovery_score_simple is None: + recovery_score_simple = _compute_recovery_score_simple( + sleep_minutes=sleep_minutes, + resting_hr_bpm=resting_hr_bpm, + hrv_daily_median_ms=hrv_daily_median_ms, + ) + recovery_explanation = { + "method": "simple_fallback", + "sleep_minutes": sleep_minutes, + "hrv_today": hrv_daily_median_ms, + "rhr_today": resting_hr_bpm, + "hrv_baseline": hrv_baseline, + "rhr_baseline": rhr_baseline, + "recovery_score_simple": recovery_score_simple, + } + + # 10. Upsert в daily recovery table. + cur.execute( + """ + insert into health_recovery_daily ( + user_id, + date, + sleep_minutes, + awake_minutes, + rem_minutes, + deep_minutes, + resting_hr_bpm, + hrv_daily_median_ms, + weight_kg, + recovery_score_simple, + recovery_explanation_json, + updated_at + ) + values ( + %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, now() + ) + on conflict (user_id, date) do update set + sleep_minutes = excluded.sleep_minutes, + awake_minutes = excluded.awake_minutes, + rem_minutes = excluded.rem_minutes, + deep_minutes = excluded.deep_minutes, + resting_hr_bpm = excluded.resting_hr_bpm, + hrv_daily_median_ms = excluded.hrv_daily_median_ms, + weight_kg = excluded.weight_kg, + recovery_score_simple = excluded.recovery_score_simple, + recovery_explanation_json = excluded.recovery_explanation_json, + updated_at = now(); + """, + ( + user_id, + target_date, + sleep_minutes, + awake_minutes, + rem_minutes, + deep_minutes, + resting_hr_bpm, + hrv_daily_median_ms, + weight_kg, + recovery_score_simple, + json.dumps(recovery_explanation), + ), +) conn.commit() + # 11. Возвращаем snapshot для API/debug. return { "ok": True, "user_id": user_id, @@ -173,4 +325,7 @@ def recompute_health_recovery_daily_for_date(user_id: str, target_date: str) -> "hrv_daily_median_ms": hrv_daily_median_ms, "weight_kg": weight_kg, "recovery_score_simple": recovery_score_simple, + "hrv_baseline": hrv_baseline, + "rhr_baseline": rhr_baseline, + "recovery_explanation": recovery_explanation, } \ No newline at end of file diff --git a/docs/ai/CURRENT_PRIORITIES.md b/docs/ai/CURRENT_PRIORITIES.md index 3af20c2..f6eda1d 100644 --- a/docs/ai/CURRENT_PRIORITIES.md +++ b/docs/ai/CURRENT_PRIORITIES.md @@ -4,13 +4,13 @@ Human Engine находится в фазе: -> стабилизация и упрощение системы +> стабилизация и согласование реализованного baseline Основной фокус: -- убрать лишнюю сложность -- сделать поведение системы предсказуемым -- зафиксировать архитектурные границы +- убрать устаревшие описания +- сделать поведение системы предсказуемым +- зафиксировать архитектурные границы --- @@ -22,43 +22,40 @@ Human Engine находится в фазе: Приоритет: -- deterministic logic -- простота -- наблюдаемость +- deterministic logic +- простота +- наблюдаемость --- ## 3. What is priority NOW -### 3.1 Backend simplification +### 3.1 Backend stabilization -- удалить AI endpoints -- убрать зависимость от Ollama -- сохранить только core функциональность +- удержать deterministic core стабильным +- не размывать текущую architecture baseline +- улучшать только подтвержденные backend layers Backend должен выполнять: -- ingestion -- хранение данных -- deterministic расчеты -- API - ---- +- ingestion +- хранение данных +- deterministic расчеты +- API ### 3.2 Deterministic core Критически важно: -- readiness logic должна быть явной -- ride briefing должен быть детерминированным -- никакой скрытой логики +- readiness logic должна быть явной +- recovery scoring должен оставаться прозрачным +- ride briefing, если появится, должен быть детерминированным +- никакой скрытой логики Нельзя: -- переносить логику в LLM -- заменять формулы текстом - ---- +- переносить логику в LLM +- заменять формулы текстом ### 3.3 Architecture boundaries @@ -66,14 +63,14 @@ Backend должен выполнять: Core: -- backend -- postgres -- доменная логика +- backend +- postgres +- доменная логика AI: -- отдельный слой -- не влияет на расчеты +- отдельный слой +- не влияет на расчеты --- @@ -85,15 +82,15 @@ RAG рассматривается как: ### Цели: -- навигация по коду -- ответы по документации -- ускорение разработки +- навигация по коду +- ответы по документации +- ускорение разработки ### Ограничения: -- не интегрировать в backend -- не использовать для принятия решений -- не делать user-facing feature +- не интегрировать в backend +- не использовать для принятия решений +- не делать user-facing feature --- @@ -103,15 +100,14 @@ RAG рассматривается как: Используется как инструмент: -- анализ кода -- генерация изменений -- помощь в документации +- анализ кода +- генерация изменений +- помощь в документации Но: -- без прямых коммитов -- только через PR -- с обязательной проверкой +- с обязательной проверкой +- без внедрения AI в deterministic core --- @@ -123,10 +119,10 @@ RAG рассматривается как: Обязательно фиксировать: -- продуктовый контекст -- архитектуру -- модели -- решения +- продуктовый контекст +- архитектуру +- модели +- решения --- @@ -134,15 +130,23 @@ RAG рассматривается как: ### Backend -- простые сервисы -- явные зависимости -- минимальная магия +- простые сервисы +- явные зависимости +- минимальная магия +- separate storage layers for load, recovery, readiness ### Data -- надежный ingestion -- хранение raw данных -- движение к воспроизводимости +- надежный ingestion +- хранение raw данных +- движение к воспроизводимости + +### Current modeling focus + +- стабилизация `load_state_daily_v2` +- стабилизация `health_recovery_daily` +- стабилизация `readiness_daily` +- readiness / probability calibration как следующий шаг, а не новый black-box слой --- @@ -152,16 +156,16 @@ RAG рассматривается как: Предпочитать: -1. простое решение вместо сложного -2. явную логику вместо скрытой -3. детерминированность вместо вероятности -4. локальные изменения вместо глобальных +1. простое решение вместо сложного +2. явную логику вместо скрытой +3. детерминированность вместо black-box вероятности +4. локальные изменения вместо глобальных Избегать: -- overengineering -- premature abstraction -- внедрения AI "потому что можно" +- overengineering +- premature abstraction +- внедрения AI "потому что можно" --- @@ -171,9 +175,9 @@ RAG рассматривается как: Прогресс — это: -- система стала понятнее -- логика стала прозрачнее -- поведение стало стабильнее +- система стала понятнее +- логика стала прозрачнее +- поведение стало стабильнее --- @@ -181,12 +185,12 @@ RAG рассматривается как: Система считается готовой к следующему этапу, когда: -- backend работает без AI зависимостей -- все ключевые расчеты детерминированы -- архитектура проста и объяснима -- документация отражает реальное состояние системы +- HealthKit full-sync стабильно проходит end-to-end +- load / recovery / readiness layers детерминированы и согласованы +- probability layer явно описан как baseline mapping +- документация отражает реальное состояние системы После этого можно: -- аккуратно возвращать AI -- рассматривать RAG как сервис \ No newline at end of file +- калибровать readiness / probability +- добавлять decision layer поверх текущего baseline diff --git a/docs/ai/GLOSSARY.md b/docs/ai/GLOSSARY.md index 5126f6b..63e9007 100644 --- a/docs/ai/GLOSSARY.md +++ b/docs/ai/GLOSSARY.md @@ -43,6 +43,8 @@ ### Feature Extraction Процесс преобразования raw и normalized data в daily features и derived state. +В текущем backend baseline расширенный отдельный feature layer еще не является центральным persisted слоем. Основные deterministic daily layers уже строятся напрямую из raw и normalized данных. + --- ## 2. Modeling layer @@ -82,6 +84,15 @@ Recovery не заменяет fatigue, а корректирует readiness поверх load model. +Текущий baseline recovery layer уже включает: + +- baseline-aware scoring +- `hrv_baseline` +- `rhr_baseline` +- `hrv_dev` +- `rhr_dev` +- explanation payload + --- ### Fitness / Fatigue @@ -125,6 +136,12 @@ Recovery не заменяет fatigue, а корректирует readiness п В текущем backend это отдельное поле `good_day_probability` в `readiness_daily`. +Важно: + +- сейчас это baseline probability-like mapping +- текущее вычисление: `good_day_probability = readiness_score / 100` +- это не статистически откалиброванная вероятность + --- ### Recommendation @@ -136,6 +153,8 @@ Recovery не заменяет fatigue, а корректирует readiness п - допустимую интенсивность - ограничения по дню +В текущем состоянии recommendation layer остается planned. + --- ### Ride Briefing @@ -147,6 +166,8 @@ Recovery не заменяет fatigue, а корректирует readiness п - стабильным - опираться на readiness layer, а не на свободный текст +В текущем состоянии ride briefing layer остается planned. + --- ## 4. System architecture @@ -162,7 +183,7 @@ Recovery не заменяет fatigue, а корректирует readiness п ### Pipeline Последовательность обработки данных: -`ingestion -> raw storage -> normalized data -> daily state -> readiness -> decision` +`ingestion -> raw storage -> normalized data -> load/recovery state -> readiness -> decision` --- @@ -221,7 +242,7 @@ Large Language Model. - сохранения raw данных - явной логики -- versioned derived layers +- versioned derived layers там, где это уже реализовано --- diff --git a/docs/ai/PRODUCT_CONTEXT.md b/docs/ai/PRODUCT_CONTEXT.md index 4282ef9..0a10718 100644 --- a/docs/ai/PRODUCT_CONTEXT.md +++ b/docs/ai/PRODUCT_CONTEXT.md @@ -6,10 +6,11 @@ Human Engine — система анализа тренировочных дан Система: -- собирает данные о тренировках -- оценивает физиологическое состояние -- рассчитывает readiness -- помогает выбрать нагрузку +- собирает данные о тренировках +- собирает recovery-сигналы +- оценивает физиологическое состояние +- рассчитывает readiness +- помогает выбрать нагрузку Ключевая идея: @@ -23,15 +24,15 @@ Human Engine — это не просто хранилище данных. Это: -- decision-support system -- physiology-driven model -- reproducible analytics pipeline +- decision-support system +- physiology-driven model +- reproducible analytics pipeline Система должна: -- давать объяснимые результаты -- работать детерминированно -- быть проверяемой +- давать объяснимые результаты +- работать детерминированно +- быть проверяемой --- @@ -39,11 +40,11 @@ Human Engine — это не просто хранилище данных. Human Engine не является: -- просто тренировочным логом -- визуальным дашбордом без логики -- AI-тренером -- black-box системой -- системой, где решения принимаются LLM +- просто тренировочным логом +- визуальным дашбордом без логики +- AI-тренером +- black-box системой +- системой, где решения принимаются LLM AI не должен становиться ядром системы. @@ -55,31 +56,33 @@ AI не должен становиться ядром системы. ### Deterministic core -- ingestion данных -- расчет метрик -- readiness logic -- prediction logic +- ingestion данных +- raw и normalized storage +- расчет метрик +- recovery logic +- readiness logic +- probability mapping Требования: -- явная логика -- воспроизводимость -- проверяемость +- явная логика +- воспроизводимость +- проверяемость ### AI layer (auxiliary) AI может использоваться для: -- генерации текста -- объяснения метрик -- навигации по документации -- помощи разработчику +- генерации текста +- объяснения метрик +- навигации по документации +- помощи разработчику AI не участвует в: -- расчете метрик -- принятии решений -- изменении доменной логики +- расчете метрик +- принятии решений +- изменении доменной логики --- @@ -87,42 +90,48 @@ AI не участвует в: Текущий этап: -- стабилизация backend -- упрощение архитектуры -- устранение AI из core -- формирование надежного data pipeline +- стабилизация deterministic backend baseline +- поддержание архитектуры Model V2 +- фиксация документации по реализованному состоянию +- формирование надежного end-to-end data pipeline Система должна сначала стать: -- корректной -- предсказуемой -- устойчивой +- корректной +- предсказуемой +- устойчивой и только потом — умной. --- -## 6. Data → Model → Decision flow +## 6. Data -> Model -> Decision flow Общий поток: Data ingestion ↓ -Data storage +Raw / normalized storage ↓ -Feature extraction -↓ -Physiology model +LoadState + RecoveryState ↓ Readiness ↓ -Recommendation +GoodDayProbability +↓ +Recommendation Этот поток должен оставаться: -- прозрачным -- трассируемым -- воспроизводимым +- прозрачным +- трассируемым +- воспроизводимым + +Текущее реализованное состояние: + +- HealthKit ingestion и full-sync уже работают в backend +- recovery и readiness уже materialized как отдельные daily layers +- decision / recommendation layer остается planned --- @@ -132,17 +141,17 @@ Recommendation Нельзя: -- заменять детерминированную логику LLM -- вводить скрытые вычисления -- смешивать AI и core-логику -- усложнять архитектуру без необходимости +- заменять детерминированную логику LLM +- вводить скрытые вычисления +- смешивать AI и core-логику +- усложнять архитектуру без необходимости Можно: -- упрощать -- делать явные модели -- улучшать наблюдаемость -- добавлять проверяемость +- упрощать +- делать явные модели +- улучшать наблюдаемость +- добавлять проверяемость --- @@ -150,11 +159,11 @@ Recommendation Любые изменения должны: -- сохранять deterministic core -- не нарушать архитектурные границы -- быть объяснимыми -- быть проверяемыми +- сохранять deterministic core +- не нарушать архитектурные границы +- быть объяснимыми +- быть проверяемыми Если возникает сомнение: -предпочтение всегда отдается простой и явной логике. \ No newline at end of file +предпочтение всегда отдается простой и явной логике. diff --git a/docs/ai/SYSTEM_MAP.md b/docs/ai/SYSTEM_MAP.md index 041bd42..bf8e6b4 100644 --- a/docs/ai/SYSTEM_MAP.md +++ b/docs/ai/SYSTEM_MAP.md @@ -4,7 +4,7 @@ Human Engine — система, которая: -> преобразует тренировочные и recovery-данные в решение о нагрузке +> преобразует тренировочные и recovery-данные в readiness и downstream decision support --- @@ -23,6 +23,8 @@ Load Model + Recovery Model ↓ Readiness Engine ↓ +Probability Layer +↓ Recommendation / Ride Briefing ↓ Workout Outcome @@ -151,7 +153,7 @@ Human Engine — это не один алгоритм. Это цепочка: -> данные -> состояние -> решение +> данные -> состояние -> readiness -> decision Если система дает неправильный результат, ошибка находится в одном из слоев: @@ -170,9 +172,11 @@ Human Engine — это не один алгоритм. - Strava ingestion pipeline - HealthKit ingestion pipeline +- HealthKit full-sync orchestration - raw data storage - normalized health layer - recovery daily layer +- baseline-aware recovery scoring - load state v2 - readiness daily - good day probability baseline diff --git a/docs/data/DATA_MODEL.md b/docs/data/DATA_MODEL.md index 37528d1..442af38 100644 --- a/docs/data/DATA_MODEL.md +++ b/docs/data/DATA_MODEL.md @@ -221,6 +221,13 @@ - `hrv_daily_median_ms` - `weight_kg` - `recovery_score_simple` +- `recovery_explanation_json` + +Комментарий: + +- поле `recovery_score_simple` исторически сохраняет имя для совместимости +- текущий backend считает его через baseline-aware scoring layer +- breakdown и baseline-компоненты сохраняются в `recovery_explanation_json` --- @@ -231,6 +238,7 @@ Load model v2. Источник: - `daily_training_load` +- календарный диапазон между training и recovery датами пользователя Свойства расчета: @@ -285,6 +293,10 @@ Load model v2. - `healthkit_ingest_raw -> health_hrv_sample` (1:N) - `healthkit_ingest_raw -> health_weight_measurement` (1:N) - `daily_training_load -> load_state_daily_v2` (N:1) +- `health_sleep_night -> health_recovery_daily` (N:1) +- `health_resting_hr_daily -> health_recovery_daily` (N:1) +- `health_hrv_sample -> health_recovery_daily` (N:1) +- `health_weight_measurement -> health_recovery_daily` (N:1) - `health_recovery_daily -> readiness_daily` (N:1) - `load_state_daily_v2 -> readiness_daily` (N:1) @@ -304,6 +316,11 @@ health_sleep_night / health_resting_hr_daily / health_hrv_sample / health_weight health_recovery_daily ``` +Комментарий: + +- `health_recovery_daily` materializes day-level recovery state +- `recovery_explanation_json` хранит breakdown текущего baseline scoring + ### 7.2 Load contour ```text @@ -316,6 +333,11 @@ daily_training_load load_state_daily_v2 ``` +Комментарий: + +- `load_state_daily_v2` materializes calendar-continuous load state +- для дней без тренировки используется `tss = 0` + ### 7.3 Readiness contour ```text @@ -324,6 +346,11 @@ load_state_daily_v2 + health_recovery_daily readiness_daily ``` +Комментарий: + +- readiness хранится отдельно от load layer +- `good_day_probability` является отдельным output внутри `readiness_daily` + --- ## 8. Reproducibility @@ -359,6 +386,11 @@ readiness_daily - `load_state_daily_v2` - `readiness_daily` +Текущая особенность: + +- `health_recovery_daily` пока не versioned отдельным полем +- для recovery breakdown используется `recovery_explanation_json` + Требование: - при изменении формул не ломать исторические расчеты @@ -379,6 +411,6 @@ readiness_daily - где хранить расширенные features - как делать массовый перерасчет -- как единообразно организовать versioning +- как единообразно организовать versioning recovery layer См. `docs/architecture/OPEN_DECISIONS.md`. diff --git a/docs/models/FEATURES.md b/docs/models/FEATURES.md index ea7bd40..ab39a67 100644 --- a/docs/models/FEATURES.md +++ b/docs/models/FEATURES.md @@ -6,23 +6,22 @@ Цель: -- определить, какие признаки извлекаются из raw данных -- зафиксировать их вычисление -- обеспечить воспроизводимость -- отделить raw данные от метрик +- определить, какие признаки извлекаются из raw данных +- зафиксировать их вычисление +- обеспечить воспроизводимость +- отделить raw данные от метрик --- ## 2. What is a feature -Feature — это производная характеристика данных, -используемая для расчета метрик и моделей. +Feature — это производная характеристика данных, используемая для расчета метрик и моделей. Пример: -- сглаженная мощность -- распределение по зонам -- вариативность нагрузки +- сглаженная мощность +- распределение по зонам +- вариативность нагрузки --- @@ -30,11 +29,12 @@ Feature — это производная характеристика данн Features находятся между: -- raw data -- metrics +- raw data +- metrics Поток: +```text Raw data ↓ Features @@ -42,6 +42,13 @@ Features Metrics ↓ Readiness +``` + +Текущее состояние: + +- базовые daily layers уже реализованы без отдельного расширенного persisted feature layer +- `daily_training_load`, normalized HealthKit tables, `health_recovery_daily`, `load_state_daily_v2` и `readiness_daily` уже работают как deterministic pipeline +- расширение feature extraction остается следующим этапом, а не обязательным условием текущего backend baseline --- @@ -49,102 +56,86 @@ Readiness Features должны быть: -- deterministic -- воспроизводимыми -- независимыми от AI -- вычисляемыми из raw данных +- deterministic +- воспроизводимыми +- независимыми от AI +- вычисляемыми из raw данных --- ## 5. Feature types ---- - ### 5.1 Activity-level features Рассчитываются на уровне одной тренировки. Примеры: -- rolling average power -- power distribution -- time in zones -- variability index - ---- +- rolling average power +- power distribution +- time in zones +- variability index ### 5.2 Stream-based features Основаны на потоках данных: -- power stream -- heart rate stream -- cadence +- power stream +- heart rate stream +- cadence Примеры: -- power smoothing -- peak values -- drift - ---- +- power smoothing +- peak values +- drift ### 5.3 Session summary features Агрегированные признаки: -- total work -- normalized load -- intensity profile - ---- +- total work +- normalized load +- intensity profile ### 5.4 Daily features Агрегация на уровне дня: -- total load -- training density -- rest gaps +- total load +- training density +- rest gaps --- ## 6. Example features ---- - ### 6.1 Rolling power Сглаженная мощность: -- используется для расчета NP - ---- +- используется для расчета NP ### 6.2 Time in zones Распределение времени по зонам мощности. ---- - ### 6.3 Variability Index (VI) -VI = NP / Average Power +`VI = NP / Average Power` Интерпретация: -- близко к 1 → равномерная нагрузка -- выше → интервальная нагрузка - ---- +- близко к 1 -> равномерная нагрузка +- выше -> интервальная нагрузка ### 6.4 Power peaks Максимальные значения мощности на интервалах: -- 5 сек -- 1 мин -- 5 мин +- 5 сек +- 1 мин +- 5 мин --- @@ -154,26 +145,20 @@ VI = NP / Average Power ### 7.1 Compute on demand -- считать при запросе -- не хранить - ---- +- считать при запросе +- не хранить ### 7.2 Persist features -- сохранять в БД -- ускорять доступ - ---- +- сохранять в БД +- ускорять доступ ### 7.3 Hybrid -- базовые features хранить -- сложные считать - ---- +- базовые features хранить +- сложные считать -(окончательное решение — см. OPEN_DECISIONS.md) +(окончательное решение — см. `OPEN_DECISIONS.md`) --- @@ -183,8 +168,8 @@ VI = NP / Average Power Пример: -- NP → требует rolling power -- TSS → требует NP, IF +- NP -> требует rolling power +- TSS -> требует NP, IF --- @@ -192,9 +177,9 @@ VI = NP / Average Power Features должны: -- давать одинаковый результат -- не зависеть от внешних факторов -- не использовать случайность +- давать одинаковый результат +- не зависеть от внешних факторов +- не использовать случайность --- @@ -202,9 +187,9 @@ Features должны: Для любого feature: -- можно восстановить из raw данных -- формула зафиксирована -- алгоритм определен +- можно восстановить из raw данных +- формула зафиксирована +- алгоритм определен --- @@ -212,9 +197,10 @@ Features должны: Планируется: -- HR features -- HRV features -- sleep-derived features +- HR features +- HRV features +- sleep-derived features +- richer activity stream features --- @@ -222,9 +208,9 @@ Features должны: Нельзя: -- хранить только features без raw данных -- использовать AI для расчета features -- использовать нефиксированные алгоритмы +- хранить только features без raw данных +- использовать AI для расчета features +- использовать нефиксированные алгоритмы --- @@ -234,8 +220,8 @@ Features должны: проверять: -1. raw данные -2. features -3. метрики +1. raw данные +2. features +3. метрики Ошибка чаще всего на уровне features. diff --git a/docs/models/METRICS.md b/docs/models/METRICS.md index c95a4c9..abbd62e 100644 --- a/docs/models/METRICS.md +++ b/docs/models/METRICS.md @@ -118,7 +118,7 @@ fatigue_slow[d] = fatigue_slow[d-1] + (load_input[d] - fatigue_slow[d-1]) / 9 ### 4.6 Fatigue Total -В текущей model v2 это не сумма, а взвешенная смесь: +В текущей Model V2 это не сумма, а взвешенная смесь: ```text fatigue_total = 0.65 * fatigue_fast + 0.35 * fatigue_slow @@ -160,17 +160,18 @@ freshness = fitness - fatigue_total ### 5.2 Recovery Score Simple -`recovery_score_simple` — baseline heuristic score из `health_recovery_daily`. +`recovery_score_simple` — текущее имя поля baseline recovery score из `health_recovery_daily`. Свойства: - диапазон `0..100` - считается из сна, resting HR и HRV -- пока не использует индивидуальные baseline deviations +- уже использует baseline HRV и baseline resting HR, если они доступны +- breakdown сохраняется в `recovery_explanation_json` --- -## 6. Readiness (model v2 baseline) +## 6. Readiness (Model V2 baseline) Readiness — ключевая метрика системы. @@ -244,9 +245,9 @@ good_day_probability = readiness_score / 100 Планируется добавить: - нелинейную трансформацию load input -- `sleep_score_simple` -- `hrv_dev` -- `rhr_dev` +- возможное прямое использование `sleep_score` +- возможное прямое использование `hrv_dev` +- возможное прямое использование `rhr_dev` - уточненную калибровку probability / readiness zones Но: @@ -258,7 +259,9 @@ good_day_probability = readiness_score / 100 ## 10. Versioning -При изменении формул: +Сейчас versioned daily metrics уже есть как минимум для: -- фиксировать версию -- не менять исторические расчеты молча +- `load_state_daily_v2` +- `readiness_daily` + +Recovery layer пока документируется через текущее baseline behavior и explanation payload. diff --git a/docs/models/READINESS_MODEL.md b/docs/models/READINESS_MODEL.md index 0292729..ca25956 100644 --- a/docs/models/READINESS_MODEL.md +++ b/docs/models/READINESS_MODEL.md @@ -41,7 +41,13 @@ - readiness больше не равен freshness - recovery contour не заменяет fatigue, а дополняет load contour -Дополнительные сигналы, такие как `sleep_score_simple`, `hrv_dev`, `rhr_dev`, пока не являются отдельными входами readiness formula. +Дополнительные recovery-компоненты уже считаются внутри recovery layer, но пока не входят в readiness formula напрямую как отдельные веса: + +- `sleep_score` +- `hrv_score` +- `rhr_score` +- `hrv_dev` +- `rhr_dev` --- @@ -78,6 +84,12 @@ Recovery contour формируется в `health_recovery_daily` из: Текущий прикладной выход этого слоя: - `recovery_score_simple` +- `recovery_explanation_json` + +Важно: + +- имя `recovery_score_simple` сохранено для совместимости схемы и API +- по факту текущий backend baseline уже использует baseline-aware scoring ### 4.3 Baseline formula v2 @@ -167,8 +179,8 @@ good_day_probability = readiness_score / 100 Текущая модель: -- использует простой recovery score как baseline recovery contour -- пока не использует индивидуальные baseline deviations +- использует агрегированный recovery score как вход readiness +- пока не подает `hrv_dev`, `rhr_dev` и component scores в readiness formula напрямую - пока не имеет отдельной probability calibration - требует дальнейшей верификации на реальных данных @@ -179,7 +191,7 @@ good_day_probability = readiness_score / 100 Планируется: - калибровка весов `freshness_norm` и `recovery_score_simple` -- явные `sleep_score_simple`, `hrv_dev`, `rhr_dev` +- возможное явное использование `sleep_score`, `hrv_dev`, `rhr_dev` в readiness formula - уточнение interpretation layer для `good_day_probability` - уточнение decision mapping diff --git a/docs/models/current-metrics-methodology.md b/docs/models/current-metrics-methodology.md index 7a68aa0..da6ee6c 100644 --- a/docs/models/current-metrics-methodology.md +++ b/docs/models/current-metrics-methodology.md @@ -10,7 +10,7 @@ Этот документ описывает, как в текущей backend-реализации Human Engine рассчитываются основные метрики тренировки и состояния. -Документ фиксирует **реально реализованный baseline для model v2**. Если код меняется, методика должна обновляться вместе с ним. +Документ фиксирует **реально реализованный baseline для Model V2**. Если код меняется, методика должна обновляться вместе с ним. ## Область действия @@ -168,7 +168,7 @@ FatigueSlow_new = FatigueSlow_prev + (load_input - FatigueSlow_prev) / 9 #### 3.1.5 Fatigue Total -В текущей model v2: +В текущей Model V2: ```text FatigueTotal = 0.65 * FatigueFast + 0.35 * FatigueSlow @@ -205,16 +205,41 @@ Freshness = Fitness - FatigueTotal - `hrv_daily_median_ms` - `weight_kg` - `recovery_score_simple` +- `recovery_explanation_json` #### 3.2.1 Recovery Score Simple -Это baseline heuristic score `0..100`. +`recovery_score_simple` остается именем поля, но текущий backend baseline уже использует baseline-aware scoring `0..100`. Свойства: - строится из сна, resting HR и HRV -- не использует персональный baseline -- является временным baseline-слоем, а не финальной recovery model +- использует baseline HRV и baseline resting HR, если они доступны +- сохраняет breakdown в `recovery_explanation_json` +- является baseline-слоем, а не финальной откалиброванной recovery model + +#### 3.2.2 Recovery baseline components + +Текущий recovery scoring использует: + +- `hrv_baseline` +- `rhr_baseline` +- `hrv_dev` +- `rhr_dev` +- `sleep_score` +- `hrv_score` +- `rhr_score` + +Базовая формула: + +```text +sleep_score = clamp(min(sleep_minutes / 480, 1.0) * 100, 0, 100) +hrv_score = clamp(50 + 50 * hrv_dev, 0, 100) +rhr_score = clamp(50 - 50 * rhr_dev, 0, 100) +recovery_score_simple = 0.4 * hrv_score + 0.3 * rhr_score + 0.3 * sleep_score +``` + +Если baseline недоступен для компонента, используется нейтральное значение `50`. --- @@ -285,6 +310,7 @@ GoodDayProbability = Readiness / 100 - HealthKit ingestion - normalized health tables - `health_recovery_daily` +- `recovery_explanation_json` - `load_state_daily_v2` - `readiness_daily` - `good_day_probability` @@ -292,6 +318,6 @@ GoodDayProbability = Readiness / 100 ### Ограничения текущего подхода 1. `load_input_nonlinear` пока фактически линейный. -2. Recovery-контур пока использует простой aggregated score без baseline deviations. +2. Recovery-контур уже baseline-aware, но readiness пока использует его как агрегированный score, а не как full multicomponent formula. 3. `GoodDayProbability` пока является простым mapping от readiness score. 4. Decision layer поверх readiness еще не откалиброван окончательно. diff --git a/docs/models/model_v2_architecture.md b/docs/models/model_v2_architecture.md index a081a16..8cb90fe 100644 --- a/docs/models/model_v2_architecture.md +++ b/docs/models/model_v2_architecture.md @@ -2,7 +2,7 @@ ## Контекст -Model v2 уже реализована в backend как baseline-архитектура. +Model V2 уже реализована в backend как baseline-архитектура. Переход выполнен от load-only readiness к двухконтурной схеме: @@ -44,7 +44,7 @@ Recovery не заменяет fatigue, а корректирует итогов В legacy load-only подходе readiness мог использоваться как proxy от freshness. -В текущей model v2: +В текущей Model V2: ```text readiness = f(load_state, recovery_state) @@ -54,7 +54,7 @@ readiness = f(load_state, recovery_state) ## 1.3 Fast / Slow fatigue -В model v2 используются: +В Model V2 используются: - `fatigue_fast` - `fatigue_slow` @@ -94,7 +94,7 @@ load_input_nonlinear = TSS ## 1.6 Probability layer -Model v2 вводит: +Model V2 вводит: - `readiness_score` - `good_day_probability` @@ -163,6 +163,14 @@ Load-side daily aggregate: - HRV daily median - latest weight - `recovery_score_simple` +- `recovery_explanation_json` + +Текущий recovery baseline: + +- использует `hrv_baseline` и `rhr_baseline` +- считает `hrv_dev` и `rhr_dev` +- считает component scores для sleep, HRV и resting HR +- сохраняет breakdown в explanation payload --- @@ -265,8 +273,9 @@ freshness[d] = Текущий recovery output: - `recovery_score_simple` +- `recovery_explanation_json` -Это baseline heuristic score, не финальная recovery model. +Это baseline-aware recovery score, не финальная откалиброванная recovery model. --- diff --git a/docs/product/CURRENT_STATE.md b/docs/product/CURRENT_STATE.md new file mode 100644 index 0000000..6d866f9 --- /dev/null +++ b/docs/product/CURRENT_STATE.md @@ -0,0 +1,298 @@ +# Human Engine — CURRENT STATE + +Last updated: 2026-04-09 + +--- + +## 1. Общий статус + +Human Engine перешел от load-only readiness к рабочей baseline-архитектуре Model V2. + +Текущая схема: + +```text +LoadState + RecoveryState -> Readiness -> GoodDayProbability +``` + +Система уже работает как end-to-end pipeline: + +```text +iOS -> public API -> backend -> raw -> normalized -> recovery -> readiness -> response +``` + +Это уже не только модельная заготовка. В backend реализованы ingestion, materialized daily layers и response path. + +--- + +## 2. Архитектура (актуальная) + +### 2.1 Load contour + +Источник: + +- Strava + +Pipeline: + +- `strava_activity_raw` +- `daily_training_load` +- `load_state_daily_v2` + +Содержит: + +- `tss` +- `load_input_nonlinear` +- `fitness` +- `fatigue_fast` +- `fatigue_slow` +- `fatigue_total` +- `freshness` +- `version` + +### 2.2 Recovery contour + +Источник: + +- HealthKit + +Pipeline: + +- `healthkit_ingest_raw` +- normalized tables: + - `health_sleep_night` + - `health_resting_hr_daily` + - `health_hrv_sample` + - `health_weight_measurement` +- `health_recovery_daily` + +Содержит: + +- `sleep_minutes` +- `awake_minutes` +- `rem_minutes` +- `deep_minutes` +- `resting_hr_bpm` +- `hrv_daily_median_ms` +- `weight_kg` +- `recovery_score_simple` +- `recovery_explanation_json` + +Важно: + +- таблица `health_recovery_daily` уже реализована +- поле `recovery_score_simple` исторически сохраняет старое имя +- по смыслу текущий recovery layer уже baseline-aware, а не purely heuristic-only placeholder + +### 2.3 Readiness layer + +Pipeline: + +- `load_state_daily_v2 + health_recovery_daily -> readiness_daily` + +Содержит: + +- `freshness` +- `recovery_score_simple` +- `readiness_score_raw` +- `readiness_score` +- `good_day_probability` +- `status_text` +- `explanation_json` +- `version` + +Важно: + +- readiness хранится как отдельный daily storage layer +- readiness не равен `freshness` +- readiness объединяет load contour и recovery contour + +--- + +## 3. Data pipeline + +### 3.1 HealthKit full sync + +Endpoint: + +`POST /api/v1/healthkit/full-sync/{user_id}` + +Flow: + +1. raw ingest в `healthkit_ingest_raw` +2. latest raw -> normalized health tables +3. сбор affected dates +4. recompute `health_recovery_daily` +5. recompute `readiness_daily` +6. response обратно в клиент + +### 3.2 Load model v2 recompute + +Endpoint: + +`POST /api/v1/model/load-state-v2/{user_id}` + +Особенности: + +- непрерывная календарная ось +- `tss = 0` в дни без тренировок +- fast + slow fatigue +- `fatigue_total` как weighted mixture + +### 3.3 Readiness recompute + +Endpoint: + +`POST /api/v1/model/readiness-daily/{user_id}/{date}` + +--- + +## 4. Реализованная baseline model + +### 4.1 Load model v2 + +Параметры: + +- `tau_fitness = 40` +- `tau_fatigue_fast = 4` +- `tau_fatigue_slow = 9` +- `weight_fatigue_fast = 0.65` +- `weight_fatigue_slow = 0.35` + +Расчеты: + +```text +load_input_nonlinear = TSS +fatigue_total = 0.65 * fatigue_fast + 0.35 * fatigue_slow +freshness = fitness - fatigue_total +``` + +Важно: + +- поле называется `load_input_nonlinear` +- в текущем backend это все еще линейный input по TSS + +### 4.2 Recovery baseline + +Текущий recovery scoring: + +- использует `sleep_minutes` +- использует `hrv_today` и `rhr_today` +- использует `hrv_baseline` и `rhr_baseline` +- считает `hrv_dev` и `rhr_dev` +- считает `sleep_score`, `hrv_score`, `rhr_score` +- сохраняет breakdown в `recovery_explanation_json` + +Базовая формула: + +```text +recovery_score_simple = 0.4 * hrv_score + 0.3 * rhr_score + 0.3 * sleep_score +``` + +Если baseline для компонента недоступен, используется нейтральное значение. + +### 4.3 Readiness baseline + +Текущая формула: + +```text +freshness_norm = clamp(50 + freshness, 0, 100) +readiness_score_raw = 0.6 * freshness_norm + 0.4 * recovery_score_simple +readiness_score = clamp(round(readiness_score_raw, 1), 0, 100) +good_day_probability = readiness_score / 100 +``` + +Важно: + +- `good_day_probability` уже реализован +- это baseline probability-like mapping +- это не статистически откалиброванная вероятность + +--- + +## 5. Что уже работает end-to-end + +- iOS приложение отправляет HealthKit payload +- backend принимает `full-sync` +- данные попадают в raw таблицу +- latest raw раскладывается в normalized health tables +- пересчитывается `health_recovery_daily` +- пересчитывается `readiness_daily` +- результат возвращается в iOS через public API + +Публичный API уже проксируется через VPS / Caddy по пути `/api/*`. + +--- + +## 6. Основные ограничения текущей версии + +1. Recovery baseline уже реализован, но еще не откалиброван на популяционных или персональных outcome данных. +2. `load_input_nonlinear` пока фактически линейный. +3. `good_day_probability` пока является простым mapping от readiness score. +4. Decision layer и recommendation layer еще не реализованы как отдельный production layer. +5. Персонализация и calibration остаются следующим этапом. + +--- + +## 7. Ключевые архитектурные решения + +- Load и Recovery разделены +- Readiness не равен freshness +- Readiness хранится отдельно в `readiness_daily` +- Recovery влияет на readiness, но не переписывает load model +- используется daily aggregation +- используется probability layer (`good_day_probability`) +- deterministic core остается приоритетом + +--- + +## 8. Текущие источники данных + +### Реальные + +- Strava +- HealthKit + +### Planned + +- Garmin +- дополнительные recovery signals +- decision / recommendation outputs + +--- + +## 9. Следующие шаги (приоритет) + +### P1 — Calibration + +- readiness / probability calibration +- проверка recovery baseline на реальных данных +- уточнение readiness zones + +### P2 — Decision layer + +- recommendation layer поверх readiness +- rule-based decision mapping + +### P3 — UX / Product integration + +- user-facing readiness screen в iOS +- объяснения на основе уже существующих breakdown payloads + +### P4 — Model improvements + +- nonlinear load transform +- personalization +- расширение feature layer + +--- + +## 10. Definition of Done для текущей стадии + +Система считается рабочей на текущем этапе, если: + +- данные приходят из HealthKit и Strava +- считается `load_state_daily_v2` +- считается `health_recovery_daily` +- считается `readiness_daily` +- `good_day_probability` доступен как отдельный output +- документация соответствует реальному backend baseline diff --git a/docs/product/SCENARIOS.md b/docs/product/SCENARIOS.md index e0b44ed..a7256d2 100644 --- a/docs/product/SCENARIOS.md +++ b/docs/product/SCENARIOS.md @@ -6,9 +6,9 @@ Цель: -- связать систему с реальным использованием -- показать, как формируется ценность -- зафиксировать ключевые user flows +- связать систему с реальным использованием +- показать, как формируется ценность +- зафиксировать ключевые user flows --- @@ -18,27 +18,29 @@ Основной сценарий системы: -> пользователь хочет понять, какую тренировку делать сегодня - ---- +> пользователь хочет понять, в каком состоянии он находится сегодня и насколько день выглядит подходящим для нагрузки ### Flow -1. Пользователь открывает систему -2. Система анализирует последние данные -3. Рассчитывается readiness -4. Формируется ride briefing -5. Пользователь принимает решение - ---- +1. Пользователь открывает систему +2. Система анализирует последние данные +3. Рассчитывается readiness +4. Система возвращает status / score / probability и explanation +5. Пользователь принимает решение ### Output Пользователь получает: -- текущий статус (готовность) -- рекомендацию по нагрузке -- краткое объяснение +- текущий статус готовности +- readiness score +- good day probability +- краткое объяснение + +Комментарий: + +- readiness output уже реализован в backend +- recommendation / ride briefing layer пока остается planned --- @@ -46,16 +48,14 @@ ### Context -- несколько дней высокой нагрузки -- накопленная усталость - ---- +- несколько дней высокой нагрузки +- накопленная усталость ### Expected system behavior -- снижение readiness -- рекомендация: отдых или легкая тренировка -- предупреждение о перегрузке +- снижение readiness +- снижение `good_day_probability` +- explanation через load + recovery breakdown --- @@ -63,15 +63,13 @@ ### Context -- период снижения нагрузки -- восстановление - ---- +- период снижения нагрузки +- восстановление ### Expected system behavior -- рост readiness -- рекомендация: интенсивная тренировка +- рост readiness +- рост `good_day_probability` --- @@ -79,15 +77,13 @@ ### Context -- регулярные тренировки -- умеренная нагрузка - ---- +- регулярные тренировки +- умеренная нагрузка ### Expected system behavior -- стабильный readiness -- рекомендация: умеренная нагрузка +- стабильный readiness +- стабильный readiness output без скрытой логики --- @@ -95,14 +91,12 @@ ### Context -- резкий рост нагрузки - ---- +- резкий рост нагрузки ### Expected system behavior -- корректировка readiness вниз -- рекомендация: снижение нагрузки +- корректировка readiness вниз +- корректировка probability вниз --- @@ -110,15 +104,13 @@ ### Context -- нет тренировок -- недостаточно данных - ---- +- нет тренировок +- недостаточно данных ### Expected system behavior -- ограниченная уверенность -- осторожная рекомендация +- ограниченная уверенность +- fallback на доступные слои readiness --- @@ -126,16 +118,14 @@ ### Context -- отсутствуют некоторые метрики -- нет power / HR - ---- +- отсутствуют некоторые метрики +- нет power / HR ### Expected system behavior -- использовать доступные данные -- не ломать модель -- явно ограничивать точность +- использовать доступные данные +- не ломать модель +- явно ограничивать точность --- @@ -143,14 +133,12 @@ ### Context -- длительный перерыв - ---- +- длительный перерыв ### Expected system behavior -- высокий readiness (формально) -- но осторожная рекомендация +- формально возможен рост readiness за счет текущего baseline +- downstream decision layer должен учитывать это отдельно, когда будет реализован --- @@ -158,9 +146,9 @@ Во всех сценариях система должна: -- быть предсказуемой -- быть объяснимой -- не давать противоречивые рекомендации +- быть предсказуемой +- быть объяснимой +- не давать противоречивые outputs между score, probability и status --- @@ -168,9 +156,10 @@ Система пока не делает: -- долгосрочное планирование -- автоматическое построение тренировочных программ -- персонализированный coaching +- долгосрочное планирование +- автоматическое построение тренировочных программ +- персонализированный coaching +- production-calibrated decision layer --- @@ -178,9 +167,11 @@ Планируется: -- адаптивные планы тренировок -- прогнозирование результата -- интеграция с календарем +- адаптивные планы тренировок +- recommendation layer +- ride briefing layer +- прогнозирование результата +- интеграция с календарем --- @@ -188,10 +179,10 @@ Этот документ используется для: -- проверки логики модели -- тестирования -- проектирования UI -- работы с AI +- проверки логики модели +- тестирования +- проектирования UI +- работы с AI --- @@ -199,6 +190,6 @@ Сценарии должны: -- соответствовать реальному поведению системы -- использоваться в тестах -- обновляться при изменениях логики \ No newline at end of file +- соответствовать реальному поведению системы +- использоваться в тестах +- обновляться при изменениях логики diff --git a/sql/health/sql_health_recovery_daily_add_explanation_json.sql b/sql/health/sql_health_recovery_daily_add_explanation_json.sql new file mode 100644 index 0000000..0acfdb9 --- /dev/null +++ b/sql/health/sql_health_recovery_daily_add_explanation_json.sql @@ -0,0 +1,2 @@ +alter table health_recovery_daily +add column if not exists recovery_explanation_json jsonb; \ No newline at end of file