feat: разбор и хранение тренировок и состояния разума
- секции `workouts` и `stateOfMind` покрыты разбором: тренировка лежит одной строкой вместе с маршрутом и внутренними рядами, запись — по ключу `род + id`; миграция 00007 заводит обе таблицы и возвращает в очередь `partial`-доставки с этими ключами - сущность заменяется целиком, но условно: приехавшая побеждает, если не теряет содержания сохранённой (множество ключей и длины верхнеуровневых массивов), а при равном содержании выигрывает версия из более поздней доставки ЖУРНАЛА — «побеждает приехавшая» было бы функцией порядка свёртки, и живая витрина расходилась бы с пересборкой молча - отпечаток витрины покрывает тренировки и записи и снимается одним снимком базы; отчёт `reindex` считает «было и стало» по каждой единице хранения
This commit is contained in:
@@ -21,7 +21,6 @@
|
||||
- [Порядок журнала при конкурентных приёмах](poryadok-zhurnala-na-priyome.md) — доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда — живое состояние расходится с reindex
|
||||
|
||||
## высокий
|
||||
- [Тренировки и секции с собственными id](trenirovki-i-zapisi.md) — Тренировки с геотреком и состояние разума приходят, но не разбираются — без них не закрыть ни трекер, ни агента-медика
|
||||
- [Измеренный род агрегации и каталог разрезов](rod-agregacii-i-katalog.md) — Без рода метрики свёртка в ответе неотличима от угадывания — а суммировать нижний слой значит завысить втрое
|
||||
- [Read API: точки, выбор слоя, свёртка по сетке](read-api-tochki.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может
|
||||
- [OpenAPI-спека и Swagger UI](openapi-swagger.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате
|
||||
@@ -31,6 +30,7 @@
|
||||
- [Словарь категориальных значений → коды HealthKit](slovar-kategorialnyh-znachenij.md) — Фазы сна и типы тренировок приходят строками русской локали — с экспортом Apple их не сверить
|
||||
- [Выведенные из данных схемы содержимого](samoopisanie-shemy.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
|
||||
- [Импорт родного экспорта Apple Health](import-eksporta-apple.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
|
||||
- [Идентичность тренировок при импорте родного экспорта](identichnost-trenirovok-pri-importe.md) — В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE
|
||||
- [Проверка секций, которых поток ещё не приносил](proverka-novyh-sekcij.md) — Симптомы, ЭКГ, лекарства, цикл и вес не виденны живьём — разбор писался вслепую
|
||||
- [Наблюдаемость: /stats](stats-nablyudaemost.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах
|
||||
- [Проверка целостности собранной витрины перед подменой](celostnost-pered-podmenoj.md) — Подмена файла необратима, а годность выхода подтверждена отпечатком на ещё открытом дескрипторе
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Идентичность тренировок при импорте родного экспорта
|
||||
|
||||
**Приоритет:** средний
|
||||
|
||||
Тренировка в витрине адресуется своим `id` из HealthKit — его шлёт HAE. В
|
||||
`export.xml` этого идентификатора **нет вовсе**: у элемента `Workout` только
|
||||
тип, источник, даты и статистика. Значит `healthlog import` не сможет сопоставить
|
||||
тренировку из снапшота с той же тренировкой, уже приехавшей от HAE, и они
|
||||
задвоятся.
|
||||
|
||||
Это ровно та дыра, что была у точек, и там её закрыли ключом `start + end`
|
||||
(находка 47): `HKObject.uuid` в выгрузку не попадает, поэтому модель
|
||||
идентичности обязана выражаться через интервал.
|
||||
|
||||
Prior art: `dogsheep/healthkit-to-sqlite` адресует тренировку **хешем
|
||||
содержимого** (`hash_id` в sqlite-utils) — ровно потому, что идентификатора в
|
||||
экспорте нет. Нам это не подходит в лоб: у нас половина тренировок уже лежит под
|
||||
настоящим `id`, и хеш содержимого дал бы третий ключ рядом с двумя.
|
||||
|
||||
Варианты, которые надо будет сравнить:
|
||||
|
||||
- **Второй уникальный ключ `(start_utc, end_utc)`** у тренировки: импорт ищет по
|
||||
нему, HAE — по `id`. Цена: индекс и вопрос, что делать при столкновении двух
|
||||
разных тренировок с одним интервалом (бывает ли такое — неизвестно).
|
||||
- **Сопоставление на стадии импорта**, без изменения схемы: импорт читает уже
|
||||
сохранённые тренировки за период и приписывает найденным их `id`. Цена: логика
|
||||
сопоставления живёт в импорте и не проверяется ничем, кроме него.
|
||||
- **Считать тренировки из экспорта отдельным родом** и не сопоставлять вовсе.
|
||||
Цена: потребитель видит две тренировки вместо одной и обязан схлопывать сам —
|
||||
ровно то, чего проект старается не делать.
|
||||
|
||||
Решать до появления формы `healthlog import` значит угадывать: неизвестно,
|
||||
понадобятся ли тренировки из экспорта вообще (у HAE они полнее — с маршрутом и
|
||||
рядами, а в экспорте маршрут лежит отдельными GPX).
|
||||
|
||||
Связано: `docs/architecture.md` → «Тренировки и прочие секции», задача
|
||||
`import-eksporta-apple`.
|
||||
@@ -36,3 +36,10 @@
|
||||
секцию, которой раньше не было, теперь **фиксируется** — остаётся научиться
|
||||
замечать его активно: один `SELECT DISTINCT` по колонке даёт список всего, что
|
||||
поток приносил, и сравнение с известным набором закрывает задачу.
|
||||
|
||||
Модель под секции с собственным `id` заложена (change
|
||||
`2026-08-02-trenirovki-i-zapisi`): таблица `record` ключуется парой
|
||||
`род + id`, и новая секция добавляется **одной строкой** в множество покрытых
|
||||
имён разбора, а не миграцией. Покрыты `workouts` и `stateOfMind`; остались
|
||||
`ecg`, `symptoms`, `cycleTracking`, `medications`, `heartRateNotifications` —
|
||||
их формы никто не видел, и разбор вслепую сознательно не писался.
|
||||
|
||||
@@ -14,8 +14,17 @@
|
||||
и не влезает — ошибка со списком доступных сеток, а не тихая подмена. Различие
|
||||
существенно: иначе агент, попросивший минутную сетку, получит суточные суммы.
|
||||
|
||||
**Отдача тренировок и записей входит сюда же.** Разбор и хранение сущностей с
|
||||
собственным `id` сделаны (change `2026-08-02-trenirovki-i-zapisi`), а эндпоинтов
|
||||
нет: тренировка с маршрутом и записи `stateOfMind` лежат в витрине и наружу не
|
||||
отдаются. Вводить их раньше конверта ответа значило бы задать контракт
|
||||
мимоходом, поэтому `GET /workouts`, `GET /workouts/{id}` и
|
||||
`GET /records/{kind}` закрываются этой задачей — вместе с формой конверта и
|
||||
правилом размера ответа. Второй сценарий паспорта (трекер) до тех пор не закрыт.
|
||||
|
||||
Готово, когда «шаги за неделю по дням» и «вес за год» отвечаются одним запросом
|
||||
каждый, а в ответе всегда видно `layer`, `bucket` и `aggregation`.
|
||||
каждый, тренировка отдаётся одним пакетом вместе с маршрутом, а в ответе всегда
|
||||
видно `layer`, `bucket` и `aggregation`.
|
||||
|
||||
Связано: `docs/architecture.md` → «Read API», план → шаг «Read API».
|
||||
|
||||
|
||||
@@ -26,14 +26,38 @@
|
||||
экспорта, записи `stateOfMind` не трогаются вовсе, а `/stats` показывает
|
||||
глубину архива и дату снапшота, до которой он подрезан.
|
||||
|
||||
## Предусловие снято
|
||||
## Предусловие снова открыто
|
||||
|
||||
Признак, без которого ретеншен был опасен, готов: доставка с непокрытой секцией
|
||||
имеет статус `partial` и список непокрытых ключей
|
||||
(change `2026-08-01-nerazobrannye-sekcii-dostavki`). Ретеншен обязан спрашивать
|
||||
статус, а не считать `parsed` разрешением: тело `stateOfMind` восстановить
|
||||
неоткуда — в экспорте Apple секции нет.
|
||||
Признак «доставка с непокрытой секцией» появился в change
|
||||
`2026-08-01-nerazobrannye-sekcii-dostavki` и работал заодно защитой
|
||||
`stateOfMind`: такие доставки числились `partial`, и ретеншен их не тронул бы.
|
||||
|
||||
Change `2026-08-02-trenirovki-i-zapisi` покрыл `stateOfMind` разбором, и защита
|
||||
исчезла: доставка из одного состояния разума теперь получает `parsed` с пустым
|
||||
списком непокрытых, то есть **побайтово неотличима** от доставки из метрик — а
|
||||
метрики восстановимы из экспорта Apple, состояние разума нет (находка 46).
|
||||
Ретеншен, написанный по правилу «удаляем всё, что не `partial`», сотрёт ровно те
|
||||
тела, которых в экспорте не существует, и первая же пересборка потеряет историю
|
||||
состояния разума навсегда.
|
||||
|
||||
Значит признак невосстановимости нужен **не производный от «непокрытости»**.
|
||||
Варианты:
|
||||
|
||||
- **Перечень покрытых секций, которых нет в экспорте Apple** рядом с доставкой
|
||||
(сегодня — ровно `stateOfMind`). Цена: колонка и строка в свёртке; читается
|
||||
так же, как `uncovered_sections`, и одним запросом.
|
||||
- **Признак у доставки «тело — единственный источник»**, выставляемый разбором.
|
||||
Цена та же, но смысл шире и требует решения, что считать единственным
|
||||
источником для будущих секций.
|
||||
- **Никогда не подрезать тела доставок, у которых есть строки в `record`.**
|
||||
Цена нулевая по схеме, но неточная: провенанс записи указывает на доставку
|
||||
её **текущей** версии, а копий у записи бывает по 26.
|
||||
|
||||
Рекомендация — первый вариант: он прямо отвечает на вопрос «что останется
|
||||
потерянным», как это уже делает `uncovered_sections`, и не требует додумывать
|
||||
семантику.
|
||||
|
||||
Вместе с этим действует правило: задача, которая начинает разбирать секцию, тем
|
||||
же изменением переводит `partial`-строки с этим ключом в `pending`. Ретеншену
|
||||
позволено смотреть на `partial` только пока правило соблюдается.
|
||||
же изменением переводит `partial`-строки с этим ключом в `pending` (так сделала
|
||||
миграция `00007`). Ретеншену позволено смотреть на `partial` только пока правило
|
||||
соблюдается.
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# Тренировки и секции с собственными id
|
||||
|
||||
**Приоритет:** высокий
|
||||
|
||||
Тренировки приезжают с геотреком, состояние разума — с кодами HealthKit. Ни то,
|
||||
ни другое сейчас не разбирается. Тренировки нужны трекеру (второй сценарий),
|
||||
состояние разума — агенту-медику.
|
||||
|
||||
Модель отличается от метрик: у этих сущностей есть собственный `id`, они редки,
|
||||
и по часам их группировать незачем. Тренировка **перезаписывается** целиком —
|
||||
она приезжает повторно, когда доедет маршрут.
|
||||
|
||||
Шаги:
|
||||
- миграции `workout` и `record` (секции `stateOfMind`, `ecg`, `symptoms`,
|
||||
`cycleTracking`, `medications`, `heartRateNotifications` — модель одна);
|
||||
- заголовок тренировки колонками, маршрут и внутренние ряды — блобом;
|
||||
- пульс внутри тренировки не смешивать с метрикой `heart_rate`: разные таблицы.
|
||||
|
||||
Готово, когда тренировка отдаётся одним пакетом вместе с маршрутом, а
|
||||
`stateOfMind` виден записями.
|
||||
|
||||
Связано: `docs/architecture.md` → «Тренировки и прочие секции».
|
||||
|
||||
Reference in New Issue
Block a user