--- name: healthlog-review-pipeline description: Конвейер ревью изменений healthlog — детерминированный гейт, сверка с дельта-спеками OpenSpec в обе стороны, враждебные постановки и эксплуатационный постмортем, независимая реализация по триггеру, архитектура и обязательный триаж. Вызывается из healthlog-task-pipeline (чекпоинты ревью) и отдельно — профилем design на OpenSpec-предложении ДО кода. --- # Конвейер ревью (healthlog) Готовит ревью — **не заменяет его**. Потребитель отчёта — оркестратор, который чинит код; человек читает только сводку, развилки и границы покрытия. ## Три правила, из которых всё следует Если ситуация не покрыта инструкцией — решай по ним. 1. **Recall чек-листа равен длине чек-листа.** Проход, устроенный как «проверь пункты 1..N», найдёт ровно перечисленное. Всё неявное — идиомы, форма решения, «так не делают» — неперечислимо по определению: перечислимое уже стало бы конвенцией. Отсюда деление проходов на **applicative** (применяют заданный критерий) и **generative** (сперва порождают критерий или альтернативу, потом сравнивают). Расширять чек-листы бесполезно; неявный слой достают только generative-проходы. 2. **Ценность верификатора = наличие внешнего оракула × декорреляция с автором**, а не число ролей. Под всеми ролями одна модель с одними априорными, вход у всех общий: седьмая роль почти не добавляет recall, но линейно удорожает триаж. Иерархия надёжности: детерминированный инструмент > агент, который его **запускает** и интерпретирует вывод > агент с чистым мнением. Максимум работы переносим вниз. 3. **Отчёт без границ покрытия хуже отсутствия отчёта.** «Критичных проблем не обнаружено» потребляет ощущение проверенности, ничего не гарантируя. Секция границ покрытия обязательна и не сокращается — в том числе в докладе человеку. ## Что этот конвейер защищает в 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, потому что …». ## Стадия 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//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` — постмортем от симптома у владельца сервиса к строке кода. **Эта стадия зарабатывает больше всех остальных вместе, и потому стоит в `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` + дельта-спеки, но кода ещё нет. Состав: 1. `healthlog-review-specs` в режиме «дизайн ДО кода»; 2. `healthlog-review-rubric`, фаза 1 без фазы 2: рубрика на задуманный узел становится приёмочными критериями и уезжает в `tasks.md`; 3. `healthlog-review-architecture` на предложении: вводит ли change новое понятие, можно ли выразить существующими — **включая конструкции stdlib**, — не появляется ли второй способ. Вопрос «не изобретаем ли то, что уже есть в библиотеке» переехал сюда из упразднённого прохода про идиоматичность; 4. вопрос автору дизайна: **«предложи три формы решения и назови компромисс каждой»** — если ответ показывает, что рассматривалась одна, это находка. Смысл профиля: архитектурная находка на готовом коде стоит переписывания и поэтому игнорируется; та же находка на предложении стоит абзаца обсуждения. ## Контракт находок Единый для всех проходов — [references/finding-contract.md](references/finding-contract.md). Коротко: заголовок через **последствие**, обязательные поля `Файл`, `Severity`, `Confidence`, `Оракул`, `Последствие`, `Предложение`, `Найдено проходом`. `critical` без оракула или построенного пути не существует. Находка без поля «Последствие» не выводится вовсе. Каждый проход завершает вывод блоком `## Coverage of this pass`. ## Что происходит с находками дальше - Оркестратор чинит помеченное `Действие: инлайн` и **не логирует мелочь**. - `Действие: развилка` — блокером в секцию `блокеры` беклога, вопросом с вариантами и ценой каждого. Оркестратор не останавливается: он урезает изменение до остатка и доводит его. - Находка не для этого мерджа, но реальная (отложенный `major`, развилка, решённая «потом») — не теряется: заводится задачей через скилл `backlog` (интейк из ревью), с оракулом и провенансом в теле. Мелочь класса `nit` — в пакетный файл, а не файлом на находку. - `Promote candidates` — по процедуре [references/promote.md](references/promote.md): находка → конвенция → правило линтера → **удаление из конвенций и из промптов**. Третий шаг обязателен. - Дефект, проскочивший ревью и всплывший позже, идёт в [docs/review-journal.md](../../../docs/review-journal.md) — сразу, не ретроспективно: теряется именно причина непоймания. ## Честный предел Модель воспроизводит медиану публичного Go, смещённую к популярному и туториальному: отсюда тяга к интерфейсам ради интерфейсов, лишним мокам и конфигурируемости, которую никто не просил. **«Идиоматично» и «распространено» — разные вещи**; проходы обязаны различать их и опираться на поимённое положение гайда, а не на ощущение частотности. Согласие нескольких проходов — **не подтверждение**: это один источник, высказавшийся несколько раз. Совпадение повышает приоритет, но не `confidence`. Ни одному проходу принципиально недоступно: - поведение Health Auto Export на следующем обновлении приложения; - то, что реально лежит в Apple Health, — сверить можно только с ручным экспортом, а он делается раз в 2–3 месяца; - поведение таблицы SQLite под объёмом нескольких лет истории; - завязка внешних потребителей (агент-медик, трекер, игра) на текущую форму ответа; - суждение «этой метрики не должно существовать». Это и есть причина, по которой конвейер готовит ревью, а не заменяет его. ## Ссылки - [references/finding-contract.md](references/finding-contract.md) — контракт находок. - [references/promote.md](references/promote.md) — промоут находка → конвенция → правило → удаление. - [docs/review-journal.md](../../../docs/review-journal.md) — журнал проскочивших дефектов.