tie-break: записан диагноз красного verify:archive и снято прежнее решение
- байтовый тай-брейк решает 98,8% спорных координат из 84 978: step_count потерял род, потому что сохранённая точка выиграла у более поздней - вариант (б) «брать бо́льшее» снят, взято «пришедшая побеждает сохранённую»; оно не полурешётка, и задача обязана доказать равенство пересборки приёму
This commit is contained in:
@@ -33,7 +33,7 @@
|
|||||||
- [Выведенные из данных схемы содержимого](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
|
- [Выведенные из данных схемы содержимого](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
|
||||||
- [[idea] Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено
|
- [[idea] Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено
|
||||||
- [Сверка живой витрины с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит
|
- [Сверка живой витрины с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит
|
||||||
- [Тай-брейк при равной полноте точек](items/tie-break-equal-completeness.md) — При равной полноте порядок канонических форм берёт меньшее значение в 96% случаев — у накопительных это систематический недосчёт
|
- [Тай-брейк при равной полноте точек](items/tie-break-equal-completeness.md) — Байтовый тай-брейк решает 98,8% спорных координат и хранит устаревшую точку: step_count потерял род, verify:archive красный
|
||||||
- [Устаревание нижнего слоя после экспорта](items/lower-layer-expiry.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
|
- [Устаревание нижнего слоя после экспорта](items/lower-layer-expiry.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
|
||||||
- [[idea] Выгрузка в parquet отдельной командой](items/parquet-export.md) — Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно
|
- [[idea] Выгрузка в parquet отдельной командой](items/parquet-export.md) — Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно
|
||||||
- [Заголовки доставки в архиве рядом с телом](items/delivery-headers-in-archive.md) — Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке
|
- [Заголовки доставки в архиве рядом с телом](items/delivery-headers-in-archive.md) — Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке
|
||||||
|
|||||||
@@ -1,24 +1,124 @@
|
|||||||
# Тай-брейк при равной полноте точек
|
# Тай-брейк при равной полноте точек
|
||||||
|
|
||||||
- **Секция:** ядро
|
- **Секция:** ядро
|
||||||
- **Зачем:** При равной полноте порядок канонических форм берёт меньшее значение в 96% случаев — у накопительных это систематический недосчёт
|
- **Зачем:** Байтовый тай-брейк решает 98,8% спорных координат и хранит устаревшую точку: step_count потерял род, verify:archive красный
|
||||||
- **Теги:** goal:merge-robustness
|
- **Теги:** goal:merge-robustness
|
||||||
|
|
||||||
**Решение принято владельцем 2026-08-02: вариант (б) — брать бо́льшее значение
|
**Решение владельца 2026-08-04, заменяет прежнее: при равной полноте побеждает
|
||||||
точки.** Ниже — исходная постановка блокера, она же ТЗ; рекомендация в конце
|
пришедшая точка, а не сохранённая.** Байтовый порядок канонических форм
|
||||||
файла и есть выбранный вариант.
|
остаётся **только** для столкновений внутри одной доставки, где провенанс общий
|
||||||
|
и различать нечем. Это облегчённая форма варианта «в» — без колонки провенанса
|
||||||
|
и без миграции.
|
||||||
|
|
||||||
Что важно не потерять при реализации: правило обязано остаться **тотальным** —
|
**Прежнее решение от 2026-08-02 — вариант (б), брать бо́льшее значение — снято.**
|
||||||
числа у точки нет, значит откат на порядок канонических форм, — и обязано
|
Причина: разбор красного `verify:archive` (2026-08-04) возразил, что «большее»
|
||||||
остаться полурешёткой: `max` коммутативен, ассоциативен и идемпотентен, поэтому
|
неверно для мгновенных метрик, которые досчитываются **вниз**, а постановка
|
||||||
воспроизводимость свёртки не страдает. Род агрегации в правило **не входит**:
|
утверждала обратное; спор не был закрыт замером, и вместо него взято правило,
|
||||||
род есть функция витрины, и правило слияния, читающее собственную выдачу,
|
которое значение вообще не интерпретирует.
|
||||||
повторяет дефект наследования слоя «из будущего» (`docs/review.md`,
|
|
||||||
2026-08-01).
|
|
||||||
|
|
||||||
Приёмка та, что названа ниже: на прогоне живого архива отпечаток витрины обязан
|
**Что при этом сломано намеренно и обязано быть починено задачей.** «Пришедшая
|
||||||
**измениться** (иначе правило не сработало), а число столкновений с равной
|
побеждает» — не полурешётка: правило перестаёт быть коммутативным, и исход
|
||||||
полнотой — остаться прежним.
|
слияния становится функцией **порядка свёртки**. Прежняя постановка называла
|
||||||
|
полурешётку обязательной ровно затем, чтобы витрина оставалась свёрткой
|
||||||
|
журнала. Значит задача обязана определить порядок так, чтобы **пересборка
|
||||||
|
равнялась приёму**, и доказать это оракулом, а не рассуждением. Осложняющий
|
||||||
|
факт назван в соседней задаче: `replay` идёт по ULID, а живая свёртка — по
|
||||||
|
факту свёртки, и доставка, отложенная занятостью базы, их разводит
|
||||||
|
([journal-order-on-ingest](journal-order-on-ingest.md)). Если развести их не
|
||||||
|
удаётся — это вопрос владельцу, а не повод принять расхождение молча.
|
||||||
|
|
||||||
|
Род агрегации в правило **не входит**: род есть функция витрины, и правило
|
||||||
|
слияния, читающее собственную выдачу, повторяет дефект наследования слоя «из
|
||||||
|
будущего» (`docs/review.md`, 2026-08-01).
|
||||||
|
|
||||||
|
## Критерии приёмки
|
||||||
|
|
||||||
|
- ни одна метрика не теряет род из-за столкновения равной полноты; `step_count`
|
||||||
|
снова накопительная — оракул: `task verify:archive`, ноль противоречащих
|
||||||
|
часов
|
||||||
|
- живая свёртка и пересборка дают **один** отпечаток витрины — оракул:
|
||||||
|
`healthlog reindex` против живого состояния; это же и есть страховка от
|
||||||
|
потерянной коммутативности
|
||||||
|
- повторный прогон реплея даёт тот же отпечаток — оракул: `task verify:archive`,
|
||||||
|
второй прогон подряд
|
||||||
|
- столкновение **внутри одной доставки** разрешается прежним байтовым порядком —
|
||||||
|
оракул: тест на двух точках одной доставки с равной полнотой
|
||||||
|
- «пришедшая точка проиграла сохранённой» считается отдельно от общего
|
||||||
|
`MergeStats.Overwrites` — оракул: тест плюс прогон на живом архиве, число
|
||||||
|
сходится с замером 2026-08-04
|
||||||
|
|
||||||
|
## Рамки
|
||||||
|
|
||||||
|
Схему не трогаем: колонки провенанса на точку не заводим. Отпечаток витрины
|
||||||
|
обязан **измениться** — иначе правило не сработало. Пересборка обязательна и
|
||||||
|
делается человеком при остановленном сервисе; подмена файла базы — необратимое
|
||||||
|
действие и здесь не выполняется.
|
||||||
|
|
||||||
|
## Диагноз 2026-08-04: у дефекта появился независимый оракул
|
||||||
|
|
||||||
|
`task verify:archive` покраснел на `master` без единого коммита — отказ приехал
|
||||||
|
с ростом корпуса. Разбор довёл до причины, и причина эта.
|
||||||
|
|
||||||
|
**Противоречащий час — `2026-08-03T07:00Z`, метрика `step_count`.** Часовой слой
|
||||||
|
несёт одну точку `V`, минутный — две точки, каждая ровно `V`. Значит
|
||||||
|
`sum = 2V`, `mean = V`, и сверка слоёв объявляет метрику **мгновенной** — один
|
||||||
|
голос против 22 накопительных. `internal/catalog/catalog.go:498` уводит
|
||||||
|
`step_count` в `unknown`, то есть **суммировать шаги Read API больше не имеет
|
||||||
|
права**.
|
||||||
|
|
||||||
|
**Правильное значение лежит в архиве.** На координату приехало 4 точки, два
|
||||||
|
различных значения: `V` от первой доставки и `2V` от трёх последующих, при
|
||||||
|
совпадающих `source` и офсете. `2V` в точности равно сумме минутных точек, то
|
||||||
|
есть даёт накопительную, как остальные 22 часа. Побеждает `V` — потому что
|
||||||
|
каноническая форма меньшего числа сортируется первой
|
||||||
|
(`internal/store/bucket.go:517` не находит превосходства по полноте, управление
|
||||||
|
уходит на `bucket.go:523`, `bytes.Compare(canon.SortKey(...))`).
|
||||||
|
|
||||||
|
**Корпусный замер (453 171 координата) переоценивает масштаб:**
|
||||||
|
|
||||||
|
| | координат |
|
||||||
|
| --- | --- |
|
||||||
|
| спорных (больше одной канонической формы) | 84 978 |
|
||||||
|
| решено полнотой | 1 022 (1,2%) |
|
||||||
|
| **упало на байтовый тай-брейк** | **83 956 (98,8%)** |
|
||||||
|
|
||||||
|
Прежняя оценка «1916 столкновений, 0.43% координат» была снята на меньшем
|
||||||
|
корпусе и считала другое. Инвариант «выигрывает более полная точка» на живом
|
||||||
|
потоке решает 1,2% спорных координат; в остальных 98,8% исход определяет
|
||||||
|
лексикографический порядок канонического JSON.
|
||||||
|
|
||||||
|
**Потеря не наблюдаема.** `MergeStats.Overwrites` считает столкновение в обе
|
||||||
|
стороны и на этом корпусе сработал бы ~84 000 раз; отличить «оставили новое» от
|
||||||
|
«выбросили новое» по нему нельзя. Счётчика «пришедшая точка проиграла
|
||||||
|
сохранённой при равной полноте» не существует, и завести его — часть этой
|
||||||
|
задачи.
|
||||||
|
|
||||||
|
**Возражение против выбранного варианта (б), которого при выборе не звучало.**
|
||||||
|
Разбор отвергает «брать бо́льшее» тем, что оно неверно для мгновенных метрик,
|
||||||
|
которые досчитываются **вниз**. Постановка ниже утверждает обратное — что для
|
||||||
|
мгновенных выбор безразличен, поскольку это пересэмплирование. Одно из двух
|
||||||
|
утверждений неверно, и решает это замер, а не рассуждение: до реализации надо
|
||||||
|
проверить на живом корпусе, встречается ли мгновенная метрика, у которой
|
||||||
|
поздняя версия точки меньше ранней.
|
||||||
|
|
||||||
|
Разбор предлагает вместо (б) разрешать равную полноту **позицией в журнале**
|
||||||
|
(вариант «в» ниже) либо его облегчённую форму: при равной полноте предпочитать
|
||||||
|
пришедшую точку сохранённой, оставив байтовый порядок только для столкновений
|
||||||
|
внутри одной доставки, где провенанс общий. Первое требует смены формата
|
||||||
|
`payload` (миграция `00003_bucket.sql`), пересборки витрины и смены отпечатка;
|
||||||
|
второе схему не трогает. У обоих одна общая развилка: `replay` идёт по ULID, а
|
||||||
|
живая свёртка — по факту свёртки, и отложенная занятостью доставка их разводит;
|
||||||
|
«позиция в журнале» обязана быть определена так, чтобы пересборка равнялась
|
||||||
|
приёму.
|
||||||
|
|
||||||
|
Ужесточение измерения (`minFinePoints` 2→3) замерено и отвергнуто как плохой
|
||||||
|
размен: чинит `step_count`, но роняет `blood_oxygen_saturation`,
|
||||||
|
`environmental_audio_exposure` и `stair_speed_up` в `unknown` — и прячет
|
||||||
|
неверное хранимое, из-за которого Read API отдаёт за этот час половину шагов.
|
||||||
|
|
||||||
|
Не проверено: останется ли измерение бесконфликтным после починки — изменится
|
||||||
|
81 343 координаты. Тот же ли дефект у слияния сущностей (`internal/store/winner.go`,
|
||||||
|
отношение другое) — не мерялось.
|
||||||
|
|
||||||
## Что происходит
|
## Что происходит
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user