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