Дозакрыты находки ревью по слиянию сущностей

- Правило покрытия получило второй разряд (условный, как у точек), запрет
  вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и
  ряд из null больше не затирают маршрут. Победитель внутри доставки стал
  функцией множества версий — общим помощником с точками, — а провенанс
  поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину
  к прежнему содержимому.
- Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной
  записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма
  считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает
  на старте; текст ошибки разбора не несёт значений из тела.
- Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты:
  безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя
  был квадратичен по числу присланных версий одного ключа.
This commit is contained in:
av
2026-08-02 16:38:18 +03:00
parent 51a5272c96
commit 8331328134
52 changed files with 4921 additions and 481 deletions
+21 -2
View File
@@ -7,9 +7,7 @@
принятое, что происходит с несвёрнутым при остановке и рестарте. Цена ошибки
здесь наивысшая в проекте — доставка, не попавшая в архив и в журнал, не
восстанавливается: телефон её не перешлёт.
## Requirements
### Requirement: Ответ приёма отражает сохранность, а не разбор
Приём SHALL отвечать `200` после того, как тело записано в сырой архив и
@@ -302,3 +300,24 @@ NOT: факт уходит в `DEBUG`, приём продолжается.
- **WHEN** запрос идёт не на приём
- **THEN** его бюджет записи ответа остаётся общим `write_timeout`
### Requirement: Отказ учёта доставки называет класс причины
Система SHALL логировать отказ, случившийся **после** того, как тело легло в архив, но до появления учётной записи, так, чтобы владелец отличал **занятость базы** от прочих причин. Различается именно занятость: у неё уже есть доменная ошибка, и она означает конкуренцию за запись, которая будет повторяться.
Расширять признак до «обстоятельств вообще» система MUST NOT, хотя предикат с таким смыслом в проекте есть: он включает ещё и отмену работы снаружи, а на этом пути отмена невозможна по построению — учёт ведётся на контексте, переживающем обрыв соединения. Назвать отменённую работу занятостью базы значило бы отправить владельца искать конкуренцию там, где её нет.
Уровень при этом остаётся `ERROR` независимо от класса: тело лежит в архиве без
учётной записи, то есть осиротело, и вернуть его в журнал может только
пересборка. Занятость базы этого не отменяет — она объясняет причину, а не
снимает работу. Смысл различения в другом: занятость означает конкуренцию за
запись, которая будет повторяться и лечится не тем же, чем лечится сбой диска
или испорченная база.
#### Scenario: Занятая база при учёте доставки видна как отдельный класс
- **WHEN** запись учёта доставки не проходит из-за занятости базы
- **THEN** отказ логируется на уровне `ERROR` вместе с путём тела в архиве
- **AND** запись отличает занятость базы от прочих причин отказа
- **AND** тот же отказ по другой причине этого признака не несёт