- роадмап отвечает «что умеет и чего не умеет»: PLAN.md → ROADMAP.md, четыре канонические секции, достигнутые звенья строками в «Готово», цели переформулированы возможностями приложения - задачи: род работы и «Затрагивает» набору спринта, 34 заголовка в форму действия, «Завершение» целей перечнями со ссылкой из каждой задачи - вычитка проходами task-form и doc-wording, починены протухшие факты в README, паспорте и review.md
63 KiB
Ревью: настройка и журнал
Конвейер — скилл 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разбираемой доставки. - Правило выбора между версиями — функция множества версий либо явно функция порядка журнала; третьего состояния нет.
- Столкновение разрешается полнотой, а при равной полноте — положением в
журнале: побеждает стоящая позже
(ADR). Изменение
запечатанного часа пишется
WARN, но данные пишутся. - Транзакция не держит блокировку дольше
busy_timeout: канонизация и сжатие — вне её.
Файловый архив и ретеншен (internal/archive)
- Путь строится из значений, которых отправитель не контролирует.
- Удаление тела опирается на колонку, отличающую ноль от «не измерялось».
- Место на диске и рост каталога названы числом.
Проигрыватель журнала и CLI (internal/replay, cmd/)
- Повторный прогон даёт то же состояние и тот же отпечаток.
- Новая единица хранения входит в отпечаток и в счётчики отчёта.
- Расход памяти не растёт вместе с длиной журнала.
- Подмена базы — решение человека при остановленном сервисе, не команды.
Обработчик чтения и адаптер MCP (internal/httpapi: каталог и точки
написаны; свёртка по сетке, тренировки, записи и 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, чекпоинт кода прошёл без трёх проходов).specs: считается ли внешним поведением состояние, которое даёт пересборка — витрина наблюдаема через пересборку, поэтому расхождение с журналом не внутренняя деталь, а поведение, которого спека не заказывала. Внешнее здесь — ещё и код ответа приёма, форма ответа чтения и содержимое архива (переселено из триггеров профиля, канон 3).
Триггеры профиля
Уточняет умолчания конвейера, не отменяет их. Рабочее умолчание — standard:
миграция схемы, публичный контракт и инвариант ступень не поднимают, их
проверяют проходы, которые в standard и так есть.
- Новое понятие или структурная единица (
wide) — новый пакет вinternal/, новый род узла из перечня выше, новый тип провода вinternal/httpapi, новая единица хранения, входящая в отпечаток, новый транспорт рядом с HTTP. - Правила идентичности, слияния и разбора (
deep) живут в трёх местах:internal/hae— разбор пакета и вывод слоя;internal/fold— выбор между версиями точки;internal/store— координатный ключ и запись часового объекта. Правку правила в любом из них ступень поднимает; перенос кода без правки правила — нет. quick— правка документов, конфигурации, сообщений; ничего, что меняет хранимое.
reimpl живёт за барьером deep и по тому же триггеру — новое правило слияния,
идентичности или разбора. Замеры окупаемости: единственный раз, когда триаж
назвал его отсутствие дырой покрытия, — задача с новым правилом слияния
сущностей. Второй замер (2026-08-03, словарь категориальных значений): триггер
сработал на новом правиле разбора и ключе реестра, проход окупился — он
независимо подтвердил замером две находки, до того имевшие только одно измерение
(пик памяти накопителя: 1002 МиБ против 780 на базе; единицы счётчика
отброшенных), и отдельно назвал семь мест, где существующее решение оказалось
лучше его собственного. Второе ценно не меньше первого: оно показывает, где
проход соглашается, а не только где спорит.
Недоступно проверке
Не проверит ни один проход. Реальный профиль нагрузки: телефон шлёт молча и
непрерывно, объём и частота меряются только по факту. Поведение приложения HAE
за пределами наблюдённого — расписание автоматизаций пожелание, а не гарантия
(разведка, находка 28). Полнота словаря переводов после обновления iOS.
Секции, которых поток ещё не приносил: symptoms, ecg,
heartRateNotifications, cycleTracking, medications — разбор писался
вслепую, и проход может судить только форму кода, не соответствие реальности.
Перестали проверять сознательно.
- Шаг покрытия диффа гейт не красит.
CLAUDE.mdобъявляет, что непокрытая изменённая строка красит гейт безусловно;scripts/diff-coverage.pyвсегда возвращает0, и шаг печатаетOKпри любом покрытии. То есть «гейт зелёный» не означает «покрытие диффа полное», и разбор непокрытых строк остаётся человеку или проходу. Найдено проходомgate2026-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.
Реализованные задачи, находки ревью и решения сюда не пишутся: у них есть коммит, спека и задача. Здесь только промахи конвейера и решения о его составе.
Форма:
ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман]
- Где: путь:строка либо «конвейер, а не код»
- Симптом: как обнаружилось, кем и когда
- Причина: что на самом деле было не так
- Чем воспроизведён: тест, команда, замер — с числами
- Почему не поймали: только для проскочивших — какой проход обязан был найти и что ему помешало
- Что меняем: правило прохода, шаг гейта, конвенция, факт в документе проекта — либо «ничего, цена поимки выше цены дефекта»
Последний пункт важнее остальных. Вывод «ничего не меняем» — законный исход: не всякий дефект стоит того, чтобы усложнять ради него ревью каждой задачи.
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 — проверка «в логе
нет значения» разбирает запись и выбрасывает
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 так не пишут». Класс обратимый — портит форму кода, не данные, — но он теперь не покрыт вовсе, и это надо признавать в границах покрытия, а не считать проверенным. - Итог по конвейеру:
quick4,standard6,deep7–8,design3. Было 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: величина, зависящая от хода часов, не участвует в утверждении. Запись здесь — потому что это второй случай одного класса за два дня, и третий стоит считать сигналом, а не совпадением.
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в change2026-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/...вместо./.... Не сделано намеренно — один случай не отличим от случайности, а правило, введённое по одному случаю, потом никто не помнит зачем.