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

- Правило покрытия получило второй разряд (условный, как у точек), запрет
  вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и
  ряд из 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
+16 -1
View File
@@ -32,6 +32,17 @@ type Outcome struct {
// Incomparable — столкновений с несравнимыми наборами полей. На живом потоке
// их не было ни разу, и на этом стоит отказ от объединения полей.
Incomparable int
// EntitiesHeld — версий сущностей, удержанных правилом «не теряем
// содержания». Без него правило слияния сущностей проверить нечем:
// сходимость отпечатка его не проверяет ПО ПОСТРОЕНИЮ — живой приём и
// пересборка пользуются одним правилом и одинаково сойдутся на одинаково
// удержанной версии. То есть слишком строгое правило (замораживающее
// тренировку на старой версии) выглядело бы идеальной сходимостью.
EntitiesHeld int
// EntitiesDiverging — версии одного ключа, приехавшие в одном теле с разным
// содержанием. Событие другого рода, чем удержание, и считается отдельно:
// смешанное число не отвечало бы ни на один из двух вопросов.
EntitiesDiverging int
}
// Add накапливает исход одной доставки в общий.
@@ -43,6 +54,8 @@ func (o *Outcome) Add(other Outcome) {
o.FailedOther += other.FailedOther
o.Partial += other.Partial
o.Incomparable += other.Incomparable
o.EntitiesHeld += other.EntitiesHeld
o.EntitiesDiverging += other.EntitiesDiverging
}
// classify раскладывает ошибку свёртки по классам исхода.
@@ -52,7 +65,7 @@ func (o *Outcome) Add(other Outcome) {
// проверяется она перебором классов, без базы и без архива.
//
// Неэкспортируемая намеренно: её результат содержит поля `Partial` и
// `Incomparable`, которые дописывает только Play, — вторая публичная дверь
// `Incomparable`, `EntitiesHeld` и `EntitiesDiverging`, которые дописывает только Play, — вторая публичная дверь
// молча занижала бы именно тот счётчик, по которому принимается решение о
// судьбе тела в архиве.
func classify(err error) Outcome {
@@ -105,6 +118,8 @@ func (p Player) Play(ctx context.Context, deliveryID string) (Outcome, error) {
out.Partial++
}
out.Incomparable += st.Incomparable
out.EntitiesHeld += st.EntitiesHeld
out.EntitiesDiverging += st.EntitiesDiverging
}
return out, err
}