Дозакрыты находки ревью по слиянию сущностей
- Правило покрытия получило второй разряд (условный, как у точек), запрет вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и ряд из null больше не затирают маршрут. Победитель внутри доставки стал функцией множества версий — общим помощником с точками, — а провенанс поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину к прежнему содержимому. - Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает на старте; текст ошибки разбора не несёт значений из тела. - Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты: безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя был квадратичен по числу присланных версий одного ключа.
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
## Context
|
||||
|
||||
Семь находок дозапущенных проходов ревью (`tmp/triage-late.md`) по коммиту
|
||||
`f8200f7`. Каждая имеет прогнанный падающий оракул; оракулы переезжают обычными
|
||||
тестами пакетов, `tmp/` в `.gitignore` и жить в нём им нельзя.
|
||||
|
||||
Ограничения, которые задача не выбирает, а наследует:
|
||||
|
||||
- **Свёртка обязана быть функцией журнала.** Живая витрина и `reindex` обязаны
|
||||
сходиться отпечатком; всё, что зависит от порядка свёртки или от порядка
|
||||
элементов на проводе, — дефект по определению.
|
||||
- **Тренировка приезжает повторно, пока источник её досчитывает** (26 копий,
|
||||
три различных содержимых на живом архиве), и значения между копиями
|
||||
расходятся **всегда**. Поэтому правило полноты точек к сущностям неприменимо,
|
||||
и `Covers` существует отдельно от `Relate`.
|
||||
- **Маршрут — 95% веса тренировки**, и в экспорте Apple его нет вовсе.
|
||||
Затирание маршрута необратимо: `reindex` проиграет журнал и получит то же.
|
||||
- **Цена канонизации измерена**: тело 40 МиБ → пик кучи 768.3 МиБ; 63 МиБ →
|
||||
блокировка удерживается 5.019 с при `busy_timeout` 5000.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Обеднённая версия сущности не замещает сохранённую и не пропадает из счётчика.
|
||||
- Победитель — функция множества версий: и внутри доставки, и между доставками.
|
||||
- Провенанс сущности отражает победителя по журналу, а не первую свёрнутую копию.
|
||||
- Поле не той формы стоит одного поля, а не сущности; пропуск виден в базе.
|
||||
- Откат бинаря поверх новой схемы отказывает, а не стартует молча.
|
||||
- Каноническая форма сущности считается один раз и вне транзакции.
|
||||
- Диагностика разбора не несёт значений из тела.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Хранение сущности с `id` и неразобранной меткой (NULL-метка) — требует схемы
|
||||
и правил чтения витрины.
|
||||
- Пределы на размер одной сущности и суммарный размер секции, потоковый расчёт
|
||||
формы и хеша — новая политика, а не правка.
|
||||
- Объединение полей несравнимых версий — отвергнуто там же и по той же причине,
|
||||
что для точек.
|
||||
- Поэлементная сверка **содержимого** элементов ряда (точка маршрута без
|
||||
`altitude`) — см. «Риски».
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. `Covers` получает второй разряд, запрет вырождения формы и счёт содержательных элементов
|
||||
|
||||
Сегодня `Covers(g)` требует от `f` лишь наличия каждого содержательного ключа
|
||||
`g` и длины верхнеуровневого массива не меньше. Этого хватает, чтобы «скелет»
|
||||
(`{"maxHeartRate":1,"heartRateData":[null,null]}` против настоящей тренировки)
|
||||
признался равным настоящей и выиграл тай-брейк журнала.
|
||||
|
||||
Правило дополняется тремя проверками, все — по верхнему уровню:
|
||||
|
||||
1. **Второй разряд: ключи без содержания.** Каждый ключ `g` (включая пустые)
|
||||
обязан быть у `f`. Тот же стандарт записан для точек в `canon.Relate`:
|
||||
«иначе `{date, qty, Min:0, Max:0}` и `{date, qty}` неразличимы, и `Min` с
|
||||
`Max` исчезли бы из витрины по жребию тай-брейка». Отличие от `Relate`:
|
||||
там второй разряд применяется только при равенстве первого, здесь включение
|
||||
всех ключей требуется безусловно. Это эквивалентно при равном первом разряде
|
||||
и строже, когда первый разряд неравен, — а строже здесь и нужно: `Covers`
|
||||
отвечает «не потеряем ли содержания».
|
||||
2. **Запрет вырождения формы.** Покрывающая версия не может подменить объект
|
||||
или массив скаляром: если `g[k]` — объект, `f[k]` обязан быть объектом; если
|
||||
массив — массивом. Обратное (скаляр у `g`, объект у `f`) разрешено: `f`
|
||||
богаче формой. Стоимость — O(ключей), первый байт литерала.
|
||||
3. **Содержательные элементы массива.** Рядом с длиной считается число
|
||||
**непустых** элементов. `[null,null,null]` и `[{},{},{}]` сохраняют длину, но
|
||||
не несут ничего, а именно так выглядит затёртый маршрут.
|
||||
|
||||
Про третью проверку важно, что она **не добавляет обхода**: `arrayLen` уже
|
||||
сегодня декодирует каждый элемент массива в выбрасываемый `json.RawMessage`,
|
||||
чтобы посчитать их количество. Добавляется `isEmpty` по литералу элемента —
|
||||
проверка первого байта и, для чисел, `strconv`; в дерево значений элемент не
|
||||
разворачивается. Поэтому цена, названная триажем («полный обход маршрута на
|
||||
каждое сравнение»), уже уплачена, и решение её не умножает.
|
||||
|
||||
**Отвергнуто:** сверка содержимого *внутри* элемента ряда (набор ключей у
|
||||
каждой точки маршрута). Вот она обход действительно умножила бы — на 768 МиБ
|
||||
пика, измеренных пунктом 4, — и остаётся названным пределом (см. «Риски»).
|
||||
|
||||
**Отношение остаётся частичным порядком** — конъюнкция включений множеств и
|
||||
нестрогих неравенств по каждому общему ключу транзитивна. На это опирается
|
||||
решение 2.
|
||||
|
||||
### 2. Победитель внутри доставки — минимум канонической формы среди непревзойдённых
|
||||
|
||||
Попарная свёртка `pickWithinDelivery` нетранзитивна: полнота — частичный
|
||||
порядок, тай-брейк — тотальный, и вместе они образуют цикл. Стандарт для точек
|
||||
записан в `architecture.md` («победитель — функция множества точек, а не порядка
|
||||
их поступления») и реализован в `store.resolve`; для сущностей он применяется
|
||||
дословно: собрать версии ключа, отбросить строго покрытые, среди оставшихся
|
||||
взять минимум канонической формы.
|
||||
|
||||
Порядок обязан быть тотальным **до конца**. У точек кандидаты с равной
|
||||
канонической формой схлопываются ещё до сравнения, поэтому минимум единственен;
|
||||
у сущностей схлопывания нет, и при двух версиях, различающихся только порядком
|
||||
ключей или дребезгом последнего разряда (измеренная норма HAE), «минимум формы»
|
||||
неединственен — в хранилище лёг бы тот элемент, что стоял в массиве раньше.
|
||||
Поэтому последним разрядом сравнения идут исходные байты, ровно тем же
|
||||
движением и по той же причине, что записана в `canon.Less`.
|
||||
|
||||
«Строго покрыта» определено явно: покрыта другой версией и сама её не покрывает.
|
||||
Покрытие — предпорядок, две версии могут покрывать друг друга взаимно, и наивное
|
||||
«выбросить всё, что кем-то покрыто» опустошило бы множество, потеряв обе.
|
||||
|
||||
Счётчик «в одном теле приехали две версии одного ключа с разным содержанием»
|
||||
становится функцией множества тем же движением: считаются кандидаты, чья
|
||||
каноническая форма отличается от формы победителя.
|
||||
|
||||
Здесь требование постановки исполнено **по канонической форме, а не по байтам**,
|
||||
и это сказано прямо, потому что постановка говорит «при равном содержании и
|
||||
разных байтах обязан считать `differs=true`». Цель постановки — чтобы счётчик
|
||||
перестал молчать на двух настоящих разных версиях — достигается: случай оракула
|
||||
(маршрут против маршрута из `null`) считается. Побайтовый вариант отвергнут
|
||||
записанным инвариантом: порядок ключей в JSON от HAE нестабилен и дребезг
|
||||
последнего разряда тоже, ради чего канонизация и заведена, — счётчик по байтам
|
||||
срабатывал бы на норме потока и стал бы неотличим от шума ровно тогда, когда
|
||||
понадобился бы. Детерминизм при этом обеспечен не счётчиком, а тай-брейком по
|
||||
байтам выше.
|
||||
|
||||
`pickWithinDelivery` исчезает: она была парной формой того, что теперь делает
|
||||
множество.
|
||||
|
||||
Отбор «максимальные элементы плюс минимум по тотальному порядку» существует в
|
||||
проекте для точек (`store.resolve`) и объявлен стандартом в `architecture.md`.
|
||||
Второй рукописный экземпляр — ровно то, чем был `pickWithinDelivery`, и он
|
||||
разошёлся со стандартом нетранзитивностью. Поэтому механизм выносится в общего
|
||||
помощника, а точки и сущности становятся двумя его вызовами с разными
|
||||
отношениями: `architecture.md` уже обещает смену тай-брейка точек, когда род
|
||||
метрики будет измерен, то есть правка одного экземпляра при живом втором
|
||||
запланирована заранее.
|
||||
|
||||
### 3. Провенанс обновляется при совпавшем хеше
|
||||
|
||||
Совпал хеш — содержимое то же, писать нечего. Но провенанс
|
||||
(`delivery_id`/`delivery_received_at`) остаётся от первой свёрнутой копии, а не
|
||||
от победителя журнала. Следствия два: провенанс устаревает гарантированно на
|
||||
каждой из ~26 повторных присылок, и при возврате содержимого к прежнему
|
||||
(A→B→A) живая витрина расходится с `reindex` — тай-брейк пункта 4 правила
|
||||
сравнивает позиции, а сохранённая позиция неверна.
|
||||
|
||||
Решение: при совпавшем хеше сравнить позиции журнала и, если сохранённая
|
||||
раньше приехавшей, обновить **только** колонки провенанса. Провенанс становится
|
||||
максимумом по журналу среди версий с этим содержимым, то есть функцией
|
||||
множества.
|
||||
|
||||
`updated_at` при этом не двигается — и это отдельное решение, а не экономия.
|
||||
Тренировка приезжает до двадцати шести раз, и бамп метки на каждой сделал бы её
|
||||
меткой **касания строки**, а не изменения содержимого. Потребитель запроса «что
|
||||
изменилось с момента X» — естественного для коллектора и уже заказанного Read
|
||||
API — получил бы двадцать шесть ложных изменений, неотличимых от настоящего
|
||||
досчёта, и выяснилось бы это после того, как потребитель написан. Провенанс
|
||||
несёт собственную метку (время приёма своей доставки), и для тай-брейка её
|
||||
достаточно.
|
||||
|
||||
Счётчик записанных сущностей такое обновление **не** увеличивает: он считает
|
||||
содержимое витрины, и его сравнимость с прежними замерами важнее, чем учёт
|
||||
обновления. Отпечаток витрины провенанса не включает, поэтому сходимость
|
||||
`reindex` от этого решения не зависит — она зависит от него косвенно, через
|
||||
тай-брейк.
|
||||
|
||||
Слово «провенанс» после этого означает у сущности не то, что у часового
|
||||
объекта: у объекта хранится доставка, **создавшая** его, и она не поднимается.
|
||||
Асимметрия законная — у объекта нет замещения версии целиком, — но записана в
|
||||
спеке явно, иначе читатель перенесёт смысл с одного на другое.
|
||||
|
||||
### 4. Каноническая форма считается один раз, вне транзакции
|
||||
|
||||
Комментарий `bucket.go` утверждает, что канонизация вынесена наружу; фактически
|
||||
`analyze()` зовётся из `compareEntities` **внутри** `inTx`, а его результат
|
||||
пишется в **копию** элемента среза и не переживает даже одной попытки.
|
||||
|
||||
Ключевое наблюдение: `canon.Hash(raw)` уже считает полную каноническую форму и
|
||||
**выбрасывает** её, а `analyze()` считает ту же форму заново. То есть дорогая
|
||||
часть и так платится на каждой версии, в `prepareEntities`, вне транзакции.
|
||||
|
||||
Решение: `canon` отдаёт форму и хеш **одним проходом** (`FormAndHash`, внутри —
|
||||
запись в `io.MultiWriter(буфер, sha256)`, той же формой, какой уже написан
|
||||
`HashAll`), `newEntityVersion` зовёт его один раз и держит результат вместе с
|
||||
`canon.Analyze`. Ленивость, `analyze()` и флаг `parsed` уходят.
|
||||
|
||||
Отдельный вызов `HashForm(form []byte)` отвергнут: он вводит контракт
|
||||
очерёдности («сперва `Form`, потом `HashForm`»), где передача сырых байт вместо
|
||||
формы даёт правдоподобный, но неверный хеш, — а компилятор такую подмену не
|
||||
ловит.
|
||||
|
||||
Баланс работы назван честно, потому что он не односторонний:
|
||||
|
||||
- на пути **разошедшегося хеша** (одна доставка из сорока четырёх) — минус одна
|
||||
полная канонизация: сегодня форма считается дважды;
|
||||
- на пути **совпавшего хеша** (сорок три из сорока четырёх) — плюс один мелкий
|
||||
разбор `json.Unmarshal` в `map[string]json.RawMessage`, то есть проход по телу
|
||||
и копия каждого верхнеуровневого значения. Сегодня на этом пути `analyze()` не
|
||||
зовётся вовсе.
|
||||
|
||||
Плюс оценивается величиной входа, минус — величиной входа с константой развёртки
|
||||
в дерево значений, так что суммарно решение не дороже. Но «строго меньше» было
|
||||
бы неправдой, и приёмка меряет пик кучи до и после, а не верит рассуждению.
|
||||
|
||||
Разбор **сохранённой** версии остаётся внутри транзакции: её содержимое читается
|
||||
оттуда же и только когда хеш разошёлся (одна доставка из сорока четырёх).
|
||||
Убрать это можно лишь оптимистичным чтением до транзакции с перепроверкой
|
||||
внутри — это уже пределы размера и потоковый расчёт, то есть остаток.
|
||||
|
||||
### 5. Страж версии схемы переезжает в `Open`
|
||||
|
||||
`OpenForRead` сверяет версию схемы и отказывает при расхождении; `Open`
|
||||
мигрирует безусловно. Поэтому старый бинарь поверх схемы 7 стартует молча,
|
||||
незнакомые секции игнорирует, а доставки за окно отката помечает разобранными —
|
||||
и ничто не намекает, что для этого окна нужен `reindex`.
|
||||
|
||||
Асимметрия у `Open` законная: версия базы **ниже** версии бинаря — это ровно то,
|
||||
ради чего миграции существуют. Отказ ставится на «версия базы **выше** версии
|
||||
бинаря». Асимметрия относится только к `Open`: `OpenForRead` сохраняет строгое
|
||||
равенство, как требует спека пересборки, — иначе `reindex` начал бы читать
|
||||
рабочую базу схемы старее бинаря по колонкам, которых там нет. В одно место
|
||||
выносится **чтение** версии, а не сравнение.
|
||||
|
||||
Чтение берётся у самого goose (`Provider.GetVersions` отдаёт и текущую версию
|
||||
базы, и целевую), потому что имя таблицы учёта, имя колонки и правило «максимум
|
||||
= текущая версия» принадлежат ему: рукописная копия его приватной схемы
|
||||
разошлась бы при обновлении зависимости — и не отказом, а тем, что страж
|
||||
перестал бы ловить. Если окажется, что на соединении только для чтения этот путь
|
||||
требует записи или отдаёт лишнюю задержку (SQLite-диалект goose не умеет
|
||||
`TableExists`, поэтому на отсутствующей таблице уходит в повторы), копия
|
||||
допустима, но одной функцией и с названной причиной — и тогда «таблицы нет»
|
||||
распознаётся структурно (`sqlite_master`) и `NULL` читается как `NULL`
|
||||
(`sql.NullInt64`), а не по тексту ошибки драйвера: сообщения драйвера не
|
||||
контракт, это уже записанное правило проекта.
|
||||
|
||||
**Цена отказа названа, потому что она реальна.** Страж останавливает сервис
|
||||
целиком, а телефон шлёт непрерывно и молча: доставка, не попавшая в архив, в
|
||||
журнал не попадает вовсе. Взвешено так: откат бинаря — действие оператора,
|
||||
который в этот момент рядом и видит crash-loop сразу; дыры плотных метрик за
|
||||
время простоя закроют широкий и глубокий проходы синхронизации. Не закроют
|
||||
`stateOfMind` — у него доставки HAE единственный источник, — и это цена решения.
|
||||
Она меньше цены молчания: молчаливый старт портит витрину за всё окно отката, а
|
||||
узнать об этом неоткуда, и после ретеншена тел чинить будет нечем. Отвергнуты:
|
||||
деградированный режим «принимать и архивировать, свёртку не начинать» (сохраняет
|
||||
оба инварианта, но заводит режим, о существовании которого надо помнить, и
|
||||
правила его видимости) и отказ только воркеру свёртки (требует доказать, что
|
||||
старый бинарь корректно пишет учёт в новую схему, — доказательства нет).
|
||||
|
||||
### 6. Мягкий заголовок сущности
|
||||
|
||||
`entityHead` держит `ID`/`Name`/`Date`/`Start`/`End` типизированными строками, и
|
||||
сущность теряется целиком при смене типа любого из пяти. Причина названа точно,
|
||||
потому что от неё зависит выбор решения: роняет не `encoding/json`, а строка
|
||||
`entity.go:51-53`, где любая ошибка разбора считается фатальной. Сам
|
||||
`json.Unmarshal` «skips that field and completes the unmarshaling as best it
|
||||
can» и возвращает `*UnmarshalTypeError`.
|
||||
|
||||
Отсюда напрашивается трёхстрочная альтернатива — `errors.As(err, &ute)` и
|
||||
продолжить с уже заполненным заголовком. Она **отвергается**, и по названной
|
||||
причине: та же документация тут же оговаривает — «it's not guaranteed that all
|
||||
the remaining fields following the problematic one will be unmarshaled». Разбор,
|
||||
построенный на дозаполнении, перестал бы быть функцией тела: одна и та же
|
||||
тренировка давала бы разный заголовок в зависимости от порядка ключей на
|
||||
проводе, а он у HAE нестабилен.
|
||||
|
||||
Решение — штатная точка расширения `encoding/json`: тип `softString` с
|
||||
`UnmarshalJSON`, который на нестроковом значении ничего не пишет и возвращает
|
||||
`nil`. Теги остаются декларативными, ручных извлечений нет, гарантия полная.
|
||||
Заодно сохраняется различение счётчиков: элемент, который сам не объект, даёт
|
||||
ошибку **верхнего** уровня и по-прежнему уходит в «не разобралось как объект», а
|
||||
не в «нет `id`».
|
||||
|
||||
`id` при этом остаётся требованием, а не полем: без него сущность не адресуема.
|
||||
Число вместо строки в `id` — это сменившаяся форма идентификатора, и
|
||||
превращать `42` в `"42"` значило бы придумать идентичность за источник. Такая
|
||||
сущность пропускается прежним счётчиком.
|
||||
|
||||
`start`, приехавший не строкой, на `date` **не** откатывается. Мягкое чтение
|
||||
объявляет непонятое значение отсутствующим, а фолбэк `start → date` существует
|
||||
для сущностей, у которых `start` не прислан вовсе; композиция этих двух правил
|
||||
подставила бы метку другого момента времени, неотличимую от настоящей и ничем не
|
||||
считаемую. Поэтому нестроковый `start` — это неразбираемая метка.
|
||||
|
||||
Граница правила названа вслух: оно закрывает смену **типа**, но не смену
|
||||
**формата строки**, а наблюдался именно дрейф формата дат. Тренировка с датой в
|
||||
незнакомом формате по-прежнему теряется целиком — теперь со счётчиком в базе, —
|
||||
и закрыть это может только хранение сущности с неразобранной меткой, вынесенное
|
||||
остатком.
|
||||
|
||||
**Пропуски становятся видны в базе.** Миграция `00008` добавляет доставке
|
||||
колонку `skipped_entities`; свёртка пишет туда сумму трёх счётчиков пропуска
|
||||
сущностей. Причина не в отчётности: ретеншен решает «что потеряется, если тело
|
||||
удалить», по базе, и сегодня получает ответ «терять нечего» ровно там, где
|
||||
теряется тренировка с маршрутом.
|
||||
|
||||
Статус доставки от пропуска сущности **не** меняется: `partial` определён
|
||||
списком непокрытых секций, и второй источник истины для него завёл бы ровно то
|
||||
расхождение, которое спека запрещает явно.
|
||||
|
||||
### 7. Диагностика разбора без значений из тела
|
||||
|
||||
`fmt.Errorf("… встречено %v", tok)` подставляет токен целиком: тело 8 МиБ даёт
|
||||
текст ошибки 8 МиБ, который уходит атрибутом `error` на уровень `WARN`.
|
||||
Инвариант «тела запросов только на `DEBUG` и с обрезкой» нарушен буквально.
|
||||
|
||||
Ошибка называет **тип токена** и `dec.InputOffset()`. Смещение полезнее
|
||||
значения: по нему место в теле находится в архиве, а значение из тела в логе не
|
||||
имеет права быть в принципе.
|
||||
|
||||
## Три формы решения главного узла и компромисс каждой
|
||||
|
||||
Главный узел — глубина сравнения содержания при слиянии версий сущности.
|
||||
Рассматривались три, и выбор записан не по умолчанию:
|
||||
|
||||
1. **Поэлементная сверка содержимого рядов** (у каждого элемента маршрута
|
||||
сравнивать набор ключей). Ловит всё, включая точку маршрута без `altitude`.
|
||||
Компромисс: полный обход маршрута с материализацией каждого элемента на
|
||||
каждое сравнение — умножение уже измеренных 768 МиБ пика; плюс пересмотр
|
||||
правила слияния целиком. Отвергнута ценой.
|
||||
2. **Второй разряд + запрет вырождения формы + счёт содержательных элементов**
|
||||
(выбрана). Ловит скелет из скаляров, обнулённый ряд и исчезающий пустой ключ.
|
||||
Компромисс: строже правила точек, поэтому чаще удерживает; событие видно
|
||||
счётчиком, но контроль требует вывести счётчик в отчёт пересборки — иначе
|
||||
мера «сходимость `verify:archive`» его не увидит по построению. Стоимость —
|
||||
O(ключей) плюс `isEmpty` на элементах в уже существующем обходе.
|
||||
3. **Принять предел, оставить только наблюдаемость** (счётчик по различию байт,
|
||||
предел записан в `architecture.md`). Компромисс: маршрут продолжает теряться
|
||||
необратимо при обеднённой версии, а восстановить его после ретеншена тел
|
||||
неоткуда — в экспорте Apple маршрута нет. Отвергнута последствием.
|
||||
|
||||
Форма (2) принята владельцем в постановке; здесь она не переоткрывается, а
|
||||
уточняется недостающими определениями (пустота элемента, строгость покрытия,
|
||||
тотальность порядка) и получает контроль, которого у неё не было.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Порча внутри элемента ряда не ловится** (точка маршрута без `altitude`:
|
||||
длина та же, элемент непуст, форма не выродилась) → предел записывается в
|
||||
`architecture.md` рядом с описанием `Covers` так же прямо, как он записан в
|
||||
комментарии кода. Закрыть его может только сверка с телом в архиве, а тело
|
||||
живёт до ретеншена.
|
||||
- **Второй разряд `Covers` строже прежнего правила** и может удержать версию,
|
||||
которая раньше замещала: досчёт, потерявший ключ с пустым значением, теперь
|
||||
проигрывает. Мера контроля — **не** сходимость `verify:archive`: живой приём и
|
||||
пересборка пользуются одним правилом и одинаково сойдутся на одинаково
|
||||
замороженной версии, то есть слишком строгое правило выглядело бы идеальной
|
||||
сходимостью. Контроль — счётчик удержаний, выведенный в отчёт пересборки, и
|
||||
замер его значения на живом архиве.
|
||||
- **`isEmpty` считает ноль пустотой**, и это переносится на элементы ряда: ряд
|
||||
настоящих нулей будет выглядеть опустошённым → ошибка направлена в безопасную
|
||||
сторону (удерживаем, а не затираем) и видна счётчиком; наблюдённые ряды HAE
|
||||
состоят из объектов. Записано в спеку, потому что после мерджа это часть
|
||||
правила слияния навсегда.
|
||||
- **Мягкий заголовок принимает больше входов**, то есть сущности, ранее
|
||||
уходившие в `SkippedEntityMalformed`, начнут попадать в витрину → это
|
||||
изменение разбора, и по правилу «покрыли — пересверните» такие доставки надо
|
||||
пересворачивать. Замер на живом архиве сделан **до** утверждения формулировок:
|
||||
118 тел, пропусков `noID=0 noTime=0 malformed=0`, то есть пересворачивать
|
||||
нечего, и data-миграции нет.
|
||||
- **Мягкий заголовок увеличивает долю тел, доходящих до канонизации**: сущность,
|
||||
раньше отсекавшаяся на разборе заголовка почти бесплатно, теперь канонизуется
|
||||
целиком, и худший случай по памяти становится достижим на входах, которые до
|
||||
него не доходили → предел на размер сущности из задачи-остатка перестаёт быть
|
||||
желательным и становится **обязательным условием**; записано в её теле.
|
||||
- **Обновление провенанса при совпавшем хеше — дополнительная запись** там, где
|
||||
раньше её не было: ~26 повторных присылок на тренировку → запись касается трёх
|
||||
колонок без `payload`, то есть не трогает самое тяжёлое; счётчик записанных
|
||||
сущностей и `updated_at` не двигаются, и сравнимость замеров сохраняется.
|
||||
- **Страж версии схемы отказывает при старте** — сервис не поднимется на базе
|
||||
из будущего, то есть приём останавливается, а телефон не перешлёт → цена
|
||||
взвешена выше, в решении 5, вместе с отвергнутыми альтернативами; в
|
||||
`architecture.md` уезжает эксплуатационный контракт: как это выглядит
|
||||
(crash-loop контейнера) и чем лечится (возврат бинаря).
|
||||
- **Пункт 5 правила (несравнимые наборы) остаётся функцией порядка
|
||||
проигрывания**, и второй разряд `Covers` делает этот исход чаще → приёмочный
|
||||
критерий сходимости сформулирован условно (перестановка даёт один отпечаток
|
||||
при нулевом счётчике несравнимых), а сам предел записан в `architecture.md`
|
||||
как единственная точка, где витрина не является функцией множества доставок.
|
||||
Reference in New Issue
Block a user