# Триаж ревью: точки метрики за период

## Сводка

- **Профиль:** `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 в гипотезы названы поимённо выше.
