# Триаж ревью — `forma-provoda-chteniya`

## Сводка

- **Профиль:** `deep`, режим `по графу`. База диффа
  `bd832337df6dae5829432a9ee021b380cc6be1b1`, изменения застейджены.
- **Гейт:** зелёный, 13/13 шагов, покрытие изменённых строк 100%.
- **Проходы поимённо, с исходом** (состав сверен с профилем: `deep` = 6 кодовых
  проходов + `reimpl` по триггеру + триаж; расхождения с профилем нет):
  - `gate` — отработал, 2 находки (1 починена инлайн, починка проверена триажем);
  - `specs` (код против спек) — отработал, 2 находки, обе починены инлайн,
    починки проверены мутациями; `openspec validate --strict` — valid;
  - `code` (прозаические конвенции) — отработал, 1 находка, дубль находки 1
    `specs` (найден независимо), закрыт той же починкой;
  - `adversary` — отработал, 3 построенных пути (1 major, 2 minor) — **все три
    унаследованы от базы, диффом не внесены** (проверено триажем по списку
    файлов диффа и по диффу `internal/catalog/catalog.go`); плюс секция
    «атаковано и не пробилось» и 3 свойства без построенного пути;
  - `ops` — отработал, 1 находка minor, две гипотезы закрыты замерами;
  - `architecture` (по коду) — отработал, 1 находка minor с готовой развилкой;
  - `reimpl` — **не запускался**: триггер «новое правило слияния, идентичности
    или разбора» не сработал — дифф не трогает `internal/store`, `internal/fold`,
    `internal/hae` и схему (проверено списком файлов диффа);
  - `triage` (этот отчёт) — ничего нового не ищет по определению; пропуск
    любого прохода — его пропуск тоже.
- На профиле `design` (до кода) отдельно отработали `specs`, `rubric`,
  `architecture`; их находки закрыты в предложении, рубрика легла критериями
  Р1–Р12 в `tasks.md`.
- **Счёт находок:** на вход пришло 10 именованных находок и 3 свойства без
  построенного пути. После дедупликации по причине — 8 уникальных
  (`code` = `specs`-1; «ссылка на архив» у `architecture` = находка 2 `gate`).
  Из них 3 починены инлайн и проверены триажем мутациями. Открытых осталось 6:
  3 в секции «Стоит исправить сейчас», 3 унаследованных/задокументированных —
  строками в границах покрытия (в урожай). Гипотез — 2, promote — 3.

### Проверка инлайн-починок (не по пересказу — прогонами)

1. **Байтовые утверждения тела отказа** — есть
   (`internal/httpapi/catalog_test.go:216,233`). Мутация
   `json:"error"` → `json:"message"` в изолированной копии дерева →
   `TestКаталогОтдаётОжидаемыеБайтыОтказа` красный. До починки эта мутация была
   зелёной (оракул `specs`). **Починено.**
2. **Требование `json`-тега на полях формы провода** — есть
   (`internal/httpapi/wire_internal_test.go:72-82`), в таблице заведомо красных
   13 контролей, включая «определённый тип поверх домена» и «поле без тега».
   Две мутации в копии: (а) поле `aggregationWire.Hours` без тега →
   `TestФормаПроводаДоменаНеСодержит` красный; (б) `type layerWire
   catalog.LayerRange` с конверсией вместо перевода полем в поле → красный и
   сторож (4 поля «без json-тега»), и байтовые тесты. Ровно та калитка из
   находки 2 `specs` закрыта. **Починено.**
3. **`GOLANGCI_LINT_CACHE` в `scripts/gate.py`** — есть (строка 119, кеш в
   `tmp/gate/golangci`), `env` корректно сливается с `os.environ`
   (`run()`, строка 70). `ops` отдельно замерил: кеш не растёт без предела,
   повторный прогон 0.57 с. **Починено.**

Плюс контрольный прогон триажа: `go build ./...`, `go vet ./...`,
`go test ./internal/...` — целиком зелёные на застейдженном дереве.

---

## Блокирует мердж

Пусто. Единственный `major` прогона (путь `adversary` про метку года 10000)
воспроизведён триажем и **не принадлежит этому мерджу** — см. первый пункт
следующей секции: весь задействованный код (`internal/hae/hae.go`,
`internal/store/store.go`, `internal/store/catalog.go`) вне диффа, дифф его
поведения не меняет, а починка расширила бы scope задачи («трогается только
каталог и его транспорт») и по триггеру потребовала бы `reimpl` и
`verify:archive`/`verify:busy`. Прецедент дисциплины тот же, что в записи
2026-08-03 `docs/review.md`: краснота унаследована — заводится задачей, а не
чинится попутно.

Формулировка «критичных проблем не обнаружено» здесь не употребляется — что
именно проверено и что не могло быть проверено, см. «Границы покрытия».

## Стоит исправить сейчас

### 1. Одна принятая доставка с меткой года ≥10000 навсегда убивает маршрут чтения — 500 по всем метрикам

- Файл: `internal/hae/hae.go:812` → `internal/store/store.go:263` →
  `internal/store/catalog.go:174-182` → `internal/httpapi/catalog.go`
- Severity: major (унаследовано от базы, диффом не внесено)
- Confidence: high
- Оракул: падающий тест `adversary` (прогнан) + независимое воспроизведение
  триажем на уровне механизма: `time.Parse("2006-01-02 15:04:05 -0700",
  "9999-12-31 23:00:00 -0700")` принимается, `FormatTime` → `"10000-01-01T06:00:00Z"`,
  `ParseTime` (RFC 3339) на этой строке отказывает
  (`cannot parse "0-01-01T06:00:00Z" as "-"`). Побочно подтверждено:
  `"10000-…" < "2026-…"` как TEXT — защита горизонта «свидетельство из
  будущего» на этом входе не срабатывает.
- Последствие: строка уже в витрине и в архиве, `import + replay` её
  воспроизводит; лечится правкой кода **и** ручной правкой
  `./data/healthlog.db` — необратимой операцией из списка запретов. Класс
  «порча с низкой вероятностью» — выше любого гарантированного неудобства.
- Ловушка починки (названа `adversary`, сохранить в задаче): чинить только со
  стороны `ParseTime` нельзя — отказ `json.Marshal` станет достижим, и
  `writeJSON` отдаст 200 с пустым телом; плюс откроется дыра TEXT-горизонта.
  Чинить надо на приёме области значений (разбор HAE / `FormatTime`) вместе с
  горизонтом.
- Действие: **развилка.** Вопрос владельцу: (а) завести отдельную задачу в
  каталоге — рекомендация триажа: правка трогает разбор, по триггеру тянет
  `reimpl` + `verify:archive`/`verify:busy`, в рамки этой задачи не лезет;
  (б) чинить в этой ветке с расширением scope и полным повторным циклом;
  (в) принять риск без задачи — не рекомендуется: отказ молчащий до первого
  чтения, а лечение требует необратимой ручной операции.

### 2. Ссылка на архивный путь change с угаданной датой умрёт молча

- Файл: `docs/architecture.md:1638`
  (`openspec/changes/archive/2026-08-04-forma-provoda-chteniya/design.md`)
- Severity: minor (внесено диффом)
- Confidence: high
- Оракул: пути не существует (проверено `ls`); ссылка инлайн-кодом, `docs.py
  check` её не проверяет; дата архивации угадана — архивирование не сегодня
  ломает её без сигнала. Дубль: `gate` и `architecture` нашли независимо.
- Последствие: разбор чужих решений (единственное место, где записано «почему
  не как у Prometheus/Kubernetes») станет недостижим из обзора.
- Действие: **инлайн.** Убрать угаданную дату: сослаться на change по имени
  (`openspec/changes/forma-provoda-chteniya/design.md`, поправить при
  архивации) либо назвать документ словами без пути — на вкус оркестратора.

### 3. Маршрут, забывший строку в таблице образцов, останется без стража молча

- Файл: `internal/httpapi/wire_internal_test.go` (таблица `cases`,
  `TestФормаПроводаДоменаНеСодержит`)
- Severity: minor (внесено диффом — сторож новый, и он opt-in)
- Confidence: high
- Оракул: прогон `architecture` — новый обработчик с `catalog.Metric` в ответе,
  не добавленный в таблицу, не красит ни один тест; байтовый литерал нового
  маршрута закрепил бы доменные имена как норму.
- Последствие: забытая строка неотличима от отсутствия проблемы — ровно тот
  класс, против которого это же изменение завело конвенцию заведомо красного
  случая. Впереди четыре маршрута и MCP, каждый копирует образец.
- Действие: **развилка.** Варианты (сформулированы `architecture`):
  (а) тест полноты таблицы через `chi.Walk` — ~15 строк, забывание краснеет,
  цена — сцепка теста с роутером; (б) тестовый hook в `writeJSON` — ноль мест
  на новый маршрут, цена — шов в продакшн-коде; (в) оставить как есть — ноль
  строк, одна молчащая дыра на каждый забытый маршрут. Триаж склоняется к (а):
  сцепка с роутером дешевле шва в продакшне и дешевле молчащей дыры, но выбор
  затрагивает форму, которую скопируют пять раз, — потому развилка, не инлайн.

## Гипотезы без доказательства

- **`catalogWire` делит `*time.Time` со снимком (нет глубокой копии).**
  Понижено до nit: построенного пути нет — сегодня снимок не кешируется и
  алиасинг ненаблюдаем; станет наблюдаемым с первым кешем снимка. Источник —
  `adversary`, «свойства без построенного пути». Не действие, а память: при
  задаче о кеше перечитать.
- **Ненайденный маршрут пишет сырой `r.URL.Path` в лог уровня INFO.** Понижено
  до nit: путь до значения точки или токена через URL не построен (токены в
  заголовке), последствия не названо. Источник — `adversary`.

Ничего из `critical`/`major` понижать не пришлось: единственный `major` получил
оракул (дважды — проходом и триажем). Согласие проходов нигде не считалось
подтверждением: дубль `specs`+`code` закрыт одной починкой и проверен мутацией,
а не «подтверждён двумя проходами».

## Promote candidates

1. **Правило линтера: экспортированные типы вне `internal/httpapi` не несут
   `json`-тегов.** Сторож графа типов ослепнет, если доменная структура
   когда-нибудь получит теги обратно, — обход в неё не пойдёт как в «свою».
   Механизируемо (forbidigo/ручное правило по AST); источник — `adversary`.
2. **Записать трактовку «метка времени точки — координата, а не значение».**
   До WARN/ERROR доезжают `last_ts` и текст `parse time "10000-…"`; `adversary`
   назвал это границей, которую проект перешёл сознательно, но трактовка нигде
   не записана — следующий прогон поднимет её заново. Место —
   `docs/security.md` или `docs/conventions/logging.md`.
3. **Проверка инлайн-код-ссылок на существование пути в `docs.py check`.**
   Находка 2 существует ровно потому, что ссылка инлайн-кодом невидима
   проверке раскладки. Механизируемо; цена — ложные срабатывания на
   намеренно-будущих путях, потому promote, а не инлайн.

## Границы покрытия

**Прогон.** Профиль `deep`, режим «по графу». Запускались: `gate`, `specs`,
`code`, `adversary`, `ops`, `architecture`, триаж. На профиле `design` до кода —
`specs`, `rubric`, `architecture`.

**Не запускалось и почему:**

- `reimpl` — триггер «новое правило слияния, идентичности или разбора» не
  сработал: дифф не трогает `internal/store`, `internal/fold`, `internal/hae`,
  миграций нет (сверено триажем по списку файлов диффа, не по пересказу);
- `task verify:archive` и `task verify:busy` — изменение не трогает разбор,
  идентичность, слияние и схему; по правилу гейта их гоняет человек или
  оркестратор перед изменением этих правил. Если развилка 1 решится вариантом
  (б) — оба прогона и `reimpl` становятся обязательными;
- `rubric` на коде — по составу конвейера намеренно (судить код по критерию,
  под который он писался, — корреляция по построению); его свойства лежат
  критериями Р1–Р12 в `tasks.md` и проверены `specs` поимённо.

**Не влезло в потолок / унаследовано — в урожай задач, не выброшено:**

- каталог отдаёт 8 единиц из 12 без признака урезания
  (`internal/catalog/catalog.go`, `sortedKeys`, потолок 8) — minor, оракул
  прогнан `adversary`, унаследовано от базы (участок диффом не тронут);
- два WARN (`future data`, `aggregation style conflict`) стоят в теле обработки
  запроса — частота равна частоте чтений, условие не гаснет; проект правило
  «постоянный WARN обесценивает уровень» для себя уже формулировал (`fold.go`) —
  minor, оракул прогнан (10 запросов → 10 WARN), унаследовано;
- сторож графа типов слеп к полям `any`/`interface{}` — воспроизведено `ops`
  собственным тестом; риск назван в `design.md` (Risks) этим же изменением,
  вероятный путь эволюции (`json.RawMessage`) слепую зону обходит. Слепая зона
  сторожа, а не дефект диффа.

**Что запущенные проходы не могли проверить в принципе (из charter'ов):**

- `gate` — только механизируемое: смысл контракта, полноту таблицы образцов и
  честность спек он не видит;
- `specs` — исключение для дословного содержимого (`json.RawMessage`) проверено
  только на синтетике; MCP кода не имеет; четыре из пяти читающих маршрутов не
  написаны — какие байты они отдадут, проверять нечего; какие поля несёт тело
  отказа, дельта не говорит; правило «отсутствие как `null`» имеет предмет
  только у `*time.Time`;
- `adversary` — доказывает существование пути, не отсутствие: «атаковано и не
  пробилось» (побайтовый повтор ×50, `304` 0/12, `*time.Time`, значение в теле
  отказа, отказ `Marshal`) — это невыведенные атаки, а не гарантия;
- `ops` — поведение под реальным потоком телефона не наблюдал; откат бинаря
  проверен рассуждением и байтовой сверкой, не прогоном на живом стенде;
- `architecture` — соответствие реализации дельте по пунктам не его работа
  (названо им самим про `scripts/gate.py`, которого нет в `Impact`
  предложения; `specs` это покрыл: изоляция кеша — находка `gate`, а не
  самодеятельность);
- триаж — ничего нового не находит по определению; пропуск любого прохода —
  его пропуск тоже.

**Ослабленный оракул замеров.** На машине фоново живут чужой процесс
`healthlog serve` на `:18099` и рабочий контейнер на `:8080`; в замерах они не
участвовали, но общий шедулинг ОС разбавляет числа `ops` (6.16 мкс/оп на живом
размере каталога, 305 мкс на пределе, 0.57 с повторный прогон линтера) —
порядки надёжны, точные значения нет.

**Целиком на человеке — «Недоступно проверке» из `docs/review.md`, двумя
списками, не слитыми.**

*Не проверит ни один проход:*

- реальный профиль нагрузки: телефон шлёт молча и непрерывно, объём и частота
  меряются только по факту;
- поведение приложения HAE за пределами наблюдённого — расписание
  автоматизаций пожелание, а не гарантия (разведка, находка 28);
- полнота словаря переводов после обновления iOS;
- секции, которых поток ещё не приносил: `symptoms`, `ecg`,
  `heartRateNotifications`, `cycleTracking`, `medications` — разбор писался
  вслепую, проход судит форму кода, не соответствие реальности.

*Перестали проверять сознательно:*

- прогон живого архива (`task verify:archive`) и свёртка под удерживаемой
  блокировкой (`task verify:busy`) в гейт не входят (минута и ~50 секунд,
  данные только на этой машине); гоняет человек перед задачей, трогающей
  разбор или слияние (записи 2026-08-02…04 — три красноты этого класса подряд);
- класс «в Go так не пишут» — поимённая сверка с Effective Go, Go Code Review
  Comments, стайлгайдами Uber и Google — не покрыт вовсе после упразднения
  `idiom`;
- класс «чего нет в зрелой реализации такого узла» — вне профиля `design`.

*Плюс общее, что не покрывает никакой конвейер:* история инцидентов, поведение
под реальным потоком, поведение внешних систем в их версиях, завязка
потребителей на текущее поведение (у каталога потребителей сегодня нет — задача
на этом стоит, и это проверяемо лишь отсутствием знания о чужих проектах) и
вопрос «нужна ли эта функциональность вообще».

**Документы проекта.** Всё, что нужно триажу, на месте: `CLAUDE.md` с
инвариантами и severity, `docs/review.md` со всеми четырьмя нужными разделами
(журнал, типовые ложноположительные, вопросы к проходам, «Недоступно
проверке»), `docs/security.md`, `docs/conventions/`. Ни один проход о
недостающем документе не заявил; отсев ложноположительных шёл по заполненному
проектному списку (совпадений находок с ним не было — выбрасывать по нему
ничего не пришлось). Строк деградации в этом прогоне нет.
