Files
av 29ca8d415c httpapi: точки метрики за период отдаются одним запросом
- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
  объявляет слой, измеренный род, его применимость к отданному ряду и границу
  окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
  точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
  под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
  хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
  записи: дословность содержимого точки иначе не удерживается, а оборванное
  тело уходило под видом успешного `200`
2026-08-04 18:46:45 +03:00

141 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Критерии приёмки (из постановки, переживают удаление файла задачи)
Задача `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`, а живой архив
основного репозитория трогать запрещено; триггер прогона
(изменение правила разбора, идентичности или слияния) не сработал — маршрут
только читает витрину. Названо строкой в границах покрытия, а не замолчано