httpapi: точки метрики за период отдаются одним запросом
- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
объявляет слой, измеренный род, его применимость к отданному ряду и границу
окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
записи: дословность содержимого точки иначе не удерживается, а оборванное
тело уходило под видом успешного `200`
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
## Критерии приёмки (из постановки, переживают удаление файла задачи)
|
||||
|
||||
Задача `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`, а живой архив
|
||||
основного репозитория трогать запрещено; триггер прогона
|
||||
(изменение правила разбора, идентичности или слияния) не сработал — маршрут
|
||||
только читает витрину. Названо строкой в границах покрытия, а не замолчано
|
||||
Reference in New Issue
Block a user