блокеры разобраны замером: три стали задачами, один закрыт
- находки 48 и 49: единицы не менялись ни разу; настоящих столкновений 0.65%, несравнимых наборов полей нет, тай-брейк берёт меньшее в 96% случаев - edinicy-metriki-v-razreze закрыт: гипотеза не подтвердилась замером - тай-брейк отложен до каталога рода агрегации
This commit is contained in:
@@ -5,3 +5,4 @@
|
||||
<!-- - ГГГГ-ММ-ДД `slug` — Заголовок. Причина: … Был приоритет: … -->
|
||||
- 2026-08-01 `bekap-dannyh` — Резервное копирование ./data. Причина: бекап обеспечивает готовый механизм на сервере пет-проектов — своего заводить не нужно, задача снимается деплоем. Был приоритет: высокий.
|
||||
- 2026-08-01 `identichnost-epizodnyh-metrik` — Идентичность эпизодных метрик. Причина: решён измерением и prior art: ключ эпизода — метрика+слой+start+end (находка 47), вариант А; вернулся в scope razbor-metrik-v-obekty. Был приоритет: блокеры.
|
||||
- 2026-08-01 `edinicy-metriki-v-razreze` — Единицы метрики: часть координаты или свойство объекта. Причина: измерено: на 99 доставках единицы не менялись ни у одной из 30 метрик (находка 49 → 48); реализованное правило «сохранённое побеждает + WARN + счётчик» делает событие наблюдаемым. Был приоритет: блокеры.
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
одному — прерывать поток ради каждого дороже, чем накопить.
|
||||
|
||||
## блокеры
|
||||
- [Разнести ответ приёма и свёртку доставки](otvet-i-svyortka.md) — синхронная свёртка не помещается в write_timeout: широкие проходы получают обрыв вместо 200
|
||||
- [Правило выбора победителя при столкновении точек](pravilo-sliyaniya-tochek.md) — полнота считается числом ключей, поэтому мусорные поля бьют измерение
|
||||
- [Единицы метрики: часть координаты или свойство объекта](edinicy-metriki-v-razreze.md) — смена единиц в настройках HAE делит час на точки в разных единицах
|
||||
- [Судьба доставки, у которой разобрана не вся секция data](nerazobrannye-sekcii-dostavki.md) — тело с одним stateOfMind помечается parsed, а ретеншен снесёт его как разобранное
|
||||
|
||||
## высокий
|
||||
- [Тренировки и секции с собственными id](trenirovki-i-zapisi.md) — Тренировки с геотреком и состояние разума приходят, но не разбираются — без них не закрыть ни трекер, ни агента-медика
|
||||
@@ -24,6 +20,9 @@
|
||||
- [Read API: точки, выбор слоя, свёртка по сетке](read-api-tochki.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может
|
||||
- [OpenAPI-спека и Swagger UI](openapi-swagger.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате
|
||||
- [MCP-сервер поверх Read API](mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
|
||||
- [Полнота точки — по множеству ключей, а не по их числу](pravilo-sliyaniya-tochek.md) — точка с пятью пустыми полями бьёт настоящее измерение: полнота считается счётчиком, а не множеством
|
||||
- [Непокрытые секции доставки видны в статусе разбора](nerazobrannye-sekcii-dostavki.md) — тело с одним stateOfMind числится parsed, а ретеншен снесёт его как разобранное — и в экспорте Apple его нет
|
||||
- [Разнести ответ приёма и свёртку доставки](otvet-i-svyortka.md) — синхронная свёртка не помещается в write_timeout: широкие проходы получают обрыв вместо 200
|
||||
|
||||
## средний
|
||||
- [Словарь категориальных значений → коды HealthKit](slovar-kategorialnyh-znachenij.md) — Фазы сна и типы тренировок приходят строками русской локали — с экспортом Apple их не сверить
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# Единицы метрики: часть координаты или свойство объекта
|
||||
|
||||
**Приоритет:** блокеры
|
||||
|
||||
Вынуто ревью кода задачи `razbor-metrik-v-obekty` (профиль `deep`, найдено
|
||||
тремя проходами независимо).
|
||||
|
||||
## Что решить
|
||||
|
||||
Единицы измерения живут колонкой объекта — одна на все точки часа. Что делать,
|
||||
когда в тот же час приезжают точки в **других** единицах.
|
||||
|
||||
## Почему это не мелочь
|
||||
|
||||
Внутри точки единиц нет: проверено на 89 доставках, поле `units` не
|
||||
встретилось ни разу, оно живёт только на уровне метрики. Значит у точки,
|
||||
сохранённой раньше, не остаётся **ничего**, по чему её единицы восстановимы —
|
||||
кроме сырого архива, пока он жив.
|
||||
|
||||
Смена реальна и не требует злого умысла: переключатель единиц в настройках
|
||||
HAE, смена локали телефона, переименование единицы в новой версии приложения.
|
||||
|
||||
## Варианты и цена
|
||||
|
||||
**(1) Единицы — часть координаты объекта** (`метрика + слой + единицы + час`).
|
||||
Цена: миграция, ключ шире, каталог разрезов обязан показывать единицы. Зато
|
||||
точки никогда не подписаны чужим — разные единицы просто разные ряды.
|
||||
|
||||
**(2) Первое непустое побеждает, расхождение — `WARN` и счётчик** (сделано
|
||||
сейчас как безопасное умолчание).
|
||||
Цена: нулевая, уже работает. Но час, начавшийся в километрах, останется
|
||||
километровым навсегда, даже если телефон окончательно переехал на мили.
|
||||
|
||||
**(3) Хранить обе величины у объекта** (`units` и `units_seen`).
|
||||
Цена: малая, но это откладывание решения: читателю всё равно придётся
|
||||
выбирать.
|
||||
|
||||
## Рекомендация
|
||||
|
||||
**(1)**, если единицы вообще могут меняться на живом потоке — а они могут.
|
||||
Сегодняшний (2) безопасен, но оставляет систематическую ложь в подписи.
|
||||
|
||||
## Что стоит без решения
|
||||
|
||||
Ничего не теряется: сохранённые единицы больше **не перезаписываются** молча
|
||||
(было — пришедшие всегда побеждали), расхождение считается в
|
||||
`MergeStats.UnitsConflicts` и даёт `WARN` в свёртке.
|
||||
|
||||
Отдельная тонкость, которую надо закрыть тем же решением: единицы не входят в
|
||||
хеш содержимого, поэтому доставка с теми же точками и исправленными единицами
|
||||
уходит по ветке «ничего не изменилось» и колонку не трогает. То есть единицы
|
||||
сегодня обновляются тогда и только тогда, когда изменилось содержимое, —
|
||||
правило, которого никто не формулировал.
|
||||
@@ -1,58 +1,60 @@
|
||||
# Судьба доставки, у которой разобрана не вся секция data
|
||||
# Непокрытые секции доставки видны в статусе разбора
|
||||
|
||||
**Приоритет:** блокеры
|
||||
**Приоритет:** высокий
|
||||
|
||||
Вынуто ревью кода задачи `razbor-metrik-v-obekty` (профиль `deep`, проход
|
||||
негативного пространства). **Решить до реализации ретеншена.**
|
||||
Была блокером, вынутым ревью кода задачи `razbor-metrik-v-obekty` (профиль
|
||||
`deep`, проход негативного пространства). **Решение принято** — ниже задача.
|
||||
|
||||
## Что решить
|
||||
## Что не так сегодня
|
||||
|
||||
Разбор читает только `data.metrics`. Доставка, состоящая из `workouts`,
|
||||
`stateOfMind`, `symptoms` или `ecg`, помечается `parse_status=parsed` с нулём
|
||||
точек — неотличимо от доставки с пустой секцией метрик.
|
||||
|
||||
## Почему это блокер, а не задача
|
||||
Само по себе некритично: секции пока не разбираются сознательно, тела лежат в
|
||||
архиве. Опасность в **сцепке с ретеншеном**. Ретеншен по замыслу срезает архив
|
||||
до следующего проверенного экспорта. Если он будет ориентироваться на
|
||||
`parse_status`, он снесёт тела, которые числятся разобранными, — а для
|
||||
`stateOfMind` это необратимо: **в экспорте Apple его нет** (находка 46),
|
||||
доставки HAE для него единственный источник.
|
||||
|
||||
Само по себе это некритично: секции пока не разбираются сознательно, тела
|
||||
лежат в архиве. Опасность в **сцепке с ретеншеном**.
|
||||
Цена ошибки здесь не «придётся пересобрать», а «истории состояния разума больше
|
||||
не существует».
|
||||
|
||||
Ретеншен по замыслу срезает архив до следующего проверенного экспорта. Если он
|
||||
будет ориентироваться на `parse_status`, он снесёт тела, которые числятся
|
||||
разобранными, — а для `stateOfMind` это необратимо: **в экспорте Apple его
|
||||
нет** (находка 46), доставки HAE для него единственный источник.
|
||||
## Что решено
|
||||
|
||||
То есть цена ошибки здесь не «придётся пересобрать», а «истории состояния
|
||||
разума больше не существует».
|
||||
Вариант (1): **разбор возвращает список верхнеуровневых ключей `data`, которые
|
||||
он не покрыл; статус доставки — `partial`.**
|
||||
|
||||
## Варианты и цена
|
||||
Почему он, а не альтернативы:
|
||||
|
||||
**(1) Разбор возвращает список верхнеуровневых ключей `data`, которые он не
|
||||
покрыл; статус `partial`.**
|
||||
Цена: малая — один `json.Decoder` по верхнему уровню, без чтения содержимого.
|
||||
Ретеншен и `reindex` получают честный признак, а в логе появляется момент,
|
||||
когда поток принёс новую секцию (ровно то, ради чего заведена задача
|
||||
`proverka-novyh-sekcij`).
|
||||
- Считать `parsed` только полностью разобранную доставку, а остальные держать
|
||||
в `pending` — дёшево, но `pending` перестаёт означать «ещё не смотрели», и
|
||||
подбор зависших доставок теряет свой признак. А подбор `pending` как раз
|
||||
появляется задачей [otvet-i-svyortka](otvet-i-svyortka.md).
|
||||
- Запретить ретеншену смотреть на `parse_status` — нулевая цена сейчас, но
|
||||
ретеншен становится тупым и не защищает от «тело разобрано неверно, а мы его
|
||||
уже срезали».
|
||||
|
||||
**(2) Считать `parsed` только доставку, у которой разобрано всё; остальные —
|
||||
`pending`.**
|
||||
Цена: малая, но `pending` перестаёт означать «ещё не смотрели», и подбор
|
||||
зависших доставок теряет свой признак.
|
||||
Список непокрытых ключей — дешёвая честность, и он же закрывает задачу «не
|
||||
пропустить момент, когда поедет новая секция».
|
||||
|
||||
**(3) Ничего не менять, но ретеншену запретить смотреть на `parse_status` —
|
||||
резать только по дате экспорта.**
|
||||
Цена: нулевая сейчас, но ретеншен становится тупым и не защищает от «тело
|
||||
разобрано неверно, а мы его уже срезали».
|
||||
## Что делать
|
||||
|
||||
## Рекомендация
|
||||
1. `hae.Parse`: перечислить верхнеуровневые ключи `data` и вернуть те, что
|
||||
разбор не покрыл. Один `json.Decoder` по верхнему уровню, **без чтения
|
||||
содержимого** — секции доходят до десятков МиБ.
|
||||
2. Статус `partial` рядом с `parsed`/`failed`; миграция, если статус хранится
|
||||
ограниченным набором.
|
||||
3. Свёртка пишет непокрытые ключи в исход доставки и логирует их один раз —
|
||||
именами ключей, без содержимого.
|
||||
4. Тест на фикстуре с секцией, которой разбор не знает: статус `partial`,
|
||||
ключ в списке, точки метрик при этом сохранены.
|
||||
|
||||
**(1).** Список непокрытых ключей — дешёвая честность, и он же закрывает
|
||||
задачу «не пропустить момент, когда поедет новая секция».
|
||||
## Связано
|
||||
|
||||
## Что стоит без решения
|
||||
|
||||
Ничего: сегодня ретеншена нет, тела не удаляются. Задача — не дать сцепке
|
||||
сложиться позже.
|
||||
|
||||
Связано: [retenshen-syrogo-arhiva](retenshen-syrogo-arhiva.md) — решить **до**
|
||||
неё; [proverka-novyh-sekcij](proverka-novyh-sekcij.md) — тот же признак закрыл
|
||||
бы и её.
|
||||
- [retenshen-syrogo-arhiva](retenshen-syrogo-arhiva.md) — решить **до** неё.
|
||||
- [proverka-novyh-sekcij](proverka-novyh-sekcij.md) — тот же признак закрывает
|
||||
и её: момент появления новой секции становится событием в логе.
|
||||
- [trenirovki-i-zapisi](trenirovki-i-zapisi.md) — по мере разбора секций список
|
||||
непокрытых сокращается сам.
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
# Разнести ответ приёма и свёртку доставки
|
||||
|
||||
**Приоритет:** блокеры
|
||||
**Приоритет:** высокий
|
||||
|
||||
Вынуто ревью кода задачи `razbor-metrik-v-obekty` (профиль `deep`, находка №4
|
||||
триажа, severity major).
|
||||
Была блокером, вынутым ревью кода задачи `razbor-metrik-v-obekty` (профиль
|
||||
`deep`, находка №4 триажа, severity major). **Решение принято** — ниже задача.
|
||||
|
||||
## Что решить
|
||||
## Что не так сегодня
|
||||
|
||||
Свёртка выполняется **синхронно внутри обработчика запроса**, поэтому время
|
||||
ответа равно времени свёртки. Вопрос: разносить ли их, и какой ценой.
|
||||
|
||||
## Оракул: измерено
|
||||
ответа равно времени свёртки.
|
||||
|
||||
`WriteTimeout` в Go ставится в `readRequest` — **до** чтения тела и до вызова
|
||||
обработчика (`net/http/server.go:993-997`, прочитано в исходниках). Значит
|
||||
@@ -46,28 +44,39 @@ client: elapsed=501ms err=EOF
|
||||
экспорт, — то есть ровно по тем, ради которых заведён инвариант «дыры
|
||||
закрываются сами».
|
||||
|
||||
## Варианты и цена
|
||||
## Что решено
|
||||
|
||||
**(а) Отвечать `200` сразу после архивации и учёта; свёртка — воркером в
|
||||
порядке журнала, с подбором `pending` при старте.**
|
||||
Цена: средняя — воркер, очередь, подбор при старте. Бонусом закрываются ещё
|
||||
две дыры: параллельные доставки одной автоматизации перестают гонять
|
||||
наследование слоя (сейчас вторая может не найти слоя первой и уйти в
|
||||
`failed`), и доставка, застрявшая в `pending` из-за сбоя записи, наконец
|
||||
кем-то подбирается.
|
||||
Вариант (а): **отвечать `200` сразу после архивации и учёта; свёртка —
|
||||
воркером в порядке журнала, с подбором `pending` при старте.**
|
||||
|
||||
**(б) Поднять `write_timeout` до согласованного с `foldTimeout`.**
|
||||
Цена: малая. Но худший случай (64 МиБ) всё равно минуты, и молчание
|
||||
`accessLog` остаётся.
|
||||
Почему он, а не альтернативы:
|
||||
|
||||
**(в) Оставить как есть, задокументировав потолок размера доставки.**
|
||||
Цена: нулевая. Широкие проходы продолжают рваться.
|
||||
- Поднять `write_timeout` до согласованного с `foldTimeout` — дёшево, но
|
||||
худший случай (64 МиБ) всё равно минуты, и молчание `accessLog` остаётся.
|
||||
Это лечит симптом.
|
||||
- Оставить как есть — широкие проходы продолжают рваться.
|
||||
|
||||
## Рекомендация
|
||||
Вариант (а) решает причину и попутно снимает две смежные дыры: параллельные
|
||||
доставки одной автоматизации перестают гонять наследование слоя (сейчас вторая
|
||||
может не найти слоя первой и уйти в `failed`), и доставка, застрявшая в
|
||||
`pending` из-за сбоя записи, наконец кем-то подбирается.
|
||||
|
||||
**(а).** Единственный вариант, который решает причину, а не симптом, и попутно
|
||||
снимает две смежные находки. Он же приближает `reindex`: подбор `pending` при
|
||||
старте — его половина.
|
||||
## Что делать
|
||||
|
||||
1. Воркер свёртки: одна горутина, очередь идентификаторов доставок, обработка
|
||||
**строго в порядке журнала** (`received_at`, `id`) — от этого зависит
|
||||
наследование слоя и воспроизводимость.
|
||||
2. Приём отвечает `200` после архивации и вставки доставки; свёртку ставит в
|
||||
очередь. Очередь переполнена — доставка остаётся `pending`, это не отказ.
|
||||
3. Подбор `pending` при старте, тем же путём. Это половина `reindex`, поэтому
|
||||
код должен быть общим с ним, а не соседним.
|
||||
4. Остановка сервиса дожидается текущей доставки: свёртка — одна транзакция,
|
||||
рвать её нечем, но очередь надо дренировать осознанно.
|
||||
5. Метка «доставка ждала свёртки дольше N» — в наблюдаемость, чтобы отставание
|
||||
воркера было видно до того, как оно станет отставанием на сутки.
|
||||
6. Тесты: порядок журнала соблюдается при конкурентных доставках; `pending`
|
||||
подбирается при старте; отмена контекста не оставляет половинчатого
|
||||
состояния; `task verify:archive` даёт то же состояние.
|
||||
|
||||
## Что стоит без решения
|
||||
|
||||
@@ -75,6 +84,9 @@ client: elapsed=501ms err=EOF
|
||||
широких доставках. Данные при этом не теряются — тело ложится в архив **до**
|
||||
свёртки.
|
||||
|
||||
Связано: [reindex-iz-arhiva](reindex-iz-arhiva.md),
|
||||
[stats-nablyudaemost](stats-nablyudaemost.md) — метка «ответ не уложился в
|
||||
таймаут» должна попасть туда.
|
||||
## Связано
|
||||
|
||||
- [reindex-iz-arhiva](reindex-iz-arhiva.md) — подбор `pending` это её половина;
|
||||
делать одним кодом.
|
||||
- [stats-nablyudaemost](stats-nablyudaemost.md) — метка «ответ не уложился в
|
||||
таймаут» и отставание воркера должны попасть туда.
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
# Правило выбора победителя при столкновении точек
|
||||
# Полнота точки — по множеству ключей, а не по их числу
|
||||
|
||||
**Приоритет:** блокеры
|
||||
**Приоритет:** высокий
|
||||
|
||||
Вынуто ревью кода задачи `razbor-metrik-v-obekty` (профиль `deep`, находка №7
|
||||
триажа, severity major). Трогает записанный инвариант «при столкновении
|
||||
выигрывает более полная точка».
|
||||
Была блокером, вынутым ревью кода задачи `razbor-metrik-v-obekty` (профиль
|
||||
`deep`, находка №7 триажа, severity major). **Решение принято замером**
|
||||
(находка 49) — ниже задача на остаток.
|
||||
|
||||
## Что решить
|
||||
## Что не так сегодня
|
||||
|
||||
Как выбирать победителя, когда по одним координатам приехали разные
|
||||
содержимые. Сегодняшнее правило измеримо неверно в двух местах.
|
||||
|
||||
## Оракул: измерено
|
||||
|
||||
**Полнота — это счётчик ключей**, чьё значение не `null`, не `""` и не пусто.
|
||||
Полнота — это **счётчик** ключей, чьё значение не `null`, не `""` и не пусто.
|
||||
Поэтому `{}`, `[]`, `0` и `false` считаются содержательными:
|
||||
|
||||
```
|
||||
@@ -24,42 +19,54 @@
|
||||
Вторая точка — настоящее измерение — проигрывает первой и стирается
|
||||
безвозвратно. Восстановить можно только из сырого архива, пока он жив.
|
||||
|
||||
**Тай-брейк при равной полноте** — лексикографический порядок канонических
|
||||
форм, то есть исход зависит от самого значения. Для накопительной метрики,
|
||||
которую досчитывают задним числом, это систематическая победа **меньшего**
|
||||
числа: `qty:1` бьёт `qty:100`. Недосчёт, неотличимый от нормы.
|
||||
## Что решено и на каком основании
|
||||
|
||||
Частота на живом потоке **не измерена** — стоит прогнать по архиву до решения.
|
||||
Замер по всем 99 доставкам (находка 49): настоящих столкновений 2 897 из
|
||||
444 256 координат (0.65%), и среди них **ноль несравнимых наборов полей**.
|
||||
|
||||
## Варианты и цена
|
||||
Отсюда:
|
||||
|
||||
**(1) Полнота по множеству ключей: побеждает надмножество; несравнимые
|
||||
множества — объединять поля, а не выбирать точку целиком.**
|
||||
Цена: средняя — правка спеки, `resolve` и тестов. Самое честное прочтение
|
||||
инварианта «ничего не теряем молча»: при несравнимых наборах не теряется
|
||||
ничего вообще.
|
||||
- **Полнота — сравнение множеств ключей, побеждает надмножество.** Ровно 981
|
||||
случай из замера, и там правило уже работает верно — но работает случайно,
|
||||
через счётчик, который на другом входе даст обратное.
|
||||
- **Объединение полей при несравнимых наборах не нужно.** Самая дорогая часть
|
||||
обсуждавшегося варианта (1) на живом потоке не срабатывает ни разу. Вместо
|
||||
реализации — счётчик: несравнимый набор считается и логируется `WARN`. Если
|
||||
событие когда-нибудь наступит, оно будет видно, а не додумано заранее.
|
||||
- **Тай-брейк при равной полноте в эту задачу не входит.** Обоснование ниже.
|
||||
|
||||
**(2) Оставить счёт ключей, но не считать содержательными `{}`, `[]`, `0`,
|
||||
`false`, `""`.**
|
||||
Цена: малая. Правило остаётся играбельным (точку с лишними непустыми полями
|
||||
никто не мешает прислать), и произвол тай-брейка не решён.
|
||||
## Что делать
|
||||
|
||||
**(3) При равной полноте побеждает точка более поздней доставки по
|
||||
`received_at`.**
|
||||
Цена: малая, но спека должна признать зависимость от журнала, и нужен
|
||||
детерминированный порядок **внутри** одной доставки — а именно там измерены
|
||||
столкновения (31 координата в 21 доставке из 89).
|
||||
1. `resolve` в `internal/store/bucket.go`: полнота — множество ключей с
|
||||
непустым значением; побеждает строгое надмножество.
|
||||
2. Несравнимые множества — счётчик в `MergeStats` плюс `WARN` с координатами
|
||||
объекта (без значений). Точку выбирает тот же тай-брейк, что и при равной
|
||||
полноте.
|
||||
3. Дельта-спека `openspec/specs/storage` → «Разрешение столкновений по
|
||||
полноте»: переформулировать с числа ключей на множество.
|
||||
4. Тесты: «бедная точка с пятью пустыми полями не стирает измерение»
|
||||
(регрессия на приведённой выше паре), «надмножество побеждает»,
|
||||
«несравнимые наборы считаются».
|
||||
5. `task verify:archive` обязан дать то же состояние — правило меняет исход
|
||||
только там, где сегодня он неверен.
|
||||
|
||||
## Рекомендация
|
||||
## Что отложено и почему
|
||||
|
||||
**(1).** Объединение полей при несравнимых наборах — единственный вариант, при
|
||||
котором столкновение вообще перестаёт быть выбором «кого потерять».
|
||||
**Тай-брейк при равной полноте.** Сегодня это лексикографический порядок
|
||||
канонических форм. Измерено (находка 49): в 1 847 случаях из 1 912 он выбирает
|
||||
**меньшее** значение — 96%. Четыре из шести затронутых метрик накопительные,
|
||||
там это систематический недосчёт; но крупнейшая группа, `heart_rate` (985
|
||||
случаев), мгновенная, и там «большее» не правильнее.
|
||||
|
||||
То есть правильный тай-брейк зависит от **рода метрики**, а род по замыслу
|
||||
проекта измеряется сверкой слоёв между собой. Выбирать его сейчас — угадывать
|
||||
ровно то, что станет известно точно.
|
||||
|
||||
Зависимость: [rod-agregacii-i-katalog](rod-agregacii-i-katalog.md). Тай-брейк
|
||||
доделывается **после** неё, отдельной задачей.
|
||||
|
||||
## Что стоит без решения
|
||||
|
||||
Правило работает и теперь **наблюдаемо**: столкновение считается расхождением
|
||||
канонических форм (а не байтов, как было), даёт `WARN` с координатами объекта
|
||||
и счётчик `overwrites`. Если правило начнёт терять — это станет видно в логе,
|
||||
а не через месяцы при сверке с экспортом Apple.
|
||||
|
||||
Связано: `openspec/specs/storage` → «Разрешение столкновений по полноте».
|
||||
Ничего. Правило работает и **наблюдаемо**: столкновение считается расхождением
|
||||
канонических форм (а не байтов), даёт `WARN` с координатами объекта и счётчик
|
||||
`overwrites`.
|
||||
|
||||
@@ -17,5 +17,11 @@ HAE. Он не сэмплы, а посекундная развёртка (на
|
||||
Готово, когда каталог отдаёт по каждой метрике единицы, род и список слоёв с
|
||||
диапазонами, а род проставлен измерением на живой истории.
|
||||
|
||||
От этой задачи зависит ещё одно решение: тай-брейк при равной полноте точек.
|
||||
Измерено (находка 49), что сегодняшний лексикографический порядок берёт меньшее
|
||||
значение в 96% случаев — для накопительных это недосчёт, для мгновенных
|
||||
безразлично. Пока рода нет, выбирать нечем; когда каталог появится, тай-брейк
|
||||
доделывается по нему — см. [pravilo-sliyaniya-tochek](pravilo-sliyaniya-tochek.md).
|
||||
|
||||
Связано: `docs/architecture.md` → «Слои гранулярности», план шаг 4.
|
||||
|
||||
|
||||
@@ -1541,6 +1541,78 @@ type sourceName sourceVersion creationDate startDate endDate value
|
||||
То есть обе распространённые схемы хранения теряют данные ровно там, где мы это
|
||||
измерили, а единственная принятая схема дедупликации — это `start`+`end`.
|
||||
|
||||
## 48. Единицы метрики на живом потоке не менялись ни разу
|
||||
|
||||
Замер по всем 99 доставкам архива: **30 различных метрик, ни у одной единицы не
|
||||
менялись**. Ни между доставками, ни внутри одной.
|
||||
|
||||
Это снимает основание под гипотезой «единицы — часть координаты объекта»: разряд
|
||||
`метрика + слой + единицы + час` защищал бы от события, которого поток не
|
||||
производит. Сегодняшнее поведение (сохранённые единицы побеждают, расхождение —
|
||||
`MergeStats.UnitsConflicts` и `WARN`) правильно ровно тем, что превращает
|
||||
гипотезу в **наблюдаемое** событие: если единицы когда-нибудь поедут, это будет
|
||||
видно в логе в тот же день, а не через квартал при сверке с экспортом.
|
||||
|
||||
Оговорка, которая остаётся верной и с этим решением: единицы не входят в хеш
|
||||
содержимого, поэтому доставка с теми же точками и другими единицами уходит по
|
||||
ветке «ничего не изменилось». При правиле «сохранённое побеждает» это не
|
||||
расхождение, а то же самое правило, — но если правило когда-нибудь поменяют,
|
||||
хеш придётся менять вместе с ним.
|
||||
|
||||
## 49. Столкновений 0.65%, несравнимых наборов полей нет, тай-брейк системно берёт меньшее
|
||||
|
||||
Замер по всем 99 доставкам, ключ — реальный (`метрика + слой + начало + конец`),
|
||||
сравнение — по канонической форме с округлением до 12 значащих цифр
|
||||
(находка 30).
|
||||
|
||||
| что мерялось | столкновений | из 444 256 |
|
||||
|---|---|---|
|
||||
| без слоя в ключе | 53 678 | 12.29% |
|
||||
| со слоем, побайтово | 47 235 | 10.63% |
|
||||
| **со слоем, после канонизации** | **2 897** | **0.65%** |
|
||||
|
||||
Первая строка меряет не то: без слоя часовая точка сталкивается с минутной, и
|
||||
это не столкновение, а два разных ряда. Разница между второй и третьей —
|
||||
дребезг последних разрядов `float64`, то есть **94% побайтовых расхождений
|
||||
канонизация схлопывает**. Это же и есть независимое подтверждение находки 30:
|
||||
округление до 12 цифр выбрано верно.
|
||||
|
||||
Разложение настоящих 2 897:
|
||||
|
||||
| случай | сколько | что делает правило |
|
||||
|---|---|---|
|
||||
| наборы полей **сравнимы** (одно надмножество другого) | 981 | правило полноты работает верно |
|
||||
| наборы полей **равны**, значения разные | 1 916 | срабатывает тай-брейк |
|
||||
| наборы полей **несравнимы** | **0** | не встречается вовсе |
|
||||
|
||||
**Ноль несравнимых наборов** — важный результат: объединение полей при
|
||||
несравнимых наборах, самая дорогая часть обсуждавшегося правила слияния, на
|
||||
живом потоке не срабатывает ни разу. Правило полноты сводится к «надмножество
|
||||
побеждает», и это не упрощение из лени, а измеренная форма данных.
|
||||
|
||||
А вот тай-брейк при равной полноте измеримо смещён. Там, где сравнение чисел
|
||||
определено (1 912 случаев из 1 916), лексикографический порядок канонических
|
||||
форм выбирает **меньшее значение в 1 847 случаях — 96%**:
|
||||
|
||||
```
|
||||
heart_rate 985
|
||||
walking_running_distance 562
|
||||
step_count 509
|
||||
active_energy 409
|
||||
basal_energy_burned 385
|
||||
apple_stand_time 14
|
||||
```
|
||||
|
||||
Четыре метрики из шести — накопительные, которые HAE досчитывает задним числом
|
||||
(находка 10): там «меньшее» это систематический недосчёт, порядка 0.4%
|
||||
координат. Но самая крупная группа, `heart_rate`, — мгновенная, и там «большее»
|
||||
не правильнее, там просто пересэмплирование.
|
||||
|
||||
Отсюда вывод, которого не было до замера: **правильный тай-брейк зависит от
|
||||
рода метрики**, а род по замыслу проекта измеряется сверкой слоёв между собой.
|
||||
Значит выбирать тай-брейк до каталога рода агрегации — значит угадывать ровно
|
||||
то, что через задачу станет известно точно.
|
||||
|
||||
## Инструмент
|
||||
|
||||
Разбор ведётся скриптом `tmp/research/hl.py` (Python 3, только стандартная
|
||||
|
||||
Reference in New Issue
Block a user