- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
объявляет слой, измеренный род, его применимость к отданному ряду и границу
окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
записи: дословность содержимого точки иначе не удерживается, а оборванное
тело уходило под видом успешного `200`
141 lines
13 KiB
Markdown
141 lines
13 KiB
Markdown
## Критерии приёмки (из постановки, переживают удаление файла задачи)
|
||
|
||
Задача `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`, а живой архив
|
||
основного репозитория трогать запрещено; триггер прогона
|
||
(изменение правила разбора, идентичности или слияния) не сработал — маршрут
|
||
только читает витрину. Названо строкой в границах покрытия, а не замолчано
|