# Триаж ревью — `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/`. Ни один проход о недостающем документе не заявил; отсев ложноположительных шёл по заполненному проектному списку (совпадений находок с ним не было — выбрасывать по нему ничего не пришлось). Строк деградации в этом прогоне нет.