- профиль отвечает «какие проходы», режим — «как их запускать» - явная просьба владельца — достаточное основание, без обоснований и переспрашивания; перекрывает эвристики в обе стороны - главный собственный повод — ожидаемые замеры: adversary и ops меряют одно железо (блокировка SQLite, куча, рост -wal) и портят числа друг другу, а находка с испорченным оракулом дороже сэкономленных минут - декорреляция не меняется ни в каком режиме: проход не видит чужих находок, «последовательно» ≠ «читает предыдущего» - ранний выход только при переделке формы изменения, с перезапуском с нулевой стадии; триаж — только на полном прогоне
34 KiB
name, description
| name | description |
|---|---|
| healthlog-review-pipeline | Конвейер ревью изменений healthlog — детерминированный гейт, сверка с дельта-спеками OpenSpec в обе стороны, враждебные постановки и эксплуатационный постмортем, независимая реализация по триггеру, архитектура и обязательный триаж. Проходы гонятся параллельно или последовательно (второе — когда ожидаются замеры или занята машина). Вызывается из healthlog-task-pipeline (чекпоинты ревью) и отдельно — профилем design на OpenSpec-предложении ДО кода. |
Конвейер ревью (healthlog)
Готовит ревью — не заменяет его. Потребитель отчёта — оркестратор, который чинит код; человек читает только сводку, развилки и границы покрытия.
Три правила, из которых всё следует
Если ситуация не покрыта инструкцией — решай по ним.
- Recall чек-листа равен длине чек-листа. Проход, устроенный как «проверь пункты 1..N», найдёт ровно перечисленное. Всё неявное — идиомы, форма решения, «так не делают» — неперечислимо по определению: перечислимое уже стало бы конвенцией. Отсюда деление проходов на applicative (применяют заданный критерий) и generative (сперва порождают критерий или альтернативу, потом сравнивают). Расширять чек-листы бесполезно; неявный слой достают только generative-проходы.
- Ценность верификатора = наличие внешнего оракула × декорреляция с автором, а не число ролей. Под всеми ролями одна модель с одними априорными, вход у всех общий: седьмая роль почти не добавляет recall, но линейно удорожает триаж. Иерархия надёжности: детерминированный инструмент > агент, который его запускает и интерпретирует вывод > агент с чистым мнением. Максимум работы переносим вниз.
- Отчёт без границ покрытия хуже отсутствия отчёта. «Критичных проблем не обнаружено» потребляет ощущение проверенности, ничего не гарантируя. Секция границ покрытия обязательна и не сокращается — в том числе в докладе человеку.
Что этот конвейер защищает в healthlog
Инварианты, нарушение которых — по умолчанию critical (подробно —
CLAUDE.md, docs/architecture.md):
- Точка хранится дословно. Хранилище — свёртка по журналу
(
import(экспорт) + replay(доставки)), поэтому разобранное пересобираемо, а вот не принятое — нет: доставка мимо архива теряется навсегда. - Идентичность по координатам (
метрика + слой + метка).sourceв ключ не входит. Неверное правило слияния портит историю молча — заметить это можно только сверкой с родным экспортом Apple, то есть месяцами позже. - Агрегации при записи нет. Свёртка живёт только в ответе и только с измеренным родом метрики. Нижний слой HAE не суммируется никогда.
- Данные о здоровье чувствительнее токенов. Тело запроса в логе на уровне
выше
DEBUG, файл выгрузки под контролем версий — это утечка, а не неаккуратность. - Приём не теряет доставку. Код ответа отражает доставку, а не разбор; тело ложится на диск до разбора.
Модель по проходу
Следует из правила 2: чем больше работы делает детерминированный инструмент, тем дешевле может быть модель; чем больше проход порождает критерий, тем дороже. Модель задана во frontmatter каждого агента, менять её здесь не нужно.
| Модель | Проходы | Почему |
|---|---|---|
sonnet |
gate, code, ops | вход структурный, критерий записан заранее |
opus |
specs, adversary, rubric, reimpl | суждение без опоры на инструмент |
fable |
triage, architecture | ошибка распространяется дальше самой находки |
Fable — только двум проходам, и это калибровка, а не осторожность. Первый
прогон конвейера (ревью дизайна razbor-metrik-v-obekty) показал, что самые
ценные находки дали opus-проходы: specs дал 13 находок с оракулами, а
упразднённый впоследствии idiom — три эксперимента против драйвера
(SQLITE_BUSY_SNAPSHOT 517 против _txlock=immediate, куча map[string]any
против json.RawMessage, потери json.Marshal без UseNumber). Разницы в
пользу более дорогой модели на опиниативных проходах не обнаружилось — значит
платить за неё там не за что.
Двое, у кого fable остаётся, отобраны по одному признаку: их ошибка распространяется дальше собственной находки.
triage— через него проходит всё, что оркестратор реализует молча: ложноположительная находка становится кодом, потерянныйcritical— дефектом. Ошибка триажа дороже ошибки любого отдельного прохода.architecture— запускается редко (толькоdeepиdesign), потолок в 3 находки делает его дешёвым по выходу, а находка на предложении стоит абзаца против переписывания на готовом коде. Дёшево × высокое плечо.
reimpl намеренно не в этом списке, хотя он самый ценный из generative:
его стоимость определяется объёмом вывода (он пишет реализацию целиком), так
что дорогая модель множит самый большой счёт. Ценность же его — в
независимости взгляда, а не в мощности модели.
Haiku не используется ни на одном проходе, и это не экономия наоборот.
Дешёвая модель на опиниативном проходе даёт правдоподобные находки, которые
триаж обязан опровергать оракулом, — а это самая дорогая операция конвейера.
Механизируемая же работа здесь давно вынесена ниже модели: gate.py,
diff-coverage.py, review-context.py, backlog.py стоят ноль токенов.
Дешёвому проходу просто не осталось работы.
Сюда же — почему triage на самой сильной модели, хотя он «всего лишь
агрегирует». Через него проходит всё, что оркестратор потом реализует
молча: ложноположительная находка становится кодом, потерянный critical —
дефектом. Ошибка триажа дороже ошибки любого отдельного прохода.
Экономия при этом достигается не понижением модели, а непуском прохода:
quick — четыре прохода, deep — семь. Правило выбора профиля ниже и есть
главный рычаг стоимости.
Профили
| Профиль | Когда | Стадии | Проходов |
|---|---|---|---|
quick |
багфикс, локальная правка, доки | 0, 1, 5 | 4 |
standard |
новая функциональность в существующем пакете | 0, 1, 2, 5 | 6 |
deep |
новый пакет, изменение публичного контракта, миграция БД, трогает инварианты выше | 0, 1, 2, 3, 4, 5 | 7–8 |
design |
до кода, на OpenSpec-предложении | specs + rubric + architecture (см. ниже) | 3 |
Состав сверяется по этой таблице до коммита. Реестр из трёх-восьми
пунктов проверяется взглядом — и это единственная защита от промаха, который
уже случился: пропуск прохода не отличим от прохода без находок (гейт
зелёный, спеки сошлись, отчёт выглядит полным), а заметить его мог бы только
триаж, который сам заполняется тем, что ему подали. Отчёт обязан перечислять
запущенные проходы поимённо и с исходом; непущенный идёт строкой «не
запускался» в границы покрытия, а не отсутствует. Цена молчащего пропуска
измерена: семь находок и отдельная задача на их дозакрытие
(docs/review-journal.md, 2026-08-02).
Правило выбора профиля — по факту изменения, не по ощущению важности:
- есть миграция в
internal/store/migrations/, новый пакетinternal/*, изменение контракта Read API или MCP, трогается правило слияния точек или вывод слоя →deep; - иначе меняется поведение, видимое снаружи (эндпоинт, форма ответа, код
ответа приёма, формат лога) →
standard; - иначе →
quick.
Профиль объявляется в отчёте. Понижение профиля — решение оркестратора, и оно попадает в границы покрытия строкой «профиль понижен до X, потому что …».
Режим запуска: параллельно или последовательно
Профиль отвечает «какие проходы», режим — «как их запускать». Стадии всегда идут по порядку номеров; выбор касается только проходов внутри стадии.
| Режим | Как | Когда |
|---|---|---|
| параллельно (умолчание) | все проходы стадии — одним сообщением | обычный случай: проходы только читают код |
| последовательно | по одному, следующий стартует после отчёта предыдущего | причины ниже, любой одной достаточно |
Последовательный режим выбирается, когда верно хоть что-то из:
- Его попросили. Владелец сказал «гони последовательно» — этого достаточно, обоснования не требуется и переспрашивать не надо. Просьба перекрывает любую эвристику ниже, в том числе когда по ним вышло бы «параллельно». В отчёте — строкой «режим: последовательный, по просьбе». Симметрично: явная просьба гнать параллельно перекрывает пункты 1–4, и тогда в границы покрытия идёт оговорка, что замеры сняты под конкурентной нагрузкой и как оракул слабее.
- Ожидаются замеры. Проход измеряет время удержания блокировки, пик кучи,
рост
-wal, длительность транзакции, пропускную способность. Два таких прохода, запущенных разом на одной машине, соревнуются за диск, CPU и за саму SQLite — и выдают числа, которые не воспроизведутся. Это не гипотеза: находки сессии опираются ровно на такие замеры (5.019 с удержания блокировки приbusy_timeout5000, пик 768 МиБ на теле 40 МиБ, 7 МБ/с роста-wal, 1492 тика из 5502). Число, снятое под конкурентную нагрузку от соседнего прохода, — это находка с испорченным оракулом, а её опровержение стоит дороже, чем весь выигрыш от параллельности. - Машина занята. Идёт другая задача, поднят сервис, гоняется
task verify:archive(минута) илиtask gate(несколько минут). - Нужен ранний выход. См. ниже.
- Разбирается сам конвейер. Когда выясняется, почему проход что-то не нашёл, порядок и изоляция важнее скорости.
Чего режим не меняет — и это не подлежит обсуждению. Проход не видит
находок других проходов ни в каком режиме. «Последовательно» значит «по
очереди», а не «следующий читает предыдущего». Вся ценность конвейера держится
на декорреляции: под всеми ролями одна модель с одними априорными, и стоит
показать ей чужой вывод — она согласится. Согласие нескольких проходов и так не
повышает confidence (см. «Честный предел»); согласие наведённое ещё и
маскируется под независимое подтверждение. Единственный, кто видит всё, —
триаж, и это его работа.
Ранний выход. В последовательном режиме допустимо остановить прогон, не докатив остаток, ровно в одном случае: находка требует переделки формы изменения, и остальные проходы будут смотреть на код, которого через час не станет. Тогда:
- прогон останавливается, находка чинится, конвейер запускается заново с нулевой стадии — а не «доезжает» остатком по старому коду;
- незапущенные проходы идут в границы покрытия строкой «не запускался: прогон остановлен на <проход> из-за <находка>», поимённо;
- триаж запускается только на полном прогоне. Отчёт триажа по половине проходов — ровно тот случай, который уже стоил семи находок: он выглядит полным, потому что агрегирует всё, что ему подали.
Ранний выход по находке, которая чинится в пределах существующей формы
(Действие: инлайн), не делается: дешевле дособрать все находки и починить
пачкой, чем гонять конвейер дважды.
Режим объявляется в отчёте наравне с профилем, и если он последовательный — с причиной. Строка «режим: последовательный, ожидались замеры удержания блокировки» стоит ничего и объясняет, почему прогон занял втрое дольше.
Стадия 0 — Gate (обязательна во всех профилях)
Агент healthlog-review-gate. Запускает task gate и интерпретирует вывод.
Пока гейт красный — опиниативные проходы не запускаются. Оркестратор чинит и перезапускает гейт. Исключение одно: отказ, унаследованный от базовой ветки (гейт проверяет это прогоном на базе) — тогда он фиксируется находкой и не блокирует.
Гейт возвращает не только «зелено/красно», но и находки класса отсутствующая
верификация: изменённые строки без покрытия, конкурентность без теста с
параллельным доступом, флаки-тест (не ниже major), недоступный инструмент.
Шаги выбираются по изменённым файлам: правка документации не гоняет тесты,
линтеры и -race. Пропуск при этом не молчит — он виден в сводке с причиной и
уезжает в границы покрытия, как и любой другой SKIP.
Два шага гейта специфичны для healthlog и красят его безусловно:
no-health-data (файл из data/ попал под контроль версий) и config-samples
(структура конфига изменилась, а config.example.toml/config.docker.toml —
нет).
Стадия 1 — Conformance (обязательна во всех профилях)
Два applicative-прохода: оба применяют записанный критерий, оба дешёвые. По умолчанию — одним сообщением параллельно; замеров они не делают, так что последовательный режим им нужен только из-за занятой машины.
healthlog-review-specs— критерий взят из дельта-спек change вopenspec/changes/<id>/specs/, а не из proposal, сообщения коммита или описания задачи. Сверка двунаправленная; направлениеcode → specважнее.healthlog-review-code— критерий взят изdocs/conventions.md, и только та его часть, которая не выражается правилом: механизируемое уже проверила стадия 0 (sloglint,forbidigo,errorlint,depguard). Уровень лога по адресату, единственный логирующий чекпоинт на доменной границе, трансляция ошибки на внешней границе,ident.Parseна входной границе, время в UTC черезstore.Now().
Recall обоих равен длине их источника — это и есть предел applicative-проходов, ради которого существует стадия 2.
Стадия 2 — Adversarial и operational (standard, deep)
Два прохода:
healthlog-review-adversary— находка есть построенный путь, а не свойство;healthlog-review-ops— постмортем от симптома у владельца сервиса к строке кода.
Это главные кандидаты на последовательный режим. Оба доказывают находки
замером, и оба меряют одно и то же железо: удержание блокировки SQLite, пик
кучи, рост -wal, длительность транзакции. Запущенные разом, они портят числа
друг другу. Если ждёшь от прогона хоть один такой замер — гони их по очереди,
а не одним сообщением.
Эта стадия зарабатывает больше всех остальных вместе, и потому стоит в
standard, а не только в deep. Измерено на пяти задачах: враждебный проход
дал пять из семи выживших находок дозапуска на f8200f7 (включая обе верхние) и
critical на каталоге (доставка с метками из будущего подменяла род метрики);
эксплуатационный — единственный, кто нашёл, что откат бинаря поверх новой схемы
стартует молча. Оба несут внешний оракул по построению: один обязан путь
прогнать, второй смотрит ось времени и эксплуатации, которую не смотрит
никто другой.
Для healthlog эксплуатационный проход обязан держать в голове: телефон шлёт непрерывно и молча, тела доходили до 42 МБ, запись в часовой объект — read-modify-write под конкурентными доставками, а тихо сломавшаяся автоматизация обнаруживается не сразу. Отдельным обязательным вопросом — хватит ли сигналов владельцу, когда поток оборвётся ночью: не «есть ли лог», а увидит ли человек факт, не залезая в SQLite.
Стадия 3 — Independent reimplementation (deep, по триггеру)
healthlog-review-reimpl— пишет свою реализацию, не открывая существующую, затем диффит по решениям. Запускается по триггеру, а не всегда: изменение вводит новое правило слияния, идентичности или разбора. Это самый дорогой проход конвейера (его счёт определяется объёмом вывода — он пишет реализацию целиком), а вне этого триггера независимый взгляд в значительной мере уже дал профильdesign: код писался под его находки. Триггер выбран по факту: единственный раз, когда триаж назвал отсутствиеreimplдырой покрытия, — это была задача с новым правилом слияния сущностей.
Стадия 4 — Global (deep, design)
Агент healthlog-review-architecture. Получает вход шире диффа: дерево
пакетов с назначением, граф внутренних зависимостей, инвентарь существующих
концепций проекта. Готовит вход команда:
task review:context > tmp/review-context.md
Главный вопрос — концептуальная целостность и второй способ делать то, что уже делается. Он же и оправдывает проход: на задаче про пересборку архитектурный проход нашёл, что прогон живого архива был вторым проигрывателем журнала со своим порядком. Второй обязательный вопрос — что опытный человек отсюда удалил бы: слой с единственной реализацией, интерфейс ради мока, незапрошенная конфигурируемость, подстраховка поверх подстраховки. Потолок — 3 находки плюс секция «дешевле переделать до мерджа».
Стадия 5 — Triage (обязательна)
Агент healthlog-review-triage. Единственный, кто агрегирует. Получает сырые
выводы всех проходов и git diff; возвращает финальный отчёт.
Без триажа проходы дают порядка сорока замечаний при единицах существенных. Потребитель здесь — оркестратор, который молча реализует всё, что прочитал: цена нетриажированного отчёта — не потерянное время человека, а разросшийся от вкусовщины код.
Порядок: дедупликация по причине → оракул для всего critical/major →
понижение неподтверждённого до гипотезы → отсев вкусовщины → ранжирование по
ущербу × вероятности → потолок 7 пунктов в основном списке.
Профиль design — до кода
Запускается на шаге ревью спек (healthlog-task-pipeline шаг 4), когда change уже имеет
proposal.md + дельта-спеки, но кода ещё нет. Состав:
healthlog-review-specsв режиме «дизайн ДО кода»;healthlog-review-rubric, фаза 1 без фазы 2: рубрика на задуманный узел становится приёмочными критериями и уезжает вtasks.md;healthlog-review-architectureна предложении: вводит ли change новое понятие, можно ли выразить существующими — включая конструкции stdlib, — не появляется ли второй способ. Вопрос «не изобретаем ли то, что уже есть в библиотеке» переехал сюда из упразднённого прохода про идиоматичность;- вопрос автору дизайна: «предложи три формы решения и назови компромисс каждой» — если ответ показывает, что рассматривалась одна, это находка.
Смысл профиля: архитектурная находка на готовом коде стоит переписывания и поэтому игнорируется; та же находка на предложении стоит абзаца обсуждения.
Контракт находок
Единый для всех проходов — references/finding-contract.md.
Коротко: заголовок через последствие, обязательные поля Файл, Severity,
Confidence, Оракул, Последствие, Предложение, Найдено проходом.
critical без оракула или построенного пути не существует. Находка без поля
«Последствие» не выводится вовсе.
Каждый проход завершает вывод блоком ## Coverage of this pass.
Что происходит с находками дальше
- Оркестратор чинит помеченное
Действие: инлайни не логирует мелочь. Действие: развилка— блокером в секциюблокерыбеклога, вопросом с вариантами и ценой каждого. Оркестратор не останавливается: он урезает изменение до остатка и доводит его.- Находка не для этого мерджа, но реальная (отложенный
major, развилка, решённая «потом») — не теряется: заводится задачей через скиллbacklog(интейк из ревью), с оракулом и провенансом в теле. Мелочь классаnit— в пакетный файл, а не файлом на находку. Promote candidates— по процедуре references/promote.md: находка → конвенция → правило линтера → удаление из конвенций и из промптов. Третий шаг обязателен.- Дефект, проскочивший ревью и всплывший позже, идёт в docs/review-journal.md — сразу, не ретроспективно: теряется именно причина непоймания.
Честный предел
Модель воспроизводит медиану публичного Go, смещённую к популярному и туториальному: отсюда тяга к интерфейсам ради интерфейсов, лишним мокам и конфигурируемости, которую никто не просил. «Идиоматично» и «распространено» — разные вещи; проходы обязаны различать их и опираться на поимённое положение гайда, а не на ощущение частотности.
Согласие нескольких проходов — не подтверждение: это один источник,
высказавшийся несколько раз. Совпадение повышает приоритет, но не confidence.
Ни одному проходу принципиально недоступно:
- поведение Health Auto Export на следующем обновлении приложения;
- то, что реально лежит в Apple Health, — сверить можно только с ручным экспортом, а он делается раз в 2–3 месяца;
- поведение таблицы SQLite под объёмом нескольких лет истории;
- завязка внешних потребителей (агент-медик, трекер, игра) на текущую форму ответа;
- суждение «этой метрики не должно существовать».
Это и есть причина, по которой конвейер готовит ревью, а не заменяет его.
Ссылки
- references/finding-contract.md — контракт находок.
- references/promote.md — промоут находка → конвенция → правило → удаление.
- docs/review-journal.md — журнал проскочивших дефектов.