## Критерии приёмки (из постановки, переживают удаление файла задачи) Задача `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`, а живой архив основного репозитория трогать запрещено; триггер прогона (изменение правила разбора, идентичности или слияния) не сработал — маршрут только читает витрину. Названо строкой в границах покрытия, а не замолчано