свёртка доставки в объекты и сшивка с приёмом

- internal/fold — свёртка по идентификатору доставки, тело из архива: тот же
  код, каким пойдёт пересборка витрины
- приём зовёт свёртку на context.WithoutCancel с собственным дедлайном;
  исход разбора на код ответа не влияет
- слой доставки хранится в delivery.derived_layer и наследуется от
  ПРЕДШЕСТВУЮЩЕЙ доставки автоматизации: без границы по времени свёртка
  переставала быть функцией от префикса журнала (1737 объектов против 1742)
- task verify:archive — сходимость на живом архиве, 99 доставок из 99
This commit is contained in:
av
2026-08-01 18:03:59 +03:00
parent c9ddf0b4dd
commit 32f21044e4
17 changed files with 898 additions and 36 deletions
@@ -83,10 +83,20 @@
- **WHEN** ни у одной метрики доставки нет десяти точек
- **THEN** слой наследуется от последнего надёжно выведенного слоя той же
автоматизации (`automation-id`)
автоматизации (`automation-id`) среди доставок, **предшествующих** этой
- **AND** если наследовать нечего, слой берётся из **надёжного** заголовка
(`Minutes``minute`, `Hours``hour`)
Граница «предшествующих» обязательна: слой обязан быть функцией от префикса
журнала. Наследование от последней доставки вообще делает свёртку зависящей от
истории, и пересборка даёт не то состояние, что живой приём — измерено на
архиве, 1737 объектов против 1742.
#### Scenario: Пересборка журнала даёт то же состояние
- **WHEN** те же доставки сворачиваются повторно в том же порядке
- **THEN** число объектов и их содержимое не меняются
#### Scenario: Наследовать нечего и заголовок ненадёжен
- **WHEN** плотных метрик нет, предыдущего слоя автоматизации нет, а заголовок
@@ -44,16 +44,16 @@
## 5. Сшивка с приёмом
- [ ] 5.1 Свёртка вызывается по идентификатору доставки, тело читается из архива — один код с будущей пересборкой
- [ ] 5.2 Работа после записи в архив — на `context.WithoutCancel` с собственным дедлайном: обрыв соединения не рвёт запись объектов
- [ ] 5.3 Отказ разбора не меняет код ответа: `200`, `parse_status=failed`, запись `ERROR` без значений
- [ ] 5.4 Единственный логирующий чекпоинт на границе: счётчики метрик, точек, объектов, пропусков, перезаписей; идентификатор доставки; ни значений, ни имён устройств
- [ ] 5.5 Тест приёма: битый JSON — 400, непонятое содержимое — 200 с `parse_status=failed`
- [x] 5.1 Свёртка вызывается по идентификатору доставки, тело читается из архива — один код с будущей пересборкой
- [x] 5.2 Работа после записи в архив — на `context.WithoutCancel` с собственным дедлайном: обрыв соединения не рвёт запись объектов
- [x] 5.3 Отказ разбора не меняет код ответа: `200`, `parse_status=failed`, запись `ERROR` без значений
- [x] 5.4 Единственный логирующий чекпоинт на границе: счётчики метрик, точек, объектов, пропусков, перезаписей; идентификатор доставки; ни значений, ни имён устройств
- [x] 5.5 Тест приёма: битый JSON — 400, непонятое содержимое — 200 с `parse_status=failed`
## 6. Сходимость на реальных данных
- [ ] 6.1 Скрипт `tmp/research/verify_buckets.py`: прогоняет архив через разбор и сверяет суммы по часовому слою с проверкой из разведки
- [ ] 6.2 Прогон на всех накопленных доставках: расхождений по накопительным метрикам нет
- [x] 6.1 Скрипт `tmp/research/verify_buckets.py`: прогоняет архив через разбор и сверяет суммы по часовому слою с проверкой из разведки
- [x] 6.2 Прогон на всех накопленных доставках: расхождений по накопительным метрикам нет
- [ ] 6.3 `task gate` зелёный; `task restart` поднимает сервис, новая доставка с телефона разбирается
## 7. Приёмочные критерии (рубрика ревью дизайна)
@@ -61,16 +61,16 @@
Порождена проходом `healthlog-review-rubric` **до** чтения предложения. Каждый
пункт проверяем: понятно, каким тестом его провалить.
- [ ] 7.1 Отсутствие паники на произвольном входе — усечённый JSON, `null` вместо объекта, массив вместо объекта, число вместо строки даты
- [ ] 7.2 Незнакомое поле точки переживает round-trip дословно
- [ ] 7.3 Идемпотентность повторной записи, включая переставленный порядок ключей и точек
- [ ] 7.4 Различие не теряется молча: перезапись и изменение запечатанного часа оставляют след
- [ ] 7.5 Конкурентное слияние того же часа не теряет точки — под `-race`, с проверкой суммы
- [ ] 7.6 Отмена посреди слияния не оставляет половинчатого состояния: объект либо прежний, либо полный
- [ ] 7.7 Граница размера входа явная; вход в сотни мегабайт не кладёт процесс по памяти
- [ ] 7.8 Ошибки различимы по типу, а не по тексту (`errors.Is`/`errors.As`)
- [ ] 7.9 Частично непонятный пакет имеет явную судьбу: что сохранено, что отброшено — видно в счётчиках
- [ ] 7.10 Время нормализовано без потери зоны
- [ ] 7.11 Слой выводится из данных, а не из заголовка; неопределимый слой имеет явную судьбу
- [ ] 7.12 Парсер детерминирован и чист: ни `time.Now`, ни генерации id; два вызова на одном входе равны
- [ ] 7.13 (добавлено после снятия блокера) Точка-интервал не схлопывается по метке: прогон архива даёт 174 координаты сна, а не 170
- [x] 7.1 Отсутствие паники на произвольном входе — усечённый JSON, `null` вместо объекта, массив вместо объекта, число вместо строки даты
- [x] 7.2 Незнакомое поле точки переживает round-trip дословно
- [x] 7.3 Идемпотентность повторной записи, включая переставленный порядок ключей и точек
- [x] 7.4 Различие не теряется молча: перезапись и изменение запечатанного часа оставляют след
- [x] 7.5 Конкурентное слияние того же часа не теряет точки — под `-race`, с проверкой суммы
- [x] 7.6 Отмена посреди слияния не оставляет половинчатого состояния: объект либо прежний, либо полный
- [x] 7.7 Граница размера входа явная; вход в сотни мегабайт не кладёт процесс по памяти
- [x] 7.8 Ошибки различимы по типу, а не по тексту (`errors.Is`/`errors.As`)
- [x] 7.9 Частично непонятный пакет имеет явную судьбу: что сохранено, что отброшено — видно в счётчиках
- [x] 7.10 Время нормализовано без потери зоны
- [x] 7.11 Слой выводится из данных, а не из заголовка; неопределимый слой имеет явную судьбу
- [x] 7.12 Парсер детерминирован и чист: ни `time.Now`, ни генерации id; два вызова на одном входе равны
- [x] 7.13 (добавлено после снятия блокера) Точка-интервал не схлопывается по метке: прогон архива даёт 174 координаты сна, а не 170