Files
healthlog/openspec/changes/archive/2026-08-02-trenirovki-i-zapisi/proposal.md
T
av f8200f7f80 feat: разбор и хранение тренировок и состояния разума
- секции `workouts` и `stateOfMind` покрыты разбором: тренировка лежит одной
  строкой вместе с маршрутом и внутренними рядами, запись — по ключу `род + id`;
  миграция 00007 заводит обе таблицы и возвращает в очередь `partial`-доставки
  с этими ключами
- сущность заменяется целиком, но условно: приехавшая побеждает, если не теряет
  содержания сохранённой (множество ключей и длины верхнеуровневых массивов), а
  при равном содержании выигрывает версия из более поздней доставки ЖУРНАЛА —
  «побеждает приехавшая» было бы функцией порядка свёртки, и живая витрина
  расходилась бы с пересборкой молча
- отпечаток витрины покрывает тренировки и записи и снимается одним снимком
  базы; отчёт `reindex` считает «было и стало» по каждой единице хранения
2026-08-02 13:05:16 +03:00

6.9 KiB

Why

Половина живого потока разбором не покрыта. Замер по 118 доставкам архива: 65 несут metrics, 27 — workouts, 26 — stateOfMind. Тренировки и состояние разума сохраняются в архив и числятся partial, но в витрину не попадают: трекеру (второй потребитель паспорта) взять тренировку неоткуда, агенту-медику состояние разума — тоже.

Для stateOfMind это дороже, чем для метрик: в родном экспорте Apple секции нет ни одним типом (находка 46), то есть доставки HAE — её единственный источник, и ретеншен архива без разобранной секции нельзя включать вовсе.

What Changes

  • Разбор покрывает две новые секции тела: workouts и stateOfMind. Остальные секции с собственными id (ecg, symptoms, cycleTracking, medications, heartRateNotifications) остаются непокрытыми намеренно — живьём поток их не приносил ни разу, и модель под них закладывается, а разбор — нет.
  • Две новые таблицы: workout (заголовок колонками, всё остальное, включая маршрут и внутренние ряды, — payload дословно) и record (секции с собственным id, ключ kind + id).
  • Сущность с собственным id заменяется целиком, а не сливается по полям. Правило замены названо явно: приехавшая версия побеждает, если не теряет содержания сохранённой; иначе сохранённая остаётся, факт считается и идёт в WARN. При равных наборах полей выигрывает версия из более поздней доставки журнала, а не свёрнутая последней, — иначе живая витрина расходилась бы с пересборкой молча.
  • Сущность несёт провенанс — доставку своей версии и её метку приёма.
  • Разбор метки времени принимает второй формат — RFC 3339 в UTC, которым HAE шлёт stateOfMind (находка 16).
  • Отпечаток витрины (оракул сходимости reindex) покрывает тренировки и записи, а не одни часовые объекты, и снимается одним снимком базы. Отчёт reindex считает «до и после» по каждой единице хранения и называет «покрыта новая секция» ожидаемым классом расхождения.
  • «Отказ разбора — всё или ничего» распространяется на сущности явно, включая ветку невыводимого слоя и повтор ключа секции.
  • Миграция переводит в pending доставки, у которых в списке непокрытых секций стоят ставшие покрытыми имена, — правило «покрыли секцию — пересверните» уже записано в спеке хранения.
  • Не входит: отдача тренировок и записей наружу. Read API в проекте пока нет вовсе; его форма (конверт ответа, выбор слоя, предел размера) проектируется задачей read-api-tochki, и вводить два эндпоинта раньше конверта значило бы задать контракт мимоходом.

Capabilities

New Capabilities

Новых нет: тренировка и запись — это то же хранилище и тот же разбор, только другая единица хранения. Отдельная capability создала бы второй словарь для того же домена.

Modified Capabilities

  • parsing: покрытых секций становится три вместо одной; появляется разбор сущностей с собственным id и второй формат метки времени (RFC 3339), оставленный предыдущей дельтой явно ненормированным; «всё или ничего» распространяется на сущности.
  • storage: появляется вторая единица хранения — сущность с собственным id, со своим правилом замены версии; отпечаток витрины перестаёт быть отпечатком одних часовых объектов; запрет на данные о здоровье в логах распространяется на содержимое сущностей.
  • reindex: счётчики отчёта покрывают все единицы хранения, а «покрыта новая секция» становится названным классом ожидаемого расхождения.

Impact

  • internal/hae — разбор двух секций, второй формат метки, новые счётчики пропусков.
  • internal/store — таблицы workout и record, слияние доставки одной транзакцией вместе с точками, отпечаток витрины.
  • internal/fold — счётчики и единственный логирующий чекпоинт свёртки.
  • internal/replay — ничего, кроме того, что отпечаток стал шире: пересборка зовёт ту же свёртку.
  • Миграция 00007 — две таблицы и перевод partial-доставок в pending.
  • docs/database.md, docs/architecture.md, docs/local-research.md — схема, правило замены версии и находка о поведении тренировки при переприсылке.
  • Фикстуры internal/hae/testdata и скрипт их сборки tmp/research/fixtures.py: тренировка с маршрутом и состояние разума — на реальных пакетах с вычищенными измерениями.