- доменные типы internal/catalog лишились json-тегов и MarshalJSON; типы metricWire/layerWire/aggregationWire и перевод catalogWire живут в транспорте, тело отказа тоже получило объявленный тип — байты ответа не изменились - заведён сторож: обход графа типов ответа утверждает, что домен не доезжает до сериализации, плюс требование json-тега на полях транспортных структур и заведомо красные случаи к обоим правилам - решение с ценой обеих сторон записано в architecture.md и ADR; шаг lint в гейте получил свой кеш — общий на машину красил прогон находками из чужого worktree
24 KiB
Триаж ревью — forma-provoda-chteniya
Сводка
- Профиль:
deep, режимпо графу. База диффаbd832337df6dae5829432a9ee021b380cc6be1b1, изменения застейджены. - Гейт: зелёный, 13/13 шагов, покрытие изменённых строк 100%.
- Проходы поимённо, с исходом (состав сверен с профилем:
deep= 6 кодовых проходов +reimplпо триггеру + триаж; расхождения с профилем нет):gate— отработал, 2 находки (1 починена инлайн, починка проверена триажем);specs(код против спек) — отработал, 2 находки, обе починены инлайн, починки проверены мутациями;openspec validate --strict— valid;code(прозаические конвенции) — отработал, 1 находка, дубль находки 1specs(найден независимо), закрыт той же починкой;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= находка 2gate). Из них 3 починены инлайн и проверены триажем мутациями. Открытых осталось 6: 3 в секции «Стоит исправить сейчас», 3 унаследованных/задокументированных — строками в границах покрытия (в урожай). Гипотез — 2, promote — 3.
Проверка инлайн-починок (не по пересказу — прогонами)
- Байтовые утверждения тела отказа — есть
(
internal/httpapi/catalog_test.go:216,233). Мутацияjson:"error"→json:"message"в изолированной копии дерева →TestКаталогОтдаётОжидаемыеБайтыОтказакрасный. До починки эта мутация была зелёной (оракулspecs). Починено. - Требование
json-тега на полях формы провода — есть (internal/httpapi/wire_internal_test.go:72-82), в таблице заведомо красных 13 контролей, включая «определённый тип поверх домена» и «поле без тега». Две мутации в копии: (а) полеaggregationWire.Hoursбез тега →TestФормаПроводаДоменаНеСодержиткрасный; (б)type layerWire catalog.LayerRangeс конверсией вместо перевода полем в поле → красный и сторож (4 поля «без json-тега»), и байтовые тесты. Ровно та калитка из находки 2specsзакрыта. Починено. 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
- Правило линтера: экспортированные типы вне
internal/httpapiне несутjson-тегов. Сторож графа типов ослепнет, если доменная структура когда-нибудь получит теги обратно, — обход в неё не пойдёт как в «свою». Механизируемо (forbidigo/ручное правило по AST); источник —adversary. - Записать трактовку «метка времени точки — координата, а не значение».
До WARN/ERROR доезжают
last_tsи текстparse time "10000-…";adversaryназвал это границей, которую проект перешёл сознательно, но трактовка нигде не записана — следующий прогон поднимет её заново. Место —docs/security.mdилиdocs/conventions/logging.md. - Проверка инлайн-код-ссылок на существование пути в
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,3040/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/. Ни один проход о
недостающем документе не заявил; отсев ложноположительных шёл по заполненному
проектному списку (совпадений находок с ним не было — выбрасывать по нему
ничего не пришлось). Строк деградации в этом прогоне нет.