httpapi: форма провода читающих маршрутов объявлена транспортом
- доменные типы internal/catalog лишились json-тегов и MarshalJSON; типы metricWire/layerWire/aggregationWire и перевод catalogWire живут в транспорте, тело отказа тоже получило объявленный тип — байты ответа не изменились - заведён сторож: обход графа типов ответа утверждает, что домен не доезжает до сериализации, плюс требование json-тега на полях транспортных структур и заведомо красные случаи к обоим правилам - решение с ценой обеих сторон записано в architecture.md и ADR; шаг lint в гейте получил свой кеш — общий на машину красил прогон находками из чужого worktree
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
# Триаж ревью — `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/`. Ни один проход о
|
||||
недостающем документе не заявил; отсев ложноположительных шёл по заполненному
|
||||
проектному списку (совпадений находок с ним не было — выбрасывать по нему
|
||||
ничего не пришлось). Строк деградации в этом прогоне нет.
|
||||
Reference in New Issue
Block a user