пересмотрена архитектура под слои, свёртку в ответе и MCP

- агрегация появилась в ответе на запрос: род метрики измеряется сверкой
  слоёв, нижний слой HAE не суммируется никогда
- переведённые строки хранятся дословно с приписанным кодом HealthKit;
  снято правило «настройки данных у всех проходов одинаковы» — слой в ключе
- добавлены устаревание нижнего слоя по проверенному экспорту и MCP поверх
  read API; план вырос до 11 шагов
This commit is contained in:
av
2026-08-01 13:48:22 +03:00
parent 18d76d9610
commit 9d06509446
4 changed files with 363 additions and 95 deletions
+53 -34
View File
@@ -4,12 +4,13 @@
## Ближайшая цель
Минимальное ядро, которое принимает и сохраняет пакет. Проверяем его
**локально**: сервис поднят на рабочей машине, телефон шлёт на её IP по
локальной сети. Ни деплоя, ни домена, ни TLS для этого не нужно — они
понадобятся, когда сервис поедет на rivendell (шаг 8).
Приём работает, поток настоящих пакетов копится в сыром архиве. Дальше —
разбор и хранилище (шаг 3): всё, что нужно, чтобы данные перестали быть
недифференцированной кучей тел запросов.
Это шаги 12.
Разведка закончена: правило вывода слоя, модель идентичности и формы точки
проверены на живом потоке, выводы — в
[local-research.md](local-research.md), 41 находка.
## Шаги
@@ -20,23 +21,40 @@
**подключаем телефон по локальной сети**
- [ ] **3. Разбор и хранилище.** Миграции, часовые объекты метрик
(`bucket`, ключ `метрика + слой + час`) + `workout`/`record` по своим
`id`, вывод слоя из выравнивания меток, канонизация с округлением чисел
и хеш содержимого, слияние точек в объект, два формата дат,
`healthlog reindex`.
- [ ] **4. Read API.** Каталог метрик со слоями и диапазонами, точки с
пагинацией и выбором слоя (сборка из часовых объектов), тренировки,
`id`. Вывод слоя из выравнивания меток; развод `sleep_analysis` на два
имени (находка 38). Канонизация с округлением чисел и хеш содержимого
как детектор изменений. Слияние точек: при столкновении выигрывает
**более полная** точка, а не последняя (находка 41). Три формата
времени: локальное со смещением, RFC 3339 Z, Unix-эпоха внутри
`heartbeatSeries` (находка 39). Словарь `(локаль, строка) → код
HealthKit` для переведённых значений (находка 37). `healthlog reindex`.
- [ ] **4. Каталог и род агрегации.** Род (`cumulative`/`instant`/`unknown`)
**измеряется** сверкой слоёв между собой, а не размечается руками
(находка 40). Каталог метрик со слоями, диапазонами и родом.
- [ ] **5. Read API.** Точки метрики из часовых объектов, выбор слоя,
необязательная свёртка по сетке. Огрубление, когда сетка не задана;
ошибка со списком доступных сеток, когда задана явно. Тренировки,
записи.
- [ ] **5. Самоописание.** Каталог разрезов + выведенные из данных схемы
содержимого со статистикой + статичная схема контракта API.
- [ ] **6. `healthlog import`.** Заливка полной истории кусками по годам.
- [ ] **7. Наблюдаемость.** `/stats`: последняя доставка, счётчики, тишина.
- [ ] **8. Деплой.** `Dockerfile`, сборка образа локально, доставка на
rivendell, конфиг Caddy, поддомен, токены.
- [ ] **6. Самоописание.** Выведенные из данных схемы содержимого со
статистикой + статичная схема контракта API.
- [ ] **7. MCP.** Эндпоинт того же процесса, транспорт Streamable HTTP, токен
чтения общий с Read API. Три инструмента: каталог, значения за период,
значения с разбивкой.
- [ ] **8. `healthlog import`.** Родной экспорт Apple Health: разбор
`экспорт.xml` в слой `sample`, маршруты GPX, ЭКГ из CSV. Заливка полной
истории кусками по годам.
- [ ] **9. Устаревание нижнего слоя.** Пометка данных HAE старше проверенного
экспорта. Проверка покрытия — непрерывность по дням и сходимость сумм с
часовым слоем. Пометка ≠ удаление: удаление включаем только после того,
как восстановление из экспорта отработает на живых данных хотя бы раз.
- [ ] **10. Наблюдаемость.** `/stats`: последняя доставка, счётчики, тишина по
потоку, список строк без кода в словаре.
- [ ] **11. Деплой.** `Dockerfile`, сборка образа локально, доставка на
rivendell, конфиг Caddy, поддомены приёма и чтения, токены.
Порядок неслучаен. После шага 2 автоматизация в Health Auto Export включена и
копит настоящие пакеты в сыром архиве. Документация формата HAE скудная,
поэтому разбор на шаге 3 пишем по реальным данным, а не по догадкам — и
заодно видим фактический объём и характер потока.
Порядок неслучаен. Шаг 4 стоит перед Read API, потому что без измеренного рода
свёртка на шаге 5 неотличима от угадывания. Шаг 8 стоит перед 9: пока импорт
экспорта не написан, помечать что-либо устаревшим не на основании чего.
## Отложено
@@ -45,12 +63,21 @@
и теми же данными почти никогда не совпадают побайтно — хеш тела не
сработает. Дедупликация возможна только по канонизированному содержимому,
а это и делает хеш часового объекта. Отдельная механика не нужна.
- ~~Вторая автоматизация без группировки.~~ **Сделано на телефоне:** метрики
здоровья идут в трёх разрезах — несуммированном для несуммируемых метрик,
минутном и часовом для всех.
- ~~Пометка локализованных полей в схемах.~~ **Переросло в шаг 3:** одной
пометки мало, нужен словарь кодов, иначе не сойтись с родным экспортом
(находка 37).
- **Ретеншен сырого архива** (`storage.raw_retention`, 14 дней) — удаление
старых тел. Пока архив не подчищается; включить после того, как разбор
устоится, иначе страховка исчезнет раньше, чем перестанет быть нужна.
- **Порог `sealed`** — с какого возраста час считается запечатанным. Ставим по
факту: сначала пишем `WARN` на изменение старых объектов и смотрим, какая
глубина досчёта встречается в жизни (наблюдалось до 22 минут, находка 10).
- **Месячный проход по ручным секциям.** Симптомы и лекарства заводятся задним
числом на недели; количественным метрикам недельного прохода хватает.
Заводить, когда эти секции появятся в потоке живьём.
- **Алерт «данных нет N часов».** Тихо сломавшаяся автоматизация — главный
эксплуатационный риск коллектора. В v1 факт виден в `/stats`; активное
уведомление добавим после.
@@ -60,20 +87,12 @@
а это отслеживается.
- **Схема тренировок** — глубину вывода определим по факту, когда увидим,
как приходят маршруты.
- **Пометка локализованных полей в схемах.** `context`, `value` и подобные
приходят на языке телефона и изменятся при смене языка iOS — клиентам не
стоит завязываться на конкретные строки
([local-research.md](local-research.md), находка 8).
- **Вторая автоматизация без группировки** для `sleep_analysis` и
`heart_rate_variability` — минутная группировка съедает фазы сна и
межударные интервалы ценой ~130 точек в сутки (находка 6).
- **Переход на более грубый нижний слой.** Посекундный слой стоит ~730 МБ в
год против ~20 МБ у минутного. Когда решим, что мелкая подробность не нужна,
достаточно выключить несуммированную автоматизацию — старые данные останутся
в своих слоях, переписывать ничего не придётся. Обратный путь тоже есть:
ручной экспорт из Apple Health + `healthlog import` восстанавливает нижний
слой.
- **Отказ от `heartbeatSeries`.** 93% объёма HRV (находка 39) ради данных,
которых нет ни в одном планируемом запросе. Решать, когда станет ясна цена
хранения нижнего слоя за год.
- **Выгрузка в parquet** — отдельной командой, на случай тяжёлой аналитики
снаружи. DuckDB читает и parquet, и файл SQLite напрямую, поэтому спешить
некуда: дверь открыта без миграции.
- **NDJSON-поток** для больших выборок из read API.
- **Слой агрегаций** поверх сырья (суточные/недельные срезы).
- **Разворачивание маршрутов тренировок** в отдельную таблицу — если появится
клиент, которому мало отдачи тренировки одним пакетом.