# Триаж ревью: точки метрики за период ## Сводка - **Профиль:** `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 в гипотезы названы поимённо выше.