- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
объявляет слой, измеренный род, его применимость к отданному ряду и границу
окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
записи: дословность содержимого точки иначе не удерживается, а оборванное
тело уходило под видом успешного `200`
13 KiB
13 KiB
Критерии приёмки (из постановки, переживают удаление файла задачи)
Задача 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. Чтение витрины
- 1.1
store.ReadSeries(ctx, window, pick)— один вход, одна транзакция чтения: охваты слоёв, точки выбранного слоя, объекты окна измерения. Правило выбора слоя приходит функцией-параметром и остаётся в домене - 1.2 Охваты слоёв:
min(first_ts),max(last_ts), охваты поGROUP BY layerв границах часов[trunc(from), trunc(to)]— по покрывающему индексуbucket_catalog, без чтения содержимого (Р10) - 1.3 Точки выбранного слоя: объекты по точному префиксу первичного ключа,
разжатие, отбор по началу координаты до
[from, to) - 1.4 Окно измерения одной метрики: переиспользует
commonHoursиreadHourPairs, второго правила отбора не заводит - 1.5 Тесты хранилища: точка
10:59из объекта10:00не теряется; точка ровно наfromесть, ровно наto— нет; период короче часа, где крупный слой имеет объект без точек внутри, а мелкий — точки (Р3); пустой период
2. Use-case «ряд точек» (internal/points)
- 2.1 Пакет
internal/points: тип запроса, тип ответа,Serviceнадstoreиcatalog - 2.2 Правило выбора слоя: охват = длина пересечения
[первая метка, последняя метка]слоя с периодом; пустое пересечение выбывает; наибольший охват, при равенстве — самый мелкий; явный слой отменяет правило и всегда уезжает в ответ (Р3) - 2.3 Род через
catalog.Measure, применимость —falseприunknown, приcumulativeна слоеrawи при отсутствии выбранного слоя (Р1) - 2.4 Метка ответа: версия витрины + горизонт, огрублённый до часа
(
catalog.Stamp, не второй экземпляр) + канонизированная форма запроса; род и метка снимаются с одного горизонта (Р2) - 2.5 Логирующий чекпоинт исхода один и на доменной границе; отмена и
занятость базы —
DEBUG, настоящий отказ —ERROR; значений точек и границ запроса в записи нет, имя метрики обрезано; предупреждения измерения маршрут точек не повторяет (Р12) - 2.6 Тесты домена: охваты различаются; охваты равны; ни одного слоя; явный слой пуст; период короче часа; применимость рода на четырёх слоях
3. Транспорт и форма провода
- 3.1 Маршрут
GET /api/v1/metrics/{name}под токеном чтения; тест, что он не перехватываетGET /api/v1/metrics; имя метрики берётся процентно декодированным - 3.2 Разбор параметров:
from/toобязательны, RFC 3339 с явной зоной,from < to,layerиз словаря, присутствиеbucket—400; каждый отказ до чтения витрины, с человекочитаемым сообщением без значений из запроса (Р7) - 3.3 Форма провода точек по образцу
catalog.go: типы*Wireсjson-тегами, перевод присваиванием поле в поле,values—json.RawMessage - 3.4 Строка в таблице образцов
wire_internal_test.go(Р11) - 3.5
writeJSONперестаёт экранировать HTML-символы (общее правилоread-api); тест на значении точки с&,<,>(Р8) - 3.6 Метка и
Cache-Controlчерез существующийsetReadHeaders; тесты: различие меток по каждому параметру по очереди, совпадение при эквивалентной записи времени, различие при переходе горизонта через час (Р2) - 3.7 Байтовое утверждение формы ответа на фиксированной витрине с
часами в прошлом — ни одно поле литерала не зависит от хода часов (прецедент
docs/review.md, 2026-08-03) - 3.8 Тесты маршрута: К2 (все поля конверта присутствуют всегда),
К3 (род
unknownназван словом,applicable: false), точка-интервал (ts_end), дословностьvalues, пустой период без слоя (layer: null), пустой период с явным слоем (layerэхом), незнакомая метрика,401
4. Документация
- 4.1
docs/architecture.md, раздел «Read API»: форма ответа точек (aggregationобъектом,applicable,ts_end), правило выбора слоя формулировкой из спеки, строка маршрута с пометкой о неподдержанномbucket, строкаpointsв таблице компонентов - 4.2
config.example.toml: строка проread_tokensназывает точки рядом с каталогом
5. Верификация
- 5.1
task gateзелёный - 5.2 Поведенческая верификация: свой
config.tomlв worktree (порт:18080, база и архив в./tmp/), наполнение реальными пакетамиinternal/hae/testdataчерез маршрут приёма, затем К1 — метрика за год одним запросом; рабочий./dataне трогается - 5.3 Замер цены маршрута числом на трёх режимах, включая худший
(Р4, прецедент
docs/review.md2026-08-04 «замер снят на корпусе, где измеряемого случая не бывает»): редкая метрика за год; плотная метрика за сутки вminute; плотная метрика за неделю вraw. Корпус собирается размножением реальных точекtestdata, а не литералами; метод записывается рядом с числом - 5.4
task verify:busy— зелёный (fold 24.08 с, replay 23.60 с).task verify:archiveне прогнан: в worktree нет./data, а живой архив основного репозитория трогать запрещено; триггер прогона (изменение правила разбора, идентичности или слияния) не сработал — маршрут только читает витрину. Названо строкой в границах покрытия, а не замолчано