## Критерии приёмки (из постановки, переживают удаление файла задачи)

Задача `docs/tasks/items/read-api-points-period.md`, «Точки за период».

- **К1.** «вес за год» отвечается одним запросом без доступа к файлу базы —
  **оракул:** запрос к поднятому сервису на живом архиве.
  *Подмена оракула на этой машине:* рабочая база `./data` принадлежит живому
  контейнеру, читать её мимо `task up`/`task run` запрещено. Вместо неё сервис
  поднимается в worktree на своём порту и своей базе, наполненной **реальными
  пакетами** `internal/hae/testdata`. Подмена названа строкой и не выдаётся за
  исходный оракул: она проверяет маршрут целиком (HTTP, токен, разбор
  параметров, чтение витрины, форма ответа), но не объём живого архива.
- **К2.** в ответе всегда видны `layer`, `aggregation` и `last_hour` —
  **оракул:** тест на форме ответа.
- **К3.** метрика, у которой род не измерен, отдаётся без свёртки и говорит об
  этом, а не молчит и не досчитывает — **оракул:** тест на метрике с неизвестным
  родом.

## Приёмочные критерии из рубрики (проход `review-rubric`, профиль `design`)

Свойства узла «HTTP-обработчик чтения временного ряда», порождённые до чтения
предложения. Проверяются наравне с К1–К3.

- **Р1.** Род, объявленный в ответе, применим к отданному ряду: сочетания, из
  которого клиент выведет разрешённой операцию, запрещённую инвариантом, в
  конверте нет.
- **Р2.** Ответ есть функция того, что уже произошло, а не момента взгляда: всё,
  что способно измениться **без коммита в базу** (горизонт, параметры запроса),
  входит в область действия метки; повтор того же запроса при том же состоянии и
  тех же часах даёт побайтово тот же ответ.
- **Р3.** Ряд собран ровно из одного слоя, слой назван всегда, правило выбора
  детерминировано на любом входе; предикат выбора слоя и предикат отбора точек
  используют **одну границу**.
- **Р4.** Отсутствие предела размера — осознанное решение, стоящее на замере
  **того режима, ради которого предел заводится**, а не на замере доступного
  корпуса.
- **Р5.** Период — полуинтервал; точка на границе попадает ровно в один из двух
  соседних ответов; правило принадлежности интервальной точки названо.
- **Р6.** Пустой результат — `200`, пустая коллекция списком, метаданные на
  месте; «данных нет» отличимо от «ресурса нет» без второго запроса.
- **Р7.** Невозможный запрос отвергается до чтения витрины, кодом `4xx`, и текст
  отказа не содержит значений из запроса.
- **Р8.** Время нормализовано и однозначно; значения точки уезжают дословно —
  без переименования, пересчёта, отбрасывания и **экранирования**.
- **Р9.** Ответ не выглядит полнее, чем он есть: «данных не было» отличимо от
  «слой выбран по охвату меньше периода» без пересчёта точек.
- **Р10.** Выборка идёт по существующему индексу без полного скана витрины,
  `context` протянут до драйвера, отмена клиента не считается отказом.
- **Р11.** Транспорт не несёт доменной логики; доменный тип до сериализации не
  доезжает.
- **Р12.** Ни значения здоровья, ни токен не доводятся до лога выше `DEBUG` и до
  тела отказа ни одним путём.

## 1. Чтение витрины

- [x] 1.1 `store.ReadSeries(ctx, window, pick)` — **один вход, одна транзакция
  чтения**: охваты слоёв, точки выбранного слоя, объекты окна измерения.
  Правило выбора слоя приходит функцией-параметром и остаётся в домене
- [x] 1.2 Охваты слоёв: `min(first_ts)`, `max(last_ts)`, охваты по
  `GROUP BY layer` в границах часов `[trunc(from), trunc(to)]` — по покрывающему
  индексу `bucket_catalog`, без чтения содержимого (Р10)
- [x] 1.3 Точки выбранного слоя: объекты по точному префиксу первичного ключа,
  разжатие, отбор по началу координаты до `[from, to)`
- [x] 1.4 Окно измерения одной метрики: переиспользует `commonHours` и
  `readHourPairs`, второго правила отбора не заводит
- [x] 1.5 Тесты хранилища: точка `10:59` из объекта `10:00` не теряется; точка
  ровно на `from` есть, ровно на `to` — нет; период короче часа, где крупный
  слой имеет объект без точек внутри, а мелкий — точки (Р3); пустой период

## 2. Use-case «ряд точек» (`internal/points`)

- [x] 2.1 Пакет `internal/points`: тип запроса, тип ответа, `Service` над
  `store` и `catalog`
- [x] 2.2 Правило выбора слоя: охват = длина пересечения `[первая метка,
  последняя метка]` слоя с периодом; пустое пересечение выбывает; наибольший
  охват, при равенстве — самый мелкий; явный слой отменяет правило и всегда
  уезжает в ответ (Р3)
- [x] 2.3 Род через `catalog.Measure`, применимость — `false` при `unknown`, при
  `cumulative` на слое `raw` и при отсутствии выбранного слоя (Р1)
- [x] 2.4 Метка ответа: версия витрины + горизонт, огрублённый до часа
  (`catalog.Stamp`, не второй экземпляр) + канонизированная форма запроса; род и
  метка снимаются с одного горизонта (Р2)
- [x] 2.5 Логирующий чекпоинт исхода один и на доменной границе; отмена и
  занятость базы — `DEBUG`, настоящий отказ — `ERROR`; значений точек и границ
  запроса в записи нет, имя метрики обрезано; предупреждения измерения маршрут
  точек не повторяет (Р12)
- [x] 2.6 Тесты домена: охваты различаются; охваты равны; ни одного слоя;
  явный слой пуст; период короче часа; применимость рода на четырёх слоях

## 3. Транспорт и форма провода

- [x] 3.1 Маршрут `GET /api/v1/metrics/{name}` под токеном чтения; тест, что он
  не перехватывает `GET /api/v1/metrics`; имя метрики берётся процентно
  декодированным
- [x] 3.2 Разбор параметров: `from`/`to` обязательны, RFC 3339 с явной зоной,
  `from < to`, `layer` из словаря, присутствие `bucket` — `400`; каждый отказ
  до чтения витрины, с человекочитаемым сообщением без значений из запроса (Р7)
- [x] 3.3 Форма провода точек по образцу `catalog.go`: типы `*Wire` с
  `json`-тегами, перевод присваиванием поле в поле, `values` — `json.RawMessage`
- [x] 3.4 Строка в таблице образцов `wire_internal_test.go` (Р11)
- [x] 3.5 `writeJSON` перестаёт экранировать HTML-символы (общее правило
  `read-api`); тест на значении точки с `&`, `<`, `>` (Р8)
- [x] 3.6 Метка и `Cache-Control` через существующий `setReadHeaders`; тесты:
  различие меток по каждому параметру по очереди, совпадение при эквивалентной
  записи времени, различие при переходе горизонта через час (Р2)
- [x] 3.7 Байтовое утверждение формы ответа на **фиксированной** витрине с
  часами в прошлом — ни одно поле литерала не зависит от хода часов (прецедент
  `docs/review.md`, 2026-08-03)
- [x] 3.8 Тесты маршрута: **К2** (все поля конверта присутствуют всегда),
  **К3** (род `unknown` назван словом, `applicable: false`), точка-интервал
  (`ts_end`), дословность `values`, пустой период без слоя (`layer: null`),
  пустой период с явным слоем (`layer` эхом), незнакомая метрика, `401`

## 4. Документация

- [x] 4.1 `docs/architecture.md`, раздел «Read API»: форма ответа точек
  (`aggregation` объектом, `applicable`, `ts_end`), правило выбора слоя
  формулировкой из спеки, строка маршрута с пометкой о неподдержанном `bucket`,
  строка `points` в таблице компонентов
- [x] 4.2 `config.example.toml`: строка про `read_tokens` называет точки рядом с
  каталогом

## 5. Верификация

- [x] 5.1 `task gate` зелёный
- [x] 5.2 Поведенческая верификация: свой `config.toml` в worktree (порт
  `:18080`, база и архив в `./tmp/`), наполнение реальными пакетами
  `internal/hae/testdata` через маршрут приёма, затем **К1** — метрика за год
  одним запросом; рабочий `./data` не трогается
- [x] 5.3 Замер цены маршрута числом на **трёх** режимах, включая худший
  (Р4, прецедент `docs/review.md` 2026-08-04 «замер снят на корпусе, где
  измеряемого случая не бывает»): редкая метрика за год; плотная метрика за
  сутки в `minute`; плотная метрика за неделю в `raw`. Корпус собирается
  размножением **реальных** точек `testdata`, а не литералами; метод
  записывается рядом с числом
- [x] 5.4 `task verify:busy` — зелёный (fold 24.08 с, replay 23.60 с).
  `task verify:archive` **не прогнан**: в worktree нет `./data`, а живой архив
  основного репозитория трогать запрещено; триггер прогона
  (изменение правила разбора, идентичности или слияния) не сработал — маршрут
  только читает витрину. Названо строкой в границах покрытия, а не замолчано
