## 1. Схема

- [x] 1.1 Миграция `00009_bucket_catalog.sql` — покрывающий индекс
      `bucket(metric, layer, hour_utc, first_ts, last_ts, points, units)`
- [x] 1.2 Обновить `docs/database.md`: индекс и зачем он

## 2. Хранилище

- [x] 2.1 `store.ReadCatalog(ctx, store.CatalogWindow)` — весь вход каталога
      **одной транзакцией чтения**: разрезы всех метрик, общие часы пары слоёв
      по каждой метрике, объекты окна обоих слоёв
- [x] 2.2 Разрезы — одним запросом `metric, layer, units, min(first_ts),
      max(last_ts), sum(points)`, группировка вместе с единицами (расхождение
      видно, а не выбирается молча)
- [x] 2.3 Общие часы — самые свежие часы с объектами обоих слоёв, от свежих к
      старым, не больше `window`
- [x] 2.4 Объекты окна — **пакетом**, один запрос на метрику на оба слоя
      (`hour_utc IN (…)`), а не по объекту за раз
- [x] 2.5 Тест: разрезы и общие часы отвечают по индексу (`EXPLAIN QUERY PLAN`
      не содержит обхода таблицы)
- [x] 2.6 Тест: число обращений к базе на метрику не зависит от размера окна
- [x] 2.7 Тест: `CommonHours` отдаёт не больше `window` и именно свежие часы

## 3. Значение точки

- [x] 3.1 `hae.PointValue(raw)` — число точки: `qty`, при его отсутствии `Avg`;
      разбор через `*json.Number`, **ноль — значение**, отсутствие ключа и
      `null` — нет значения, нечисловое и не влезающее в `float64` (`ErrRange`,
      `±Inf`) — нет значения
- [x] 3.2 В док-комментарии сказать, что словарь пустоты `canon` сюда не
      применяется, и почему
- [x] 3.3 Тесты на реальных формах точки из `testdata`: `heart_rate` с
      `Min`/`Avg`/`Max` без `qty`, `{"qty":0}`, точка без числового поля,
      `1e400`

## 4. Измерение рода

- [x] 4.1 Пакет `internal/catalog`: тип рода (`cumulative`/`instant`/`unknown`,
      пустое значение невыразимо) и основание измерения
      (`hours`/`compared`/`agreeing`/`conflicting`/границы окна)
- [x] 4.2 Именованная константа допуска `1e-9` с измеренной ценой в комментарии
      и **один** предикат `close(a, b)`, которым выражены все три сравнения
- [x] 4.3 Пригодность часа: ровно одна точка со значением у часового объекта,
      её метка совпадает с началом часа, ≥2 точек со значением у минутного,
      сумма отличима от среднего
- [x] 4.4 Сумма минутных значений считается в порядке возрастания метки
- [x] 4.5 Правило метрики: ≥3 согласных и 0 противоречащих, иначе `unknown`
- [x] 4.6 Окно 48 самых свежих общих часов
- [x] 4.7 Сборка каталога: разрезы из снимка + род из измерения; строки слоя,
      разошедшиеся единицами, схлопываются в один элемент, множество единиц
      метрики отсортировано
- [x] 4.8 Чекпоинт `WARN` при `conflicting > 0`: имя метрики и числа основания,
      без значений точек
- [x] 4.9 Тесты: накопительная, мгновенная, нулевой час, двухточечный часовой
      объект, одноточечный минутный, невыровненная часовая метка, противоречие,
      единственный слой, только слой `day`, история длиннее окна, разошедшиеся
      единицы, идемпотентность двух вызовов

## 5. HTTP

- [x] 5.1 Проверка токена **одна на оба контура**, параметризованная списком;
      схема строгая (`Bearer`), пустой список = выключено
- [x] 5.2 Предупреждение на старте о выключенной проверке чтения
- [x] 5.3 Редакция сохраняемых заголовков доставки чистит токены **обоих**
      контуров
- [x] 5.4 `GET /api/v1/metrics` — форма ответа из дизайна: `style`, `hours`,
      `compared`, `agreeing`, `conflicting`, `first_hour`, `last_hour`; срезы
      пустые, а не nil; границы окна — указатели; порядок детерминирован
- [x] 5.5 Тесты: 401 без токена, 401 с токеном приёма, 401 с голым значением без
      схемы, отдача при выключенной проверке, пустая витрина — **сравнением
      байтов** ответа с литералом
- [x] 5.6 `config.example.toml` и `config.docker.toml`: `read_tokens` перестал
      быть заделом на будущее, цена пустого списка названа комментарием

## 6. Проверка на живом архиве

- [x] 6.1 Прогон измерения в `internal/replay/archive_test.go`: свойства, а не
      числа — конфликтующих свидетельств ноль; накопительные и мгновенные
      метрики разошлись по родам; ни одна метрика не измерена по слою `raw`
- [x] 6.2 Печать измеренного рода по метрикам и стоимости каталога в `t.Logf`
- [x] 6.3 `task verify:archive` зелёный, отпечаток витрины не изменился

## 7. Документация и беклог

- [x] 7.1 `docs/architecture.md`: метод измерения, окно, порог, допуск, почему
      род не хранится, почему родов два, а не четыре, где нужен `xFilesFactor` и
      какой у него подвох с полярностью
- [x] 7.2 `docs/architecture.md`: форма каталога приведена к реализованной
- [x] 7.3 `docs/local-research.md`: находка с результатом измерения на живом
      корпусе
- [x] 7.4 Блокер «тай-брейк при равной полноте точек» в беклог, с вариантами,
      ценой и рекомендацией
- [x] 7.5 Пометка в `docs/tasks/items/read-api-points.md`: порог неполного ведра,
      его полярность и предел размера ответа решаются там
- [x] 7.6 Пометка в `docs/tasks/items/token-and-secret-management.md`: контуров теперь два
- [x] 7.7 Убрать задачу из беклога, обновить индекс

## 8. Дозакрыто по ревью кода

- [x] 8.0 Горизонт окна: часы позже `now + час` в сверку не входят, данные из
      будущего пишутся `WARN`
- [x] 8.0 Единицы обеих сторон обязаны совпасть — иначе уверенный ложный род
- [x] 8.0 Содержимое разжимается только у часов, прошедших отбор по учётным
      колонкам
- [x] 8.0 Имя метрики в логе обрезано, множество единиц ограничено потолком
- [x] 8.0 Метрика с пустым именем показывается, а не выбрасывается сентинелом
- [x] 8.0 Отмена снаружи не пишется как сбой сервиса
- [x] 8.0 Байтовый тест непустого ответа и повтора запроса

## 9. Приёмочные критерии (рубрика ревью предложения)

- [x] 9.1 Вердикт — чистая функция состояния витрины и окна: не зависит от
      порядка строк SQL, порядка точек в объекте и момента вызова
- [x] 9.2 Допуск назван величиной, один предикат на все сравнения, поведение
      около нуля объявлено
- [x] 9.3 Вырожденные свидетельства исключены явно, кворум назван числом, ниже
      кворума исход — `unknown`, а не умолчание
- [x] 9.4 `unknown` — исход первого класса, и его причины различимы клиентом без
      второго запроса
- [x] 9.5 Измерение ничего не пишет и не кешируется скрытно
- [x] 9.6 Стоимость ответа ограничена сверху и по числу запросов, и по числу
      прочитанных страниц; не растёт вместе с историей
- [x] 9.7 HTTP-контракт полон: только `GET`, пустая витрина — 200 с пустым
      списком, авторизация до работы, токены и значения здоровья не в логах
      выше `DEBUG`
- [x] 9.8 Ответ самоописателен: словарь слоёв тот же, что везде; границы
      объявляют, что метят; расхождение единиц показано, а не выбрано молча
- [x] 9.9 Каталог отдаёт наблюдаемое, а не досчитанное; деталь хранения наружу
      не протекает
- [x] 9.10 Нижний слой в сверке не участвует; `source` в измерение не входит
- [x] 9.11 Смена вердикта наблюдаема чекпоинтом
- [x] 9.12 Правило часа и правило метрики тестируются без БД; на живом архиве
      проверяются свойства, а не числа
