Files
healthlog/internal/store/winner.go
T
av 8331328134 Дозакрыты находки ревью по слиянию сущностей
- Правило покрытия получило второй разряд (условный, как у точек), запрет
  вырождения формы и счёт содержательных элементов ряда: скелет из скаляров и
  ряд из null больше не затирают маршрут. Победитель внутри доставки стал
  функцией множества версий — общим помощником с точками, — а провенанс
  поднимается и при совпавшем хеше, иначе отложенная доставка возвращала витрину
  к прежнему содержимому.
- Одно поле не того типа больше не уносит сущность, а пропуски видны в учётной
  записи доставки (миграция 00008, NULL = «не измерялось»); каноническая форма
  считается один раз и вне транзакции; откат бинаря поверх новой схемы отказывает
  на старте; текст ошибки разбора не несёт значений из тела.
- Ревью кода профилем deep (девять проходов) нашло две регрессии и обе закрыты:
  безусловный второй разряд запирал законный досчёт навсегда, а выбор победителя
  был квадратичен по числу присланных версий одного ключа.
2026-08-02 16:38:18 +03:00

78 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package store
import "context"
// pickBest выбирает победителя среди кандидатов на одни координаты.
//
// **Победитель — функция МНОЖЕСТВА кандидатов, а не порядка их поступления.**
// Попарная свёртка этого не даёт: полнота (или покрытие) — частичный порядок,
// тай-брейк — тотальный, и вместе они образуют нетранзитивное отношение победы,
// то есть цикл. При цикле повторная свёртка одной и той же доставки меняет
// содержимое витрины, и она перестаёт быть свёрткой журнала. Проверено дважды:
// сперва на точках, где нетранзитивность нашлась перебором троек, потом на
// сущностях, где ту же ошибку повторили молча.
//
// Отсюда и общий помощник вместо второй рукописной копии: механизм один,
// отношения разные. `architecture.md` уже обещает смену тай-брейка точек, когда
// род метрики будет измерен, — то есть правка одного экземпляра при живом
// втором запланирована заранее, и расхождение правил слияния ломает детерминизм
// свёртки молча.
//
// dominates(a, b) обязан быть СТРОГИМ превосходством: a не хуже b и b не не
// хуже a. Иначе взаимно покрывающие друг друга кандидаты выбьют друг друга, и
// множество непревзойдённых окажется пустым.
//
// less обязан быть ТОТАЛЬНЫМ строгим порядком: при неединственном минимуме
// победителем оказывается просто первый в срезе, то есть порядок элементов на
// проводе, а он у HAE нестабилен.
//
// Отбор непревзойдённых КВАДРАТИЧЕН по числу кандидатов, и это названо вслух,
// потому что число кандидатов приходит из чужого тела. Отсюда `ctx`: цикл, чья
// стоимость определяется размером входа, обязан видеть отмену. Без него тело с
// двадцатью тысячами версий одного ключа занимало бы единственного воркера
// свёртки дольше, чем длится его же дедлайн, — то есть дедлайн, заведённый
// ровно против такого случая, не значил бы ничего. Замерено: n=4000 — 3.9 с,
// n=8000 — вчетверо больше.
//
// Вызывающий обязан сокращать множество до входа сюда: совпавших кандидатов
// схлопывать, а число различных — ограничивать. Помощник этого не делает
// сам — что считать «тем же» кандидатом, знает только он.
//
// Возвращает индекс победителя и индексы непревзойдённых — вторые нужны тем,
// кто считает несравнимость среди них. Пустой срез кандидатов — нарушенный
// инвариант вызывающего: оба сегодняшних вызова собирают множество из карты и
// пустого дать не могут.
func pickBest[T any](ctx context.Context, cands []T, dominates func(a, b T) bool, less func(a, b T) bool) (winner int, maximal []int, err error) {
if len(cands) == 0 {
return 0, nil, errNoCandidates
}
maximal = make([]int, 0, len(cands))
for i := range cands {
if err := ctx.Err(); err != nil {
return 0, nil, err
}
beaten := false
for j := range cands {
if i == j {
continue
}
if dominates(cands[j], cands[i]) {
beaten = true
break
}
}
if !beaten {
maximal = append(maximal, i)
}
}
winner = maximal[0]
for _, i := range maximal[1:] {
if less(cands[i], cands[winner]) {
winner = i
}
}
return winner, maximal, nil
}