From de2001dea6f400dbe1ee153b69be7a525c755b1e Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Tue, 4 Aug 2026 07:55:11 +0300 Subject: [PATCH] =?UTF-8?q?tie-break:=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE=D0=B7=20?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D1=81=D0=BD=D0=BE=D0=B3=D0=BE=20verify:archi?= =?UTF-8?q?ve=20=D0=B8=20=D1=81=D0=BD=D1=8F=D1=82=D0=BE=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B6=D0=BD=D0=B5=D0=B5=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - байтовый тай-брейк решает 98,8% спорных координат из 84 978: step_count потерял род, потому что сохранённая точка выиграла у более поздней - вариант (б) «брать бо́льшее» снят, взято «пришедшая побеждает сохранённую»; оно не полурешётка, и задача обязана доказать равенство пересборки приёму --- docs/tasks/BACKLOG.md | 2 +- .../items/tie-break-equal-completeness.md | 128 ++++++++++++++++-- 2 files changed, 115 insertions(+), 15 deletions(-) diff --git a/docs/tasks/BACKLOG.md b/docs/tasks/BACKLOG.md index abb30ca..de2ad7c 100644 --- a/docs/tasks/BACKLOG.md +++ b/docs/tasks/BACKLOG.md @@ -33,7 +33,7 @@ - [Выведенные из данных схемы содержимого](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке - [[idea] Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено - [Сверка живой витрины с пересборкой](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 он избыточен - [[idea] Выгрузка в parquet отдельной командой](items/parquet-export.md) — Дверь для тяжёлой аналитики снаружи: DuckDB читает и parquet, и файл SQLite — спешить некуда, но и закрывать не нужно - [Заголовки доставки в архиве рядом с телом](items/delivery-headers-in-archive.md) — Заголовки живут только в базе — потеря базы навсегда ломает вывод слоя при пересборке diff --git a/docs/tasks/items/tie-break-equal-completeness.md b/docs/tasks/items/tie-break-equal-completeness.md index a18eda2..ef73b30 100644 --- a/docs/tasks/items/tie-break-equal-completeness.md +++ b/docs/tasks/items/tie-break-equal-completeness.md @@ -1,24 +1,124 @@ # Тай-брейк при равной полноте точек - **Секция:** ядро -- **Зачем:** При равной полноте порядок канонических форм берёт меньшее значение в 96% случаев — у накопительных это систематический недосчёт +- **Зачем:** Байтовый тай-брейк решает 98,8% спорных координат и хранит устаревшую точку: step_count потерял род, verify:archive красный - **Теги:** goal:merge-robustness -**Решение принято владельцем 2026-08-02: вариант (б) — брать бо́льшее значение -точки.** Ниже — исходная постановка блокера, она же ТЗ; рекомендация в конце -файла и есть выбранный вариант. +**Решение владельца 2026-08-04, заменяет прежнее: при равной полноте побеждает +пришедшая точка, а не сохранённая.** Байтовый порядок канонических форм +остаётся **только** для столкновений внутри одной доставки, где провенанс общий +и различать нечем. Это облегчённая форма варианта «в» — без колонки провенанса +и без миграции. -Что важно не потерять при реализации: правило обязано остаться **тотальным** — -числа у точки нет, значит откат на порядок канонических форм, — и обязано -остаться полурешёткой: `max` коммутативен, ассоциативен и идемпотентен, поэтому -воспроизводимость свёртки не страдает. Род агрегации в правило **не входит**: -род есть функция витрины, и правило слияния, читающее собственную выдачу, -повторяет дефект наследования слоя «из будущего» (`docs/review.md`, -2026-08-01). +**Прежнее решение от 2026-08-02 — вариант (б), брать бо́льшее значение — снято.** +Причина: разбор красного `verify:archive` (2026-08-04) возразил, что «большее» +неверно для мгновенных метрик, которые досчитываются **вниз**, а постановка +утверждала обратное; спор не был закрыт замером, и вместо него взято правило, +которое значение вообще не интерпретирует. -Приёмка та, что названа ниже: на прогоне живого архива отпечаток витрины обязан -**измениться** (иначе правило не сработало), а число столкновений с равной -полнотой — остаться прежним. +**Что при этом сломано намеренно и обязано быть починено задачей.** «Пришедшая +побеждает» — не полурешётка: правило перестаёт быть коммутативным, и исход +слияния становится функцией **порядка свёртки**. Прежняя постановка называла +полурешётку обязательной ровно затем, чтобы витрина оставалась свёрткой +журнала. Значит задача обязана определить порядок так, чтобы **пересборка +равнялась приёму**, и доказать это оракулом, а не рассуждением. Осложняющий +факт назван в соседней задаче: `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`, +отношение другое) — не мерялось. ## Что происходит