- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
объявляет слой, измеренный род, его применимость к отданному ряду и границу
окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
записи: дословность содержимого точки иначе не удерживается, а оборванное
тело уходило под видом успешного `200`
636 lines
62 KiB
Markdown
636 lines
62 KiB
Markdown
# Ревью: настройка и журнал
|
||
|
||
Конвейер — скилл `av-dev-pipeline:review-pipeline`, проходы — агенты
|
||
`av-dev-pipeline:review-*`. Здесь только проектная часть: чем этот проект
|
||
отличается от умолчаний конвейера и что в нём уже проскакивало.
|
||
|
||
## Как настроен конвейер
|
||
|
||
### Типовые узлы
|
||
|
||
Рода узлов проекта и свойства, по которым судится каждый. Рода, а не инвентарь
|
||
пакетов: род, который проект задумал, но ещё не написал, включён намеренно.
|
||
|
||
**Разбор пакета HAE** (`internal/hae`)
|
||
|
||
- Точка сохраняется дословно; ничего внутри неё не отбрасывается и не
|
||
переименовывается.
|
||
- Непонятое содержимое не роняет доставку: она принята, непокрытое названо.
|
||
- Слой выводится из выравнивания меток, а не из заголовка HAE — тот врёт.
|
||
- Текст ошибки не содержит значений из входа — род токена и смещение.
|
||
- Тест гоняется на реальном пакете из `testdata`, а не на выдуманном.
|
||
|
||
**HTTP-обработчик приёма** (`internal/httpapi`)
|
||
|
||
- Код ответа отражает доставку, а не разбор: битый JSON — 400, непонятое
|
||
содержимое — 200.
|
||
- Тело попадает в архив раньше, чем в разбор; потеря архива необратима.
|
||
- Тело и заголовки в лог выше `DEBUG` не уезжают, токены — никогда.
|
||
- Есть названный предел на размер тела и на заголовки.
|
||
|
||
**Свёртка и репозиторий часовых объектов** (`internal/store`, `internal/fold`)
|
||
|
||
- Результат — функция **префикса** журнала: узел не читает состояние, которое
|
||
сам же меняет, без границы по `received_at` разбираемой доставки.
|
||
- Правило выбора между версиями — функция множества версий либо явно функция
|
||
порядка журнала; третьего состояния нет.
|
||
- Столкновение разрешается полнотой, а не свежестью; изменение запечатанного
|
||
часа пишется `WARN`, но данные пишутся.
|
||
- Транзакция не держит блокировку дольше `busy_timeout`: канонизация и
|
||
сжатие — вне её.
|
||
|
||
**Файловый архив и ретеншен** (`internal/archive`)
|
||
|
||
- Путь строится из значений, которых отправитель не контролирует.
|
||
- Удаление тела опирается на колонку, отличающую ноль от «не измерялось».
|
||
- Место на диске и рост каталога названы числом.
|
||
|
||
**Проигрыватель журнала и CLI** (`internal/replay`, `cmd/`)
|
||
|
||
- Повторный прогон даёт то же состояние и тот же отпечаток.
|
||
- Новая единица хранения входит в отпечаток и в счётчики отчёта.
|
||
- Расход памяти не растёт вместе с длиной журнала.
|
||
- Подмена базы — решение человека при остановленном сервисе, не команды.
|
||
|
||
**Обработчик чтения и адаптер MCP** (Read API, MCP — ещё не написаны)
|
||
|
||
- Агрегат считается только там, где род свёртки измерен; нижний слой HAE не
|
||
суммируется никогда.
|
||
- Ответ имеет предел размера, и предел объявлен, а не подразумевается.
|
||
- Адаптер MCP собственной логики не несёт — те же обработчики.
|
||
|
||
### Типовые ложноположительные
|
||
|
||
Находки, которые здесь выглядят убедительно и всегда неверны.
|
||
|
||
- **«Ответ 200 на непонятое содержимое проглатывает ошибку.»** Не дефект:
|
||
инвариант «сохранили — значит приняли». Телефон шлёт молча и не
|
||
перешлёт — код ответа отражает доставку, а не разбор.
|
||
- **«`source` не входит в ключ — идентичность неполна.»** Не дефект: поле
|
||
измерено нестабильным (разведка, находка 36), включение его в ключ задваивает
|
||
точки.
|
||
- **«Точка хранится избыточно, поля дублируются.»** Не дефект: точки хранятся
|
||
дословно, инвариант прямой. Экономия здесь необратима.
|
||
- **«Часовой объект не считает агрегат при записи.»** Не дефект: своей
|
||
агрегации в хранении нет, род свёртки выводится сверкой слоёв в ответе.
|
||
- **«У одной метки три записи сна — дубликат.»** Не дефект: у точки-измерения
|
||
конец равен началу, под одной меткой лежит до трёх записей.
|
||
- **«Русские строки в значениях — незакрытая локализация.»** Наполовину: строки
|
||
приходят на языке телефона, и это факт источника; дефектом является только
|
||
отсутствие стабильного кода рядом с переводом.
|
||
|
||
### Вопросы к проходам
|
||
|
||
Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Задаются дополнительно к
|
||
обязательным.
|
||
|
||
- `ops`: читает ли узел состояние, которое сам же меняет, и остаётся ли
|
||
результат функцией от **префикса** журнала (запись 2026-08-01, свёртка не
|
||
воспроизводилась при пересборке).
|
||
- `ops`: поведение библиотеки, драйвера и `PRAGMA` измерено или вычитано из
|
||
документации; что возвращается в **вырожденном** случае и отличим ли этот
|
||
ответ от штатного (запись 2026-08-02 про упразднение `idiom`; прецедент
|
||
`-1 >= -1` — 1492 тика из 5502).
|
||
- `ops`: хватит ли сигналов владельцу, когда поток оборвётся ночью (переселено
|
||
из упразднённого `negative`).
|
||
- `architecture`: не изобретаем ли то, что уже есть в стандартной библиотеке —
|
||
своя абстракция, повторяющая форму существующей (переселено из `idiom`).
|
||
- `architecture`: что опытный человек отсюда удалил бы (переселено из
|
||
`negative`).
|
||
- `rubric`: пришпилено ли утверждение теста к числу, производному от размера
|
||
корпуса — корпус растёт с каждой доставкой (запись 2026-08-02, прогон живого
|
||
архива был красным).
|
||
- `adversary`: доводится ли значение точки или тело доставки до лога выше
|
||
`DEBUG` хотя бы одним путём (запись 2026-08-02, тело 8 МиБ в тексте ошибки).
|
||
- `triage`: перечислены ли запущенные проходы поимённо и с исходом; непущенный
|
||
проход идёт в границы покрытия строкой «не запускался» (запись 2026-08-02,
|
||
чекпоинт кода прошёл без трёх проходов).
|
||
|
||
### Триггеры профиля
|
||
|
||
Уточняет умолчания конвейера, не отменяет их.
|
||
|
||
- **`deep`** — изменения в правиле разбора, идентичности, слияния или вывода
|
||
слоя; миграции схемы; всё, что трогает `internal/store`, `internal/fold`,
|
||
`internal/replay`.
|
||
- **«Поведение, видимое снаружи»** здесь — код ответа приёма, форма ответа
|
||
чтения, содержимое архива и **состояние, которое даёт пересборка**: витрина
|
||
наблюдаема через пересборку, поэтому расхождение с журналом — внешнее
|
||
поведение, а не внутренняя деталь.
|
||
- **`reimpl`** запускается по триггеру «новое правило слияния, идентичности или
|
||
разбора». Единственный раз, когда триаж назвал его отсутствие дырой
|
||
покрытия, — это была задача с новым правилом слияния сущностей.
|
||
Второй замер (2026-08-03, словарь категориальных значений): триггер сработал
|
||
на новом правиле разбора и ключе реестра, проход **окупился** — он независимо
|
||
подтвердил замером две находки, до того имевшие только одно измерение (пик
|
||
памяти накопителя: 1002 МиБ против 780 на базе; единицы счётчика отброшенных),
|
||
и отдельно назвал семь мест, где существующее решение оказалось **лучше** его
|
||
собственного. Второе ценно не меньше первого: оно показывает, где проход
|
||
соглашается, а не только где спорит.
|
||
- **`quick`** — правка документов, конфигурации, сообщений; ничего, что меняет
|
||
хранимое.
|
||
|
||
### Недоступно проверке
|
||
|
||
**Не проверит ни один проход.** Реальный профиль нагрузки: телефон шлёт молча и
|
||
непрерывно, объём и частота меряются только по факту. Поведение приложения HAE
|
||
за пределами наблюдённого — расписание автоматизаций пожелание, а не гарантия
|
||
(разведка, находка 28). Полнота словаря переводов после обновления iOS.
|
||
Секции, которых поток ещё не приносил: `symptoms`, `ecg`,
|
||
`heartRateNotifications`, `cycleTracking`, `medications` — разбор писался
|
||
вслепую, и проход может судить только форму кода, не соответствие реальности.
|
||
|
||
**Перестали проверять сознательно.**
|
||
|
||
- **Шаг покрытия диффа гейт не красит.** `CLAUDE.md` объявляет, что непокрытая
|
||
изменённая строка красит гейт безусловно; `scripts/diff-coverage.py` всегда
|
||
возвращает `0`, и шаг печатает `OK` при любом покрытии. То есть «гейт зелёный»
|
||
не означает «покрытие диффа полное», и разбор непокрытых строк остаётся
|
||
человеку или проходу. Найдено проходом `gate` 2026-08-04, подтверждено
|
||
триажем; чинить нельзя мимоходом — починка немедленно красит гейт задачи, в
|
||
которой её сделали.
|
||
- Прогон живого архива (`task verify:archive`) и свёртка под удерживаемой
|
||
блокировкой (`task verify:busy`) в гейт не входят: минута и около 50 секунд
|
||
соответственно, плюс данные, которых нет ни на какой другой машине. Гоняет их
|
||
человек перед задачей, трогающей разбор или слияние (запись 2026-08-02,
|
||
прогон живого архива был красным и об этом никто не знал).
|
||
- Класс «в Go так не пишут» — поимённая сверка с Effective Go, Go Code Review
|
||
Comments, стайлгайдами Uber и Google — не покрыт вовсе после упразднения
|
||
`idiom`. Класс обратимый, портит форму кода, а не данные, но признавать это
|
||
надо в границах покрытия, а не считать проверенным (запись 2026-08-02).
|
||
- Класс «чего нет в зрелой реализации такого узла» — вне профиля `design`.
|
||
|
||
## Журнал дефектов
|
||
|
||
Запись на каждый воспроизведённый дефект, сразу, а не ретроспективно: со
|
||
временем теряется не факт, а причина непоймания.
|
||
|
||
## 2026-08-04 — правило выбора слоя мерило одно, а отбор шёл по другому [пойман]
|
||
|
||
**Что было.** Правило выбора слоя ответа Read API мерило охват **часами
|
||
объектов**, а ряд отбирался **точной меткой точки**. На периоде короче часа
|
||
множества расходятся: часовой объект попадает в границы часов запроса, а его
|
||
единственная точка в период не попадает. Ответ уходил бы пустым при непустых
|
||
данных соседнего слоя — с непустым `layer`, то есть неотличимо от честной
|
||
пустоты только по числу точек.
|
||
|
||
**Почему поймано.** Профиль `design` на предложении, до кода: и `review-specs`,
|
||
и `review-rubric` построили один и тот же вход независимо друг от друга
|
||
(`from = 10:30`, `to = 10:45`). На готовом коде находка стоила бы переписывания
|
||
выборки; на предложении — абзаца.
|
||
|
||
**Что сделано.** Охват меряется метками точек (`first_ts`/`last_ts` уже лежат в
|
||
покрывающем индексе). Класс промоутнут в
|
||
`docs/conventions/storage.md` — «предикат выбора источника и предикат отбора
|
||
данных используют одну границу»: он повторится всюду, где огрубление ради
|
||
полноты выборки соседствует с точным фильтром.
|
||
|
||
## 2026-08-04 — чекпоинт, заведённый ревью, не существовал бы в проде [пойман]
|
||
|
||
**Что было.** Враждебный проход построил путь «ответ оборвался по `WriteTimeout`
|
||
на середине, а `accessLog` написал `200`»: тело в 13 МиБ доехало на 2.7 МиБ,
|
||
клиент получил нечитаемый JSON, лог сообщил успех. Чекпоинт об обрыве завели —
|
||
и поставили ему уровень `DEBUG`.
|
||
|
||
**Почему поймано.** Эксплуатационный проход прочитал **боевой** конфиг
|
||
(`config.docker.toml`, `level = "info"`) и показал, что запись уровня `DEBUG`
|
||
не проходит фильтр `slog` никогда. То есть находка была закрыта наблюдаемостью,
|
||
которой в проде не существует.
|
||
|
||
**Что сделано.** Уровень поднят до `WARN`. Правило, которое из этого следует:
|
||
**уровень нового чекпоинта сверяется с боевым конфигом, а не с тем, что видно в
|
||
тестах** — в тестах уровень всегда `DEBUG`.
|
||
|
||
Реализованные задачи, находки ревью и решения сюда не пишутся: у них есть
|
||
коммит, спека и задача. Здесь только промахи конвейера и решения о его составе.
|
||
|
||
Форма:
|
||
|
||
<!-- копия: журнал-дефектов-форма из av-dev-pipeline/skills/review-pipeline/references/review-journal.md -->
|
||
## ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман]
|
||
|
||
- **Где:** путь:строка либо «конвейер, а не код»
|
||
- **Симптом:** как обнаружилось, кем и когда
|
||
- **Причина:** что на самом деле было не так
|
||
- **Чем воспроизведён:** тест, команда, замер — с числами
|
||
- **Почему не поймали:** только для проскочивших — какой проход обязан был найти
|
||
и что ему помешало
|
||
- **Что меняем:** правило прохода, шаг гейта, конвенция, факт в документе
|
||
проекта — либо «ничего, цена поимки выше цены дефекта»
|
||
<!-- /копия: журнал-дефектов-форма -->
|
||
|
||
Последний пункт важнее остальных. Вывод «ничего не меняем» — законный исход:
|
||
не всякий дефект стоит того, чтобы усложнять ради него ревью каждой задачи.
|
||
|
||
---
|
||
|
||
## 2026-08-01 — свёртка не воспроизводилась при пересборке журнала [проскочил]
|
||
|
||
- **Где:** `internal/store/delivery.go`, `LastDerivedLayer`
|
||
- **Симптом:** прогон живого архива (99 доставок) вторым проходом дал 1742
|
||
объекта вместо 1737, а координат сна 182 вместо 174. Нашёл тест сходимости
|
||
на шаге apply — не ревью.
|
||
- **Причина:** доставка без плотных метрик наследует слой автоматизации.
|
||
Запрос брал последний выведенный слой **вообще**, а не последний до этой
|
||
доставки, поэтому при пересборке доставка наследовала слой «из будущего».
|
||
Свёртка переставала быть функцией от префикса журнала.
|
||
- **Почему не поймали:** формулировка «наследует последний надёжно выведенный
|
||
слой той же автоматизации» звучит однозначно и в спеке, и в дизайне —
|
||
пропущенное слово «предшествующей» не выглядит пропуском. Проходы `specs` и
|
||
`architecture` сверяли код со спекой и понятиями, а инвариант
|
||
«`import + replay` даёт то же состояние» ни один из них не проверял на
|
||
конкретном правиле: он записан в архитектуре как свойство системы, а не как
|
||
критерий для каждого узла, читающего состояние.
|
||
- **Что меняем:** в проходы `rubric` и `ops` — вопрос
|
||
«читает ли узел состояние, которое сам же меняет, и остаётся ли он функцией
|
||
от префикса журнала». Дешевле правила: любой запрос к `delivery` из свёртки
|
||
обязан иметь границу по `received_at` разбираемой доставки. Тест сходимости
|
||
на живом архиве (`internal/fold/replay_test.go`) остаётся постоянным —
|
||
именно он это поймал.
|
||
|
||
## 2026-08-02 — прогон живого архива был красным и об этом никто не знал [проскочил]
|
||
|
||
- **Где:** `internal/fold/replay_test.go` (перенесён в `internal/replay/archive_test.go`)
|
||
- **Симптом:** первый же запуск `task verify:archive` в задаче про пересборку
|
||
дал `координат sleep_analysis 222, измерено 174`. Проверено прогоном прежней
|
||
редакции теста на том же архиве: она даёт ровно те же 222, 2049 объектов и тот
|
||
же отпечаток — значит тест покраснел не от изменений задачи, а сам, когда
|
||
архив дорос с 94 доставок до 116.
|
||
- **Причина:** утверждение было пришпилено к **числу, производному от корпуса**
|
||
(174 координаты сна). Корпус растёт с каждой доставкой, то есть константа
|
||
протухает по расписанию телефона. Проверяемое свойство при этом другое и от
|
||
размера корпуса не зависит: ключ по интервалу не схлопывает записи до ключа
|
||
по метке (222 координаты против 218 меток).
|
||
- **Почему не поймали:** прогон живого архива намеренно не входит в `task gate`
|
||
(минута работы, данные есть только на этой машине). У проверки, которую гейт
|
||
не гоняет, краснота никому не видна — она обнаруживается только следующей
|
||
задачей, которая до неё дотянется. Ни один проход ревью прогон не запускал:
|
||
проходы читают код, а не гоняют опциональные команды.
|
||
- **Что меняем:** утверждение переписано на само свойство (координат строго
|
||
больше, чем различных меток), измеренные числа остались в `t.Logf`. Правило
|
||
общее и годится в конвенции: **в проверке на живом корпусе нельзя утверждать
|
||
число, производное от размера корпуса** — утверждать надо инвариант, а число
|
||
печатать. Гейт при этом не трогаем: цена ежедневной минуты выше цены такой
|
||
протухшей константы, а после этой задачи прогон стал ещё и единственным, кто
|
||
проверяет настоящий проигрыватель журнала.
|
||
|
||
## 2026-08-02 — чекпоинт кода прошёл без трёх проходов, и ровно они нашли всё [проскочил]
|
||
|
||
- **Где:** конвейер, а не код: коммит `f8200f7` («тренировки и записи с
|
||
собственным `id`»), шаг 7 пайплайна задачи (тогда — проектная копия
|
||
`healthlog-task-pipeline`, ныне `av-dev-pipeline:task-pipeline`), профиль
|
||
`deep`.
|
||
- **Симптом:** изменение было закоммичено и заархивировано как прошедшее ревью.
|
||
Дозапуск трёх пропущенных проходов на **уже закоммиченном** коде дал девять
|
||
причин, семь из которых пошли в работу с прогнанными оракулами: скелет из
|
||
`null` затирает маршрут молча и необратимо; одно поле не той формы уносит
|
||
тренировку, а доставка при этом числится разобранной; откат бинаря поверх
|
||
новой схемы стартует без слова; победитель внутри доставки зависит от порядка
|
||
элементов на проводе; провенанс устаревает на каждой повторной присылке;
|
||
канонизация идёт внутри транзакции вопреки собственному комментарию (768 МиБ
|
||
пика, 5.019 с удержания блокировки); тело в 8 МиБ целиком уезжает в текст
|
||
ошибки и оттуда в `WARN`.
|
||
- **Причина:** сабагент, проводивший задачу, на чекпоинте кода запустил не все
|
||
проходы профиля `deep` — не отработали `adversary`, `ops` и архитектурный.
|
||
Отчёт триажа при этом был выпущен и выглядел полным: он агрегирует то, что
|
||
ему подали, и о непоступивших проходах не знает. Секция границ покрытия
|
||
обязана была это назвать, но она заполняется тем же триажем — то есть
|
||
единственный, кто мог заметить пропуск, узнаёт о нём из того же источника,
|
||
который его допустил.
|
||
- **Почему не поймали:** пропуск прохода **не отличим от прохода без находок**.
|
||
Гейт зелёный, спеки сошлись, applicative-проходы отработали — снаружи это
|
||
выглядит как чистое ревью. Все семь находок принадлежат ровно тем классам,
|
||
которые applicative-проходы не достают по построению: враждебно
|
||
сконструированный вход (`adversary`), поведение под откатом и конкуренцией
|
||
(`ops`), второй способ делать уже сделанное (архитектура). Recall чек-листа
|
||
равен длине чек-листа, а этих пунктов в чек-листах нет и быть не может.
|
||
- **Что меняем:** отчёт ревью обязан перечислять запущенные проходы **поимённо
|
||
и с исходом**, а оркестратор задачи — сверять этот перечень с составом
|
||
профиля до того, как коммитить; непущенный проход идёт в границы покрытия
|
||
строкой «не запускался», а не отсутствует. Правилом линтера это не
|
||
выражается, автоматической проверки нет — но пропуск, названный в отчёте,
|
||
стоит одной строки, а пропуск молчащий стоил семи находок и отдельной задачи
|
||
на их дозакрытие. Состав проходов и профилей при этом не трогаем: они
|
||
сработали ровно так, как задуманы, — их просто не позвали.
|
||
|
||
## 2026-08-02 — тест на утечку значений в лог краснел от хода часов [проскочил]
|
||
|
||
- **Где:** `internal/fold/log_test.go`, `TestFoldНесравнимыеНаборыДаютWarn`
|
||
- **Симптом:** гейт задачи про цену читающего маршрута покраснел на чужом
|
||
тесте: «в логе оказалось значение точки "5.1"». Значения в логе не было —
|
||
подстрока нашлась в метке времени записи (`…T20:23:35.193…` содержит `5.1`).
|
||
Повторный прогон зелёный.
|
||
- **Причина:** утверждение искало секрет в **сыром буфере** записи, а буфер
|
||
содержит служебное поле `time` с долями секунды. Вероятность совпадения для
|
||
двухсимвольного числа с точкой — около процента на прогон, то есть тест
|
||
флаки по построению, и краснеет он у того, кто мимо проходил.
|
||
- **Почему не поймали:** шаг `flaky` гейта гоняет набор дважды подряд —
|
||
вероятность поймать однопроцентную флаки за два прогона мала, а сам тест
|
||
выглядит образцовым: он проверяет ровно тот инвариант, который проекту
|
||
дороже всего («данные о здоровье чувствительнее токенов»). Ни один проход
|
||
ревью не смотрит на тесты чужих задач.
|
||
- **Что меняем:** правило в [conventions/testing.md](conventions/testing.md) — проверка «в логе
|
||
нет значения» разбирает запись и выбрасывает `time`, а не ищет в сыром
|
||
буфере. Гейт не трогаем: два прогона против однопроцентной флаки не помогут,
|
||
а десять стоили бы дороже самой находки.
|
||
|
||
## 2026-08-02 — состав конвейера сужен: 11 проходов до 6–9
|
||
|
||
Не промах, а решение по итогам пяти задач подряд. Записано здесь, потому что
|
||
именно здесь лежит цена непоймания: если что-то теперь проскочит, первый вопрос
|
||
будет «не тот ли это класс, который мы перестали проверять».
|
||
|
||
- **Повод:** профиль `deep` стоял на всех пяти задачах сессии и гонял 11
|
||
проходов на коде плюс 4 на дизайне — порядка полутора миллионов токенов на
|
||
задачу. Ревью, а не написание кода, стало основной статьёй расхода.
|
||
- **На чём основано:** поимённая атрибуция находок надёжна только для
|
||
дозапуска трёх проходов на `f8200f7` — там оркестратор запускал их сам.
|
||
В двух циклах, которые вели сабагенты, находки перечислены без указания
|
||
прохода, и это ограничение вывода названо здесь честно.
|
||
- **Что убрано и почему:**
|
||
- `negative` — **удалён**. За сессию ни одной именной находки; блокер про
|
||
откат релиза он нашёл дублем с `ops`, то есть заплатил триажу
|
||
дедупликацией. Два его живых вопроса переселены: «хватит ли сигналов
|
||
владельцу, когда поток оборвётся ночью» — в `ops`, вопрос 7; «что опытный
|
||
человек отсюда удалил бы» — в `architecture`, вопрос 5.
|
||
- `rubric` — **только в `design`**. Его же 14 свойств из design-прогона
|
||
ложатся приёмочными критериями в `tasks.md`; судить код по критерию, под
|
||
который он писался, — корреляция по построению.
|
||
- `reimpl` — **по триггеру** «новое правило слияния, идентичности или
|
||
разбора». Самый дорогой проход конвейера; единственный раз, когда триаж
|
||
назвал его отсутствие дырой покрытия, — это была задача с новым правилом
|
||
слияния сущностей, то есть ровно триггерный случай.
|
||
- **Что переставлено, и это важнее сокращения:** `adversary` и `ops` были в
|
||
`deep`-только, а `standard` гонял четыре самых слабых generative-прохода.
|
||
То есть профиль, которым закрывается большинство задач, запускал ровно тех,
|
||
кто ничего не принёс, и не запускал тех, кто принёс почти всё. Оба переехали
|
||
в `standard`. Это одновременно дешевле и качественнее.
|
||
- **Что чуть не убрали по ошибке:** `idiom` был в списке на удаление как
|
||
«вкусовщина». Отменено фактом: в задаче про цену читающего маршрута он нашёл,
|
||
что `-1 >= -1` читается как «журнал разобран целиком», и **воспроизвёл** —
|
||
1492 тика из 5502. Плюс три эксперимента на дизайне `razbor-metrik-v-obekty`.
|
||
Вывод, который стоит помнить: этот проход зарабатывает **экспериментами
|
||
против поведения stdlib и драйвера**, а не цитатами из гайдов, — и потому у
|
||
него есть внешний оракул. Оценка «не всплыл поимённо ни разу» была верна по
|
||
имевшимся данным и неверна по существу.
|
||
- **Что мы сознательно перестали проверять:** класс «чего нет в зрелой
|
||
реализации такого узла» вне профиля `design`, и «пять вопросов второго
|
||
инженера» как отдельная постановка. Обратимость этого класса высокая: он
|
||
портит форму кода и полноту наблюдаемости, а не данные. Если проскочит
|
||
дефект этого класса — запись сюда и пересмотр решения.
|
||
- **Побочная выгода, ради которой стоило резать отдельно:** реестр из 6–9
|
||
проходов сверяется взглядом. Промах 2026-08-02 (запись выше) был молчащим
|
||
пропуском трёх проходов из одиннадцати; на коротком списке требование
|
||
«перечисли запущенные проходы поимённо и с исходом» наконец выполнимо.
|
||
|
||
## 2026-08-02 — `idiom` тоже упразднён, класс переселён
|
||
|
||
Решение владельца, принятое после того, как оркестратор привёл доводы против
|
||
удаления (находка на чекпойнте WAL, воспроизведённая: 1492 тика из 5502) и они
|
||
были выслушаны. Записано отдельной строкой, потому что довод был, и если класс
|
||
проскочит — искать надо здесь.
|
||
|
||
- **Что переселено, а не выброшено.** Проход зарабатывал экспериментами против
|
||
поведения stdlib и драйвера, и именно эта способность перенесена поимённо:
|
||
- «поведение библиотеки, драйвера и `PRAGMA` измеряется, а не вычитывается из
|
||
документации; что возвращается в **вырожденном** случае и отличим ли этот
|
||
ответ от штатного» — в `ops`, обязательный вопрос 8, вместе с прецедентом
|
||
`-1 >= -1` и оговоркой про `data_version` как свойство соединения;
|
||
- «не изобретаем ли то, что уже есть в библиотеке» — в `architecture`,
|
||
вопрос 1, с перечнем конструкций stdlib: своя абстракция, повторяющая форму
|
||
существующей, — находка того же класса, что и второй способ делать одно и
|
||
то же.
|
||
- **Что действительно потеряно.** Поимённая сверка с положениями Effective Go,
|
||
Go Code Review Comments, Go Proverbs и стайлгайдов Uber и Google. Различение
|
||
«идиоматично» против «распространено» больше не задаётся никем: `architecture`
|
||
спрашивает про форму решения, `ops` — про поведение под нагрузкой, но ни один
|
||
не спросит «в Go так не пишут». Класс обратимый — портит форму кода, не
|
||
данные, — но он теперь не покрыт вовсе, и это надо признавать в границах
|
||
покрытия, а не считать проверенным.
|
||
- **Итог по конвейеру:** `quick` 4, `standard` 6, `deep` 7–8, `design` 3.
|
||
Было 11 на коде и 4 на дизайне.
|
||
|
||
## 2026-08-03 — метка от часов в отпечатке сделала тест функцией секунды прогона [пойман]
|
||
|
||
- **Где:** `internal/fold/categorical_test.go`, `TestFoldЛокальНеМеняетСостояния`
|
||
- **Симптом:** гейт покраснел на одном подтесте из четырёх: «заголовок
|
||
`{"Accept-Language":["de"]}` сдвинул отпечаток витрины». Три подтеста прошли.
|
||
- **Причина:** тест сравнивал отпечатки четырёх независимых витрин, а метку
|
||
приёма доставки брал из `store.Now()`. Провенанс первой встречи входит в
|
||
отпечаток реестра — значит отпечаток зависел от того, уложились ли подтесты в
|
||
одну секунду. Тест был флаки по построению и краснел бы у того, кто мимо
|
||
проходил.
|
||
- **Чем воспроизведён:** сам гейт; после замены `store.Now()` на фиксированную
|
||
метку — `go test ./internal/fold -count=2` зелёный.
|
||
- **Что меняем:** ничего в конвейере — гейт сработал ровно так, как задуман, и
|
||
поймал класс, который прошлый раз (2026-08-02, подстрока «5.1» в метке
|
||
времени) прожил незамеченным. Правило то же и уже записано в
|
||
[conventions/testing.md](conventions/testing.md): величина, зависящая от хода
|
||
часов, не участвует в утверждении. Запись здесь — потому что это второй случай
|
||
одного класса за два дня, и третий стоит считать сигналом, а не совпадением.
|
||
|
||
## 2026-08-03 — прогон живого архива красный на master, и это не заметили две задачи подряд [проскочил]
|
||
|
||
- **Где:** `internal/replay/archive_test.go`, `measureStyles`
|
||
- **Симптом:** `task verify:archive` в задаче про словарь категориальных
|
||
значений упал на `step_count: противоречащих часов 1 при 22 согласных`.
|
||
Проверено прогоном **базовой ревизии** `3df42af` из копии дерева на том же
|
||
архиве: те же 2875 объектов, те же 285 координат сна, тот же отказ. Краснота
|
||
унаследована, изменением не внесена.
|
||
- **Причина:** утверждение «противоречий ноль» — посылка «род измерим», верная
|
||
на корпусе, где её снимали. Корпус вырос до 145 доставок, и у `step_count`
|
||
появился час, где минутный и часовой слои разошлись. Сама система при этом
|
||
ведёт себя правильно: род объявляется только при единогласном свидетельстве,
|
||
и `step_count` числится `unknown`.
|
||
- **Почему не поймали:** ровно та же причина, что и в записи 2026-08-02, — у
|
||
проверки, которую гейт не гоняет, краснота никому не видна. Разница в том, что
|
||
тогда протухла константа, а теперь под вопросом сама посылка: противоречие —
|
||
это либо дефект правила, либо законное свойство корпуса, и решать это не
|
||
прогону.
|
||
- **Что меняем:** конвейер — ничего. Решение о том, чем стал `step_count`
|
||
(дефект измерения рода или законное противоречие, которое надо печатать, а не
|
||
утверждать), принадлежит владельцу и заведено задачей отдельно от этого
|
||
изменения. Названо здесь, чтобы третья задача подряд не открывала его заново.
|
||
|
||
## 2026-08-03 — ответ владельца не превращал задачу в берущуюся [проскочил]
|
||
|
||
- **Где:** конвейер, а не код — учёт задач, шаг «ответ на вопрос»
|
||
- **Симптом:** первая сессия по `av-dev-pm:session` показала четыре задачи с
|
||
тегом `question`. Три из них были решены владельцем **2026-08-02**, и решение
|
||
лежало первым абзацем тела: тай-брейк — вариант (б), порядок журнала —
|
||
вариант (в) после `/stats`, откат релиза — вариант (2). Но раздел «Вопросы»
|
||
остался непустым, тег остался на месте, и `sprint take` отказал бы взять эти
|
||
задачи в набор.
|
||
- **Причина:** ответ на вопрос — это **три правки** (опустошить раздел, снять
|
||
тег, переписать «зачем»), и делаются они в момент ответа. Была сделана только
|
||
запись решения. Судит при этом раздел, а не тег, поэтому решённая задача
|
||
выглядела нерешённой ровно так же, как настоящая нерешённая.
|
||
- **Чем воспроизведён:** `tasks.py list --questions` — 4 записи, из них 3 с
|
||
датированным решением в теле. После правок — 0.
|
||
- **Что изменено:** ничего в коде; три задачи приведены в берущийся вид,
|
||
четвёртая (`entity-without-parsed-label`) решена на этой сессии.
|
||
|
||
Два числа этой же сессии, названные, чтобы их было с чем сравнивать:
|
||
|
||
- **Ориентир «5–8 задач в спринте» ничем не замерян** — он взят из умолчания
|
||
скилла. Первый собственный замер даст этот спринт, и пересматривать ориентир
|
||
надо на следующей сессии, а не «когда-нибудь».
|
||
- **Отбор порции по залежалости (`list --stale`) в этом цикле слеп:** все 49
|
||
файлов каталога получили одну дату при переезде на канон (коммит `d79189b`),
|
||
и храповик на давно неподвижных задачах включится только с накоплением
|
||
собственной истории правок. Порция этой сессии отобрана по цели.
|
||
|
||
## 2026-08-04 — оракул `verify:archive` покраснел от роста корпуса второй раз за два дня [проскочил]
|
||
|
||
- **Где:** `internal/store/bucket.go`, `pointLess` — тай-брейк равной полноты
|
||
- **Симптом:** `task verify:archive` красный на `master` без единого коммита:
|
||
`step_count: противоречащих часов 1 при 22 согласных`. Разбор довёл до
|
||
причины: на час `2026-08-03T07:00Z` приехало четыре точки с двумя значениями,
|
||
победило меньшее — оно же приехавшее первым, — потому что его каноническая
|
||
форма сортируется раньше. Сверка слоёв объявила метрику мгновенной против 22
|
||
согласных часов, и `step_count` ушёл в `unknown`.
|
||
- **Причина:** байтовый тай-брейк выбран как «детерминированный и ни на что не
|
||
опирающийся», и это было верно. Неверной оказалась оценка его области:
|
||
считалось, что он крайний разряд после полноты. Перемер (находка 54) на
|
||
настоящем ключе: полнота решает 1,2% спорных координат, тай-брейк — 98,8%.
|
||
То есть «выигрывает более полная точка» — не главное правило слияния, а
|
||
редкий частный случай, и главным всё это время был лексикографический
|
||
порядок JSON.
|
||
- **Чем воспроизведён:** `task verify:archive` до и после. До — FAIL,
|
||
`step_count unknown`, отпечаток `bf36b477…`; после — PASS, `step_count
|
||
cumulative`, отпечаток `03aace91…`, ноль противоречащих часов, и заодно
|
||
`headphone_audio_exposure` вернулся из `unknown` в `instant`.
|
||
- **Почему не поймали:** та же причина, что 2026-08-02 и 2026-08-03, третий раз
|
||
подряд. Прогон живого архива в гейт не входит, значит его краснота видна
|
||
только следующей задаче, которая до него дотянется. Но добавилось новое:
|
||
здесь протухла не константа, а **оценка области действия правила**, снятая на
|
||
корпусе, где спорных координат было 2 897. Ни один проход ревью не
|
||
перепроверяет числа, на которых стоит нормативный текст спеки, — они читаются
|
||
как факт. Поймал это проход `specs` на профиле `design`: он сверил число в
|
||
дельте с находкой 49, увидел расхождение в 29 раз и потребовал назвать метод.
|
||
Метод оказался неверным (ключ без слоя), число — завышенным, а соотношение —
|
||
верным.
|
||
- **Что меняем:** ничего в составе конвейера — он сработал. Два правила
|
||
промоутятся в конвенции (см. `conventions/testing.md`): «в проверке на живом
|
||
корпусе утверждается инвариант, число печатается» — оно было записано здесь
|
||
2026-08-02 со словами «годится в конвенции» и не доехало, после чего класс
|
||
повторился дважды; и «оракул сходимости называет свою посылку рядом с собой».
|
||
Третий случай одного класса за три дня — это уже не совпадение, и в
|
||
`docs/conventions/testing.md` он теперь правило, а не запись в журнале.
|
||
|
||
## 2026-08-04 — гейт после интеграции пропустил все go-шаги и объявил себя зелёным [пойман]
|
||
|
||
- **Где:** конвейер, а не код — `Taskfile.yml`, шаг `gate`, и правило батча
|
||
«после каждой интеграции — гейт на основной ветке»
|
||
- **Симптом:** после `git merge --ff-only` ветки задачи `task gate` без
|
||
аргументов напечатал «код не менялся — go-шаги пропускаются» и вышел с нулём.
|
||
Сборка, тесты, гонки, покрытие диффа и миграции **не гонялись вовсе**, а исход
|
||
выглядел как зелёный прогон.
|
||
- **Причина:** база диффа по умолчанию — `git merge-base HEAD master`. На самой
|
||
ветке `master` после ff-слияния это сам `HEAD`, дифф пуст, и все шаги,
|
||
привязанные к изменённым файлам, честно пропускаются. Пропуск по пустому
|
||
диффу — правильное поведение шага; неправильно то, что **правило интеграции
|
||
на него опирается**: батч вливает ветку и проверяет результат прогоном,
|
||
который в этот момент проверить ничего не может.
|
||
- **Чем воспроизведён:** `task gate` — 0, все go-шаги SKIP. `task gate
|
||
BASE=<коммит до слияния>` на том же дереве — 45 изменённых файлов, 13 шагов,
|
||
и **красный** `lint`.
|
||
- **Что изменено:** `.golangci.yml` — `./tmp` исключён из проверок
|
||
(`9f77e56`): `CLAUDE.md` велит держать черновое в `./tmp`, а линтер про это не
|
||
знал, и туда попадали и worktree батча, и диагностические программы. Краснота
|
||
по причине, не связанной с изменением, приучает не читать красноту.
|
||
- **Что осталось незакрытым:** гейт после интеграции обязан звать `BASE`
|
||
вершиной **до** слияния. Сейчас это знание живёт только в этой записи —
|
||
ни `Taskfile.yml`, ни скилл батча его не несут.
|
||
|
||
## 2026-08-04 — событие о новой секции терялось на отказе слияния [пойман]
|
||
|
||
- **Где:** `internal/fold/fold.go`, ветвь отказа `store.Merge` в change
|
||
`2026-08-04-aktivnaya-proverka-novyh-sekcij`
|
||
- **Симптом:** доставка, принёсшая имя секции впервые, при нетранзиентном отказе
|
||
слияния писала имя в `delivery.uncovered_sections`, но запись об отказе его не
|
||
называла. Следующая доставка считала имя виденным — событие, однократное за
|
||
всю жизнь имени, пропадало **навсегда**, то есть ровно то, ради чего задача и
|
||
делалась.
|
||
- **Причина:** ветвей записи исхода в свёртке четыре, а дизайн рассмотрел одну.
|
||
Признак новизны считался до ветвления и корректно доезжал до `residueOf`
|
||
(отказ разбора), но ветвь отказа слияния собирала остаток **вручную** и поле
|
||
новизны в него не клала. Дельта-спека говорила «до ветвления на успех и
|
||
отказ», подразумевая один отказ.
|
||
- **Чем воспроизведён:** свёртка доставки с новой секцией при снесённой таблице
|
||
`bucket` — запись `ERROR` без `uncovered_new`, а `SectionsSeenBefore` на
|
||
следующей доставке уже отвечает «виденное». Тест закреплён:
|
||
`TestFoldОтказСлиянияНазываетНовуюСекцию`.
|
||
- **Чем пойман:** тремя проходами независимо (`specs`, `code`, `adversary`),
|
||
причём двое написали падающий тест. Дешёвый `code`-проход нашёл его наравне с
|
||
дорогими — признак того, что дефект был в форме «ветвь собрана руками рядом с
|
||
ветвью, собранной функцией», а такое видно чтением.
|
||
- **Что изменено:** новизна передаётся и в эту ветвь; дельта-спека переписана в
|
||
терминах «каждый исход, который пишет список в учётную запись», и отдельно
|
||
названы исходы, которые список очищают (нечитаемое тело, паника) и потому
|
||
события не теряют.
|
||
|
||
## 2026-08-04 — замер стоимости снят на корпусе, где измеряемого случая не бывает [пойман]
|
||
|
||
- **Где:** `design.md` того же change, решение 3; утверждение «в режиме
|
||
постоянного приезда секции сверка стоит 18 мкс на доставку»
|
||
- **Симптом:** на числе стояло решение «частичный индекс не нужен». Число
|
||
описывало **не тот** режим.
|
||
- **Причина:** синтетический журнал наполнялся так, что новая секция была во
|
||
**всех** доставках, то есть её первая встреча лежала в самом начале журнала —
|
||
и `LIMIT 1` выходил рано. В жизни секцию включают на телефоне сегодня: первая
|
||
встреча оказывается в хвосте, и проход идёт почти по всему журналу на каждой
|
||
доставке. Разница — три порядка (31 мкс против 52 мс).
|
||
- **Чем воспроизведён:** `tmp/seenmeasure` с хвостовым именем: голова 31 мкс,
|
||
хвост 52 мс, отсутствующее имя 50 мс.
|
||
- **Чем пойман:** `adversary` — он не поверил числу и построил корпус, в котором
|
||
измеряемый случай выглядит как в жизни. Это третий случай за три дня, когда
|
||
оценка оказалась функцией того, **как устроен корпус**, а не того, что
|
||
измеряют (записи 2026-08-02, 2026-08-04 про `verify:archive`).
|
||
- **Что изменено:** замер перемерян тремя случаями (голова, хвост, отсутствие),
|
||
числа сведены в одно место (`design.md`), код и `architecture.md` формулируют
|
||
правило и ссылаются на источник. Развилка «принять цену или завести индекс»
|
||
вынесена владельцу.
|
||
- **Что осталось незакрытым:** правило «число замера обязано нести метод и
|
||
описывать тот случай, ради которого снято» действует только для тестов
|
||
(`conventions/testing.md`). На `design.md` оно теперь распространено записью
|
||
ниже, но механизировать его нечем.
|
||
|
||
## 2026-08-04 — гейт дважды покраснел от чужого мусора: кеш линтера и черновик в `./tmp` [пойман]
|
||
|
||
- **Где:** конвейер, а не код — `scripts/gate.py`, шаги `lint` и `test`
|
||
- **Симптом:** в задаче про форму провода `task gate` дал `FAIL lint` с
|
||
сообщением `../../internal/store/store.go:260: use of time.Now forbidden` —
|
||
путь ведёт в **главный репозиторий**, а прогон шёл в worktree задачи. Позже, в
|
||
том же прогоне задачи, `FAIL test` на
|
||
`TestОднаМеткаИзТелаУбиваетМаршрутКаталога` — тесте, которого в задаче нет
|
||
вовсе.
|
||
- **Причина:** два разных механизма, один класс — в гейт затекает то, что к
|
||
изменению отношения не имеет.
|
||
- `golangci-lint` ходит в **общий на машину** `~/.cache/golangci-lint`, а
|
||
конвейер задач работает в нескольких worktree одного модуля (`tmp/wt-*`).
|
||
Кеш отдаёт замечания, привязанные к путям чужого дерева, и правило-исключение
|
||
`^internal/(ident|store)/` на путь вида `../…` не распространяется.
|
||
- `go test ./...` не знает про `./tmp`: `.golangci.yml` каталог исключает
|
||
(коммит `9f77e56`), а `go test` — нет. Проход `adversary` оставил там свой
|
||
падающий тест-оракул, и он стал частью набора.
|
||
- **Чем воспроизведён:** первое — независимо проходом `review-gate`: временный
|
||
worktree базовой ревизии, `golangci-lint run ./...` без очистки кеша даёт
|
||
замечание с путём **другого** дерева; после `golangci-lint cache clean` на той
|
||
же ревизии — `0 issues`. Второе — `tmp/gate/test.log`: `FAIL` в пакете
|
||
`git.vakhrushev.me/av/healthlog/tmp/adv/oracle`.
|
||
- **Чем пойман:** обоими случаями — самим гейтом, но **ценой разбора**: краснота
|
||
выглядела как дефект изменения, и каждый раз пришлось доказывать, что это не
|
||
он. Ровно та цена, что названа записью 2026-08-04 выше: «краснота по причине,
|
||
не связанной с изменением, приучает не читать красноту».
|
||
- **Что изменено:** шаг `lint` получил свой кеш —
|
||
`GOLANGCI_LINT_CACHE=tmp/gate/golangci`, — то есть прогон стал герметичным по
|
||
дереву. Цена названа и замерена проходом `ops`: N деревьев × 10–15 МиБ вместо
|
||
одного общего кеша, штатный трим go-build-формата у него есть.
|
||
- **Что осталось незакрытым:** `go test ./...` по-прежнему видит черновые
|
||
go-пакеты в `./tmp`. Убирать за собой обязан тот, кто их создал (в этот раз —
|
||
проход ревью), и механизма против забывчивости нет. Дешёвый кандидат, если
|
||
класс повторится: `go test` по явному списку `./cmd/... ./internal/...` вместо
|
||
`./...`. Не сделано намеренно — один случай не отличим от случайности, а
|
||
правило, введённое по одному случаю, потом никто не помнит зачем.
|