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,268 @@
|
||||
# Триаж ревью: точки метрики за период
|
||||
|
||||
## Сводка
|
||||
|
||||
- **Профиль:** `deep`, режим прогона — по графу. Гейт **зелёный** (383
|
||||
изменённые исполняемые строки, 30 не покрыто, 92% — но см. находку 3: шаг
|
||||
покрытия не красит по построению).
|
||||
- **Расхождения состава прогона с профилем нет** (сверено с `docs/review.md`:
|
||||
`deep` = 6 обязательных проходов + `reimpl` по триггеру + `rubric` только в
|
||||
`design`).
|
||||
|
||||
**Проходы поимённо с исходом:**
|
||||
|
||||
Фаза `design` (до кода):
|
||||
|
||||
- `review-specs` (режим «дизайн до кода») — отработал, находки ушли правкой спек;
|
||||
- `review-rubric` (фаза 1) — отработал, свойства легли критериями в `tasks.md`;
|
||||
- `review-architecture` (на предложении) — отработал, под его находки код писался.
|
||||
|
||||
Фаза `deep` (по коду):
|
||||
|
||||
- `review-gate` — зелёный; отдельно нашёл дефект самого гейта (находка 3);
|
||||
- `review-specs` — отработал (декодирование имени, доли секунды, пустой
|
||||
`?layer=`, границы в тексте ошибки — исправлено инлайн; развилка про
|
||||
интервальную точку — находка 2);
|
||||
- `review-code` — отработал (имя метрики в `ETag` — исправлено инлайн);
|
||||
- `review-adversary` — отработал (обрыв тела, двойное декодирование —
|
||||
исправлено; размер ответа — находка 1; `nosniff` — гипотеза);
|
||||
- `review-ops` — отработал (предикат по слою: 13.9 мс → 0.026 мс; WARN вместо
|
||||
DEBUG; `ReadOnly` не запрещает запись — исправлено/названо; транзакция без
|
||||
бюджета — находка 1);
|
||||
- `review-architecture` — отработал (единый словарь слоёв, `Scope` в
|
||||
транспорте, `Spans` убран — исправлено инлайн);
|
||||
- `review-reimpl` — **не запускался**: проектный триггер (`docs/review.md`,
|
||||
«новое правило слияния, идентичности или разбора») не сработал — изменение
|
||||
только читает витрину; независимый взгляд на предложение дал профиль `design`.
|
||||
|
||||
**Счёт:** на вход триажа пришло 8 отложенных находок (плюс 13 позиций,
|
||||
заявленных как исправленные инлайн). Все 13 инлайн-позиций **проверены по коду и
|
||||
подтверждены** (`internal/httpapi/points.go`, `internal/points/points.go`,
|
||||
`internal/store/series.go`, `internal/hae/hae.go`, `internal/catalog/catalog.go`,
|
||||
`internal/httpapi/httpapi.go`; тесты пяти затронутых пакетов зелёные, включая
|
||||
`TestТочкиНеПерехватываютКаталог` и `TestСловарьСлоёвОдин`). Из 8 отложенных:
|
||||
4 в основном списке (одна — слияние двух причин), 2 понижены в гипотезы, 2
|
||||
правила ушли в promote. Ничего не выброшено молча — судьба каждой названа.
|
||||
|
||||
Дедупликация: «размер ответа» найден `adversary` и `ops` независимо — это один
|
||||
источник, высказавшийся дважды; приоритет поднят, `Confidence` — нет.
|
||||
|
||||
## Блокирует мердж
|
||||
|
||||
Пусто. Кандидат был один — ресурсный бюджет маршрута (находка 1), но обе его
|
||||
половины уже разложены владельцем в задачи текущего спринта
|
||||
(`read-api-response-limit`, `shutdown-and-migration-traces`), а риск
|
||||
материализуется только на деплое, который в этом проекте спрашивается всегда.
|
||||
Блокировать мердж значило бы отменить декомпозицию спринта решением триажа.
|
||||
|
||||
## Стоит исправить сейчас
|
||||
|
||||
### 1. Один широкий читатель способен уронить процесс вместе с приёмом — и доставки этого окна потеряны навсегда
|
||||
|
||||
- Файл: internal/store/series.go:134-190, internal/httpapi/points.go:188, docker-compose.yml
|
||||
- Severity: major
|
||||
- Confidence: high
|
||||
- Оракул: замеры проходов `adversary`+`ops`: неделя `raw` — 604 800 точек,
|
||||
1.75 с сборки, 1375 МиБ суммарных выделений; под HTTP — 2.89 с, тело
|
||||
279.7 МиБ, 1335 МиБ живой кучи; 4 одновременных запроса — 4322 МиБ.
|
||||
`docker-compose.yml` прочитан: `mem_limit` нет (есть `restart:
|
||||
unless-stopped`, что ограничивает окно простоя, но не отменяет его).
|
||||
`WriteTimeout` контекст запроса не отменяет — измерено проходом `ops`.
|
||||
Прецедент WAL: 51 МБ при лимите 8 МиБ.
|
||||
- Последствие: авторизованный читатель (свой же агент, опрашивающий по
|
||||
расписанию) четырьмя широкими запросами доводит процесс до OOM. Падает не
|
||||
маршрут — падает весь бинарь, включая приём; телефон шлёт молча и не
|
||||
перешлёт: доставки окна простоя теряются необратимо (`CLAUDE.md`: «Поток не
|
||||
останавливается»). Побочно: перекрывающиеся долгие читатели не дают
|
||||
продвинуться пассивному чекпойнту WAL. Вероятность низкая (клиенты — свои),
|
||||
ущерб — высший класс: необратимая потеря данных.
|
||||
- Предложение: работу не дублировать — обе половины уже в спринте
|
||||
(`read-api-response-limit` — предел размера, там же форма умолчания слоя;
|
||||
`shutdown-and-migration-traces` — дедлайн маршрута). Решить надо порядок:
|
||||
(а) мерджить сейчас, деплой — только после `read-api-response-limit` того же
|
||||
спринта (ноль работы, дисциплина деплоя и так требует вопроса к человеку);
|
||||
(б) то же плюс `mem_limit` в `docker-compose.yml` одной строкой уже сейчас —
|
||||
дешёвый стопор, превращающий OOM хоста в перезапуск контейнера;
|
||||
(в) сцепить мердж этой ветки с веткой предела в один батч.
|
||||
Сюда же слита развилка `rubric`: правило «при равном охвате — самый мелкий
|
||||
слой» максимизирует размер ответа; инверсия меняет правило, записанное в
|
||||
`docs/architecture.md` до этой задачи, и решается вместе с формой предела в
|
||||
`read-api-response-limit`, а не здесь.
|
||||
- Найдено проходом: adversary + ops (один источник дважды; оракулы — замеры)
|
||||
- Действие: развилка
|
||||
|
||||
### 2. Запрос «сон за ночь» молча не увидит эпизод, начавшийся до полуночи
|
||||
|
||||
- Файл: internal/store/series.go:233-256; openspec/changes/tochki-metriki-za-period/specs/points/spec.md:257-260
|
||||
- Severity: major
|
||||
- Confidence: high
|
||||
- Оракул: положение кода (`series.go:253`: `p.Start.Before(w.From)` →
|
||||
пропуск) и дословный пункт спеки: «точка-интервал, начавшаяся раньше `from`,
|
||||
в ответ не входит… запрос "сон за ночь с полуночи" не увидит эпизод,
|
||||
начавшийся до неё».
|
||||
- Последствие: потребитель получает ряд, выглядящий полным, — эпизод сна с
|
||||
23:40 отсутствует без единого признака усечения. Класс «молчание»: сон —
|
||||
профильный сценарий проекта, и решение потребителя принимается по неполным
|
||||
данным. Спека цену честно называет, поэтому это не сломанное требование, а
|
||||
открытая развилка дизайна.
|
||||
- Предложение: вопрос владельцу, варианты: (а) оставить как есть — правило
|
||||
симметрично адресации объектов, потребитель расширяет `from` сам (цена:
|
||||
каждый потребитель обязан знать про максимальную длину эпизода); (б) при
|
||||
выборке захватывать N часов до `from` и отбирать по пересечению интервала с
|
||||
периодом (цена: магическое число глубины N); (в) индекс по концу координаты
|
||||
(цена: миграция схемы и её сопровождение). Вариант (а) стоит нуля кода, но
|
||||
тогда правило обязано попасть в контракт read-api так же явно, как в спеку
|
||||
points.
|
||||
- Найдено проходом: specs
|
||||
- Действие: развилка
|
||||
|
||||
### 3. Гейт обещает красить непокрытую изменённую строку — и не красит никогда
|
||||
|
||||
- Файл: scripts/diff-coverage.py:88-96; scripts/gate.py:167-177; CLAUDE.md («Что красит безусловно»)
|
||||
- Severity: major
|
||||
- Confidence: high
|
||||
- Оракул: положение кода: `diff-coverage.py` `main()` возвращает `0` и при
|
||||
`uncovered > 0` (строка 96), `gate.py:172-173` пишет `OK` по нулевому коду
|
||||
возврата. Текущий прогон — живая демонстрация: 30 непокрытых строк, шаг
|
||||
`OK`, гейт зелёный.
|
||||
- Последствие: пункт `CLAUDE.md` «что красит безусловно: …непокрытая изменённая
|
||||
строка» ложен. Класс — молчание конвейера, тот самый, что в журнале ревью
|
||||
трижды за три дня («у проверки, которую гейт не гоняет, краснота никому не
|
||||
видна» — здесь хуже: проверка гоняется и молчит). Непокрытые строки будут
|
||||
копиться, а все читатели `CLAUDE.md` — включая проходы ревью — считают их
|
||||
невозможными.
|
||||
- Предложение: вопрос, потому что починка меняет состояние текущего прогона:
|
||||
(а) `return 1` при `uncovered > 0` — гейт этой задачи немедленно красный, и
|
||||
надо покрывать 30 строк веток ошибок драйвера (правки, которых никто не
|
||||
заказывал) либо гнать их через `//nolint`-аналог для покрытия; (б) привести
|
||||
`CLAUDE.md` к реальности: покрытие диффа — информационный шаг, печатает и не
|
||||
красит; (в) красить только строки вне уже признанных классов (ветки ошибок
|
||||
драйвера) — потребует механики исключений, которой нет. Дешевле всего (б) +
|
||||
отдельная задача на (а); выбирать не триажу — обещание записано владельцем.
|
||||
- Найдено проходом: gate
|
||||
- Действие: развилка
|
||||
|
||||
### 4. Граница, переваленная офсетом за 9999 год, молча опустошает ряд
|
||||
|
||||
- Файл: internal/httpapi/points.go:254-265; internal/store/store.go:263
|
||||
- Severity: minor
|
||||
- Confidence: high
|
||||
- Оракул: прогнан тест (временный, удалён): `9999-12-31T23:00:00-08:00` → UTC
|
||||
год 10000 → `FormatTime` даёт `10000-01-01T07:00:00Z`; `ParseTime` его не
|
||||
разбирает (`cannot parse "0-01-01…"`); лексикографически
|
||||
`'10000-…' < '2026-…'` — истина, значит `hour_utc BETWEEN '2020-…' AND
|
||||
'10000-…'` пуст всегда.
|
||||
- Последствие: уточнено против входной формулировки «роняет маршрут» — маршрут
|
||||
не падает: запрос «с 2020 до конца времён» с `to=9999-12-31T23:00:00-08:00`
|
||||
отвечает `200` и пустым рядом при непустых данных. Класс «молчание», но вход
|
||||
экзотический (нужен офсет, переваливающий год), клиенты — свои. Унаследовано
|
||||
от `store.FormatTime`, однако этот маршрут — первый, где клиент задаёт
|
||||
временные границы, то есть первый внешний путь к дефекту.
|
||||
- Предложение: в `parseBound` отвергать границу, чьё UTC-представление выходит
|
||||
за год 9999 (и симметрично — раньше года 1), с `400` и текстом без значений
|
||||
из запроса. Локально, однозначно, right-size.
|
||||
- Найдено проходом: наследие, названо оркестратором; оракул добыт триажем
|
||||
- Действие: инлайн
|
||||
|
||||
## Гипотезы без доказательства
|
||||
|
||||
- **`X-Content-Type-Options: nosniff` на читающих маршрутах** (adversary,
|
||||
minor, понижено). Сам проход назвал это «свойством без пути»: API отдаёт
|
||||
JSON под Bearer-токеном, браузерного потребителя нет, путь эксплуатации не
|
||||
построен. Одна строка в `setReadHeaders`
|
||||
(`internal/httpapi/conditional.go:145`) — но без пути это гигиена, а не
|
||||
находка; пусть едет с первой задачей, трогающей `conditional.go`.
|
||||
- **Машинно-различимый код причины отказа** (rubric, minor, понижено). Ни
|
||||
одного потребителя, различающего причины программно, ещё нет, а правка
|
||||
трогает общий `errorWire` — то есть контракт приёма, на который уже
|
||||
завязан телефон. Цена сейчас выше пользы; момент — появление первого
|
||||
программного потребителя ошибок (адаптер MCP). Правило — в promote.
|
||||
|
||||
## Promote candidates
|
||||
|
||||
- **«Обещание гейта подкрепляется кодом возврата шага»**: шаг, чей исход не
|
||||
влияет на код возврата `gate.py`, называется в `CLAUDE.md` информационным, а
|
||||
не «красящим безусловно». Кандидат в правило для `scripts/gate.py` и раздел
|
||||
«Гейт» `CLAUDE.md` (следствие находки 3 — расхождение обещания и кода прожило
|
||||
молча неизвестно сколько задач).
|
||||
- **«Тексты отказов — не контракт»**: клиенты не парсят человекочитаемые
|
||||
сообщения; при первом программном потребителе причин вводится машинный код
|
||||
единым решением для всех маршрутов (следствие пониженной находки rubric).
|
||||
|
||||
## Границы покрытия
|
||||
|
||||
**Профиль и режим:** `deep`, по графу; фаза `design` прогнана до кода. Гейт
|
||||
зелёный (с оговоркой находки 3 про шаг покрытия).
|
||||
|
||||
**Не запускалось и почему:**
|
||||
|
||||
- `review-reimpl` — триггер `docs/review.md` («новое правило слияния,
|
||||
идентичности или разбора») не сработал: изменение читает витрину, правила
|
||||
хранения не тронуты; независимый взгляд на замысел дал профиль `design`.
|
||||
Если промахнёмся классом «второй способ прочитать то же самое» — искать
|
||||
здесь.
|
||||
- `task verify:archive` — не прогнан: в worktree нет `./data`, живой архив
|
||||
основного репозитория запрещён оркестратором; триггер `CLAUDE.md` («перед
|
||||
изменением правила разбора, идентичности или слияния») не сработал —
|
||||
изменение сходимость журнала не трогает. Журнал ревью при этом трижды
|
||||
фиксирует красноту этого прогона от роста корпуса — очередной запуск остаётся
|
||||
за человеком на основной машине.
|
||||
- `task verify:busy` — **прогнан вручную, зелёный** (fold 24.08 с, replay
|
||||
23.60 с): долгий читатель этой задачи не развёл живую витрину с пересборкой.
|
||||
|
||||
**Что запущенные проходы не могли проверить в принципе (charter):**
|
||||
|
||||
- `specs` — соответствие спеки реальности формата за пределами `testdata`;
|
||||
- `code` — поведение под конкуренцией и на реальном потоке;
|
||||
- `adversary` — реальный профиль нагрузки (замеры сняты на синтетике по
|
||||
мотивам живых плотностей);
|
||||
- `ops` — поведение на боевом железе и на горизонте недель (WAL-чекпоинт под
|
||||
непрерывным опросом агента — экстраполяция из прецедента, не замер);
|
||||
- `architecture` — исполнение против замысла (судит форму, не поведение);
|
||||
- триаж — ничего нового не находит по определению; пропуск любого прохода —
|
||||
и его пропуск тоже.
|
||||
|
||||
**Непокрытые изменённые строки (30 из 383):** ветки распространения ошибок
|
||||
драйвера в `internal/store/series.go` (тот же класс, что уже непокрыт в
|
||||
`internal/store/catalog.go`) и ветка «ответ не подписался» (тот же класс, что в
|
||||
`internal/catalog/catalog.go`). Связка с находкой 3: гейт эти строки не красил
|
||||
бы в любом случае — класс признан, но признание нигде не записано, кроме этой
|
||||
секции.
|
||||
|
||||
**Подмена оракула критерия К1:** живой архив заменён реальными пакетами
|
||||
`internal/hae/testdata` на изолированном сервисе `:18080`. Пакеты реальные, но
|
||||
корпус — не архив: плотности и возраст данных другие, и оценка «на живых
|
||||
данных» остаётся на человеке.
|
||||
|
||||
**Не проверит ни один проход** (из `docs/review.md`): реальный профиль
|
||||
нагрузки — телефон шлёт молча и непрерывно, объём и частота меряются только по
|
||||
факту; поведение приложения HAE за пределами наблюдённого (расписание
|
||||
автоматизаций — пожелание, не гарантия); полнота словаря переводов после
|
||||
обновления iOS; секции, которых поток ещё не приносил (`symptoms`, `ecg`,
|
||||
`heartRateNotifications`, `cycleTracking`, `medications`) — разбор писался
|
||||
вслепую. Плюс общее: история инцидентов, поведение внешних систем в их версиях,
|
||||
завязка будущих потребителей (MCP-агентов) на текущую форму ответа и вопрос
|
||||
«нужна ли эта функциональность вообще».
|
||||
|
||||
**Перестали проверять сознательно** (отдельным списком — при следующем промахе
|
||||
первый вопрос «не тот ли это класс»):
|
||||
|
||||
- `task verify:archive` / `task verify:busy` вне гейта (в этой задаче: первый
|
||||
не прогнан с причиной выше, второй прогнан вручную);
|
||||
- класс «в Go так не пишут» — поимённая сверка со стайлгайдами не покрыта вовсе
|
||||
после упразднения `idiom`; в этой задаче никто не спросил «идиоматичен ли
|
||||
колбэк `pick` внутри транзакции» — судили форму и поведение, не идиому;
|
||||
- класс «чего нет в зрелой реализации такого узла» — вне профиля `design`;
|
||||
здесь `design`-фаза была, так что класс покрыт частично, на замысле, не на
|
||||
коде.
|
||||
|
||||
**Документы проекта:** нехватки нет — все входы триажа были на месте и
|
||||
непустые: инварианты с severity в `CLAUDE.md` (по ним ранжировано), «Типовые
|
||||
ложноположительные» в `docs/review.md` (ни одна из 8 отложенных находок под них
|
||||
не подпала — проверено поимённо), оба подраздела «Недоступно проверке»
|
||||
(разнесены выше), `docs/security.md` (периметр использован находкой про
|
||||
`ETag`), `docs/research/apple-health.md` (находки 34 и 36 использованы кодом).
|
||||
|
||||
**Потолок:** не потребовался — в основной список вошло 4 пункта из 4
|
||||
кандидатов; слияние «умолчание самого мелкого слоя» в находку 1 и понижение
|
||||
двух minor в гипотезы названы поимённо выше.
|
||||
Reference in New Issue
Block a user