# Триаж ревью: errors-as-instead-of-typecast

## Сводка

- **Размер / сложность / метка:** среднее × знакомое → **medium**. Режим — по графу.
  База диффа `origin/master` непригодна (удалённая ветка отстала на десятки
  коммитов и даёт 9136 строк шума); реальный вход — рабочее дерево: 5 файлов кода
  и конфига, 3 документа, 2 новых теста, каталог `openspec/changes/`.
- **Состояние гейта:** зелёный. Проверено проходом `autotests` дважды (exit 0) и
  переспрошено триажем поимённо: `golangci-lint run` → `0 issues`,
  `go test ./...` → все пакеты `ok`. Объявленных долгов у гейта после этого
  изменения нет — заявление `CLAUDE.md` подтверждено выводом инструментов, а не
  декларацией.
- **Находок на входе:** 12 сырых от четырёх проходов → **9 различных причин**
  после дедупликации (`Close`/`err2` пришёл трижды, MUST «ровно один раз» —
  дважды) → **5 в первых двух секциях**, 3 в гипотезах, 1 отсеяна.
- **Потолки не срабатывали:** 2 + 3 = 5 при допустимых 3 + 4. Ничего не срезано,
  ничего не выброшено молча.

### Сигнал о заниженной метке

**Пришёл, от одного прохода — `review-code`.** Основания: четыре узла правки,
изменение политики линтера на весь проект (`errcheck.check-blank`), переписанный
раздел «Гейт» в `CLAUDE.md`, введение новой capability `pipeline`. С меткой
`large` запускались бы отдельные проходы `architecture` и `operations` вместо
разбора этих тем внутри `basics`.

`review-basics` запускался и сигнала о метке не подал. Согласие/несогласие
проходов приоритет меняет, `confidence` — нет: метку выбирал `review-scope`.

**Ретроспективно сигнал подтверждается исходом:** три из пяти оставшихся находок
— про документы и норму (`architecture.md`, `conventions/README.md`,
`docs/review.md`, delta-спека), то есть ровно про тот слой, который на метке
`medium` разбирается наименее глубоко.

### План разметки с исходом по каждой теме

| Тема | Дом | Глубина | Кто закрывает | Исход |
|---|---|---|---|---|
| requirements | дельта `specs/pipeline/spec.md` | разбор | `specs` | **закрыта**, 2 находки (1 дошла до блокирующей) |
| autotests | `CLAUDE.md` «Гейт» | — | `autotests` | **закрыта**, 1 находка (понижена в гипотезы) |
| conventions | `docs/conventions/` (errors.md, logging.md, README.md) | разбор | `code` | **закрыта**, 4 находки (1 блокирующая, 1 отсеяна) |
| architecture | `docs/architecture.md` + `passport.md` | разбор | `basics` | **закрыта**, 1 находка |
| security | `docs/security.md` | разбор | `basics` | **закрыта**, 0 находок (все три вопроса неприменимы) |
| operations | `docs/architecture.md` «Эксплуатация» + `database.md` | разбор | `basics` | **закрыта**, 1 находка (понижена: изменением не создана) |

Своих тем проекта нет. **Тем без отчёта нет** — все шесть вернули вывод.

---

## Блокирует мердж

### 1. Дельта-спека закрепляет контрактом поведение, которого у системы нет, и после архивации станет ложной посылкой для следующей задачи

- Файл: `openspec/changes/errors-as-instead-of-typecast/specs/pipeline/spec.md:36-40`
- Severity: major (`critical` не ставится: построенного пути к отказу или потере
  данных нет — вред отложенный и через следующего автора)
- Confidence: high
- Найдено проходами: `specs` (находка 1), `basics` («дешевле переделать», п. 1) —
  две независимые формулировки одной причины; оракула у них не было, оракул добыт
  триажем.
- **Оракул (мой прогон, не на слово):**

  ```
  go test -overlay=<scratchpad>/overlay.json -run TestTriageOneFailureOneRecord -v ./internal/service/
  ```

  Тест поднимает `findJob` с репозиторием, отдающим `errors.New("database is gone")`,
  и логирует возвращённое так, как это делает `worker.go:59`. Вывод:

  ```
  level=ERROR msg="Failed to find and acquire job" state=created error="database is gone"
  level=ERROR msg="Worker error" worker=probe error="failed find and acquire job: created, database is gone"
  --- FAIL: один отказ дал 2 записей уровня ERROR, требование спеки — ровно одна
  ```

  Второй оракул — дословный критерий приёмки этого же изменения
  (`tasks.md`, «Добавлено разбором дизайна»): «**Норма не закрепляет контрактом
  то, что конвенции помечают строкой «Расхождение»: место записи и уровень
  журнала остаются долгом `docs/conventions/logging.md`**». Спека нормирует
  именно место записи. Долг записан в `docs/conventions/logging.md:159-162`.

- Последствие: требование `MUST быть записан ровно один раз единственной
  логирующей точкой` не имеет ни сценария (проверить его нечем — покраснеть оно
  не может), ни срока, ни владельца, ни маркера долга. Оговорка «сегодняшнее
  расхождение этим требованием нормой не объявляется» снимает обратное прочтение,
  но не снимает сам MUST. После `archive` абзац переезжает в
  `openspec/specs/pipeline/spec.md` и читается следующим автором как
  действующая норма: он либо «починит» вторую точку записи, не приняв
  сознательно решение об уровне (`ERROR` против `WARN` — дизайн его намеренно
  не принимал, `logging.md` требует `WARN` для повторяющегося сбоя фонового
  цикла), либо норма сгниёт. По `CLAUDE.md` («что такое сделана»: гейт зелёный
  **и критерии приёмки проверены поимённо») изменение сейчас не выполнено по
  своему собственному критерию.
- Предложение: см. варианты в развилке.
- **Действие: развилка.**

  > В дельта-спеке `pipeline` абзац «Отказ шага MUST быть записан ровно один раз
  > единственной логирующей точкой» нормирует поведение, которого у системы нет
  > (оракул: один отказ хранилища даёт две записи ERROR), и нарушает
  > собственный критерий приёмки задачи. Как поступаем?
  >
  > **(а) Ослабить норму до проверяемого сегодня.** Оставить в требовании только
  > «отказ шага MUST быть виден владельцу записью в журнале и засчитан в счётчик
  > с пометкой отказа» — это покрыто сценарием «Шаг отказал» и проверкой
  > `TestFailureIsLoggedAndCounted`. Единственность логирующей точки вынести
  > отдельной строкой долга со ссылкой на `docs/conventions/logging.md:159-162` и
  > завести задачу в `tasks/items/`. Цена: правка дельта-спеки → **одобрение
  > дизайна отменяется, нужен возврат на чекпоинт**; кода не касается.
  >
  > **(б) Убрать вторую точку записи в этом же изменении.** Снять
  > `s.logger.Error("Failed to find and acquire job", …)` из
  > `internal/service/transcribe.go:398`, оставив запись воркеру. Цена: это чужой
  > долг и рост scope; требует сознательного решения об уровне записи, которое
  > дизайн не принимал; нужен новый сценарий и новая проверка на «ровно один
  > раз». Дельта-спека при этом всё равно правится (добавляется сценарий).
  >
  > **(в) Оставить как есть.** Цена: в актуальную спеку уезжает MUST, который
  > система нарушает с момента `apply` и который никогда не покраснеет.

  **Прямой ответ на заданный вопрос: да, принятие этой находки меняет
  дельта-спеку — в любом из вариантов (а) и (б).** По правилу скилла `resolve`
  это отменяет одобрение дизайна и требует возврата на чекпоинт к человеку.
  Вариант (в) чекпоинта не требует, но оставляет дефект.

### 2. Закрытый долг остался записан в трёх местах, и одно из них — раздел, которым триаж отсеивает ложноположительные: следующая настоящая находка того же класса будет молча выброшена

- Файлы: `docs/conventions/README.md:21`, `docs/conventions/README.md:54`,
  `docs/review.md:69-74`
- Severity: major
- Confidence: high
- Найдено проходом: `code` (находка B); третье место (`docs/review.md`) добавлено
  триажем — это тот же дефект в третьем доме.
- **Оракул — дословные строки, живые на момент триажа:**
  - `docs/conventions/README.md:21`: «…лог пишется на каждом шаге и дублируется
    воркером, **доменные ошибки проверяются приведением типа**.» — расхождения
    больше нет, изменение его закрыло.
  - Правило самого же README (строки 27-29): «Каждое такое место названо в своей
    записи строкой «*Расхождение:*». … **Проходу ревью строка «Расхождение»
    говорит, что находка на этом месте уже известна и новой не считается.**»
  - `docs/review.md:69-74`, раздел «Типовые ложноположительные»: «Настоящий
    дефект рядом другой — **проверка идёт приведением типа и сломается при первой
    же обёртке; он уже записан в
    [conventions/errors.md](conventions/errors.md)**» — ссылка ведёт в файл, из
    которого этот текст изменением удалён (`git diff docs/conventions/errors.md`,
    строки 51-57 сняты).
  - `docs/conventions/README.md:54`: «Непроверенное возвращаемое значение ошибки |
    `.golangci.yml` → `errcheck` (кроме `defer Close` и `send`)» — таблица не
    знает о включённом `check-blank`, то есть о том, что `_ = x.Close()` теперь
    краснеет.
  - Задача сама называет ровно две правки (`tasks.md`, 4.2): «В
    `docs/conventions/errors.md` снять …; **прочие расхождения того файла** не
    трогать». Про соседний `README.md` и про `docs/review.md` там нет ничего —
    места просто пропущены, а не оставлены сознательно.
- Последствие: класс «молчание». `docs/review.md`, «Типовые ложноположительные»
  — единственный проектный вход в шаг отсева триажа. Пока строка жива, следующий
  прогон ревью, увидев приведение типа в новом коде, обязан отнести находку к
  известным и выбросить её — то есть регрессия ровно того дефекта, который
  чинило это изменение, пройдёт молча. `docs.py check` этого не ловит: ссылки не
  битые, битым стало утверждение внутри документа.
- Предложение: снять фразу «доменные ошибки проверяются приведением типа» из
  `README.md:21`; снять или переписать первый пункт «Типовых ложноположительных»
  в `docs/review.md` — оговорка про настоящий дефект рядом больше неверна, сам
  же пункт про `NoopJobError` остаётся верным; в строке таблицы «Механизировано»
  заменить «(кроме `defer Close` и `send`)» на актуальный перечень
  `exclude-functions` и упомянуть `check-blank: true`.
- **Действие: инлайн.** Три текстовые правки, решение однозначно, инвариантов не
  трогает.

---

## Стоит исправить сейчас

### 3. `Close()` адаптера SpeechKit теряет отказ закрытия второго соединения — ровно тот дефект, который изменение объявило закрытым своим критерием приёмки

- Файл: `internal/adapter/recognizer/yandex/speechkit.go:79-91`
- Severity: minor (ущерб мал — см. находку 4: на этом пути оба `Close` в
  сегодняшней реализации возвращают `nil`; вес держится критерием приёмки, а не
  последствием)
- Confidence: high
- Найдено проходами: `specs` (находка 2), `code` (находка D), `basics` (находка
  E) — **три независимых попадания, оракула ни у одного нет**. Совпадение
  повышает приоритет (значит, бросается в глаза), но не `confidence`: под всеми
  проходами одна модель.
- Оракул — дословный критерий приёмки этого же изменения (`tasks.md`, раздел
  «Критерии приёмки»): «**Отказ `Close` не теряется молча: он либо возвращается
  вызывающему, либо попадает в лог.**» Код:

  ```go
  if err1 != nil {
      return err1
  }
  return err2   // err2 при ненулевом err1 теряется молча
  ```

  `errcheck` этого не видит: значение присвоено переменной. То есть механизация,
  ради которой изменение включило `check-blank`, здесь мимо.
- Последствие: при остановке процесса, если оба gRPC-соединения отказали в
  закрытии, владелец увидит один отказ из двух и будет разбирать половину
  картины. Вероятность низкая, стоимость правки — одна строка.
- Предложение: `return errors.Join(err1, err2)` — тот же приём, который изменение
  уже применило в конструкторе восемью строками выше, и `nil` из него отбрасывается.
- **Действие: инлайн.**

### 4. Комментарии и `design.md` описывают поведение gRPC, которого нет: журнал отправит владельца разбирать недоступность Yandex по ошибке, которая может возникнуть только от нашего двойного закрытия

- Файлы: `internal/adapter/recognizer/yandex/speechkit.go:56-59`, `main.go:124-129`,
  `openspec/changes/errors-as-instead-of-typecast/design.md:81-92`
- Severity: minor
- Confidence: high
- Найдено проходом: `code` (находка A). Оракул проверен триажем поимённо.
- **Оракул — исходники `google.golang.org/grpc@v1.74.2`:**
  - `clientconn.go:145` — `func NewClient(...)`: конструктор ленив, соединения не
    открывает (устанавливает его первый RPC либо явный `Connect()`);
  - `clientconn.go:1142-1156` — `Close()` возвращает **только** `nil` либо
    `ErrClientConnClosing`, и только при повторном закрытии (`if cc.conns == nil`);
  - `clientconn.go:67` — `ErrClientConnClosing = status.Error(codes.Canceled,
    "grpc: the client connection is closing")`: статическая константа.
- Последствие, по местам:
  - `speechkit.go:56-59` — комментарий обещает «**уже открытое** соединение могло
    не закрыться»; на деле второй операнд `errors.Join` на этом пути **всегда
    `nil`**. Конструкция безвредна и защищает от смены реализации, но читатель
    выведет из комментария неверную модель API;
  - `main.go:124-129` — комментарий «недоступность внешнего сервиса разбирает он»
    и уровень `ERROR` (по `logging.md` — класс «сбой БД, диска, недоступность
    внешнего сервиса»). Запись достижима только двойным закрытием, то есть нашим
    дефектом. Владелец, увидев её, пойдёт разбирать Yandex вместо своего кода;
  - `design.md:81-92` — тот же неверный образ записан двумя утверждениями
    («соединение с распознаванием уже открыто»; ошибка «несёт состояние
    соединения и адрес узла» — `ErrClientConnClosing` не несёт ни того, ни
    другого) и после архивации поедет дальше как обоснование.
- Предложение: код не трогать. Привести к действительности три текста: в
  `speechkit.go` — «`grpc.NewClient` ленив, закрытие здесь почти всегда `nil`;
  `errors.Join` стоит на случай смены реализации»; в `main.go` — «единственный
  достижимый отказ здесь — повторное закрытие, то есть дефект наш, а не
  внешнего сервиса»; в `design.md` — снять оба неверных утверждения.
- **Действие: инлайн.** Правка `design.md` фактическая, а не решенческая:
  решение («отказ закрытия — по месту, а не единым правилом») остаётся тем же,
  меняется только неверное описание чужого API. Дельта-спеку не трогает,
  чекпоинта не требует.

### 5. Преамбула `docs/architecture.md` описывает состояние после архивации: сегодня она утверждает существование спеки, до которой нет пути

- Файл: `docs/architecture.md:11-23`
- Severity: minor
- Confidence: high
- Найдено проходом: `basics` («дешевле переделать», п. 2)
- Оракул — состояние дерева на момент триажа: `ls openspec/specs/` даёт **только**
  `intake`; в новой преамбуле у пункта `intake` ссылка есть
  (`../openspec/specs/intake/spec.md`), у пункта `pipeline` ссылки **нет** — она
  снята, потому что `docs.py check` краснел битой ссылкой. Задача предписала эту
  правку сама (`tasks.md`, 4.4).
- Последствие: пока изменение не заархивировано, документ утверждает «Заведены
  две capability», а найти вторую читателю негде — единственная форма, в которой
  она существует, лежит в `openspec/changes/`. Если изменение уедет без
  `archive` (а `archive` — отдельный шаг и отдельная команда), расхождение
  останется постоянным, и поймать его нечем: гейт зелёный именно потому, что
  ссылку сняли.
- Предложение: у пункта `pipeline` дописать оговорку о том, где спека лежит
  сейчас и когда переедет — «дельта в
  `openspec/changes/errors-as-instead-of-typecast/specs/pipeline/spec.md`,
  переезжает в `openspec/specs/` при архивации задачи». Одно предложение, ссылка
  на существующий файл, гейт остаётся зелёным.
- **Действие: инлайн.**

---

## Гипотезы без доказательства

### Новая ветка `errors.Join` в `newSpeechKitService` не покрыта ни одним тестом

Понижено с **major/high** до гипотезы и, по существу, до строки в границах
покрытия. Пришло от прохода `autotests` с настоящим оракулом
(`go tool cover -func` → `newSpeechKitService 0.0%`; триаж перепроверил:
`go test -coverprofile` даёт `internal/adapter/recognizer/yandex — coverage:
0.0% of statements`, тестовых файлов в пакете нет вовсе).

Почему понижено: находка 4 показывает, что ветка практически недостижима.
`grpc.NewClient` с константным корректным адресом (`operation.api.cloud.yandex.net:443`)
и валидными TLS-credentials отказывает только на разборе target'а, а второй
операнд `errors.Join` на этом пути всегда `nil`. То есть непокрыт код, который
и выполняться-то не будет. **Предложенное самим проходом «вынести создание
клиента за шов» триаж не рекомендует**: это разросшаяся абстракция в адаптере
ради единственной недостижимой ветки, и заказывать её на основании процента
покрытия — ровно та правка, от которой защищает потолок. Принят второй вариант
самого же прохода: занести в границы покрытия (сделано ниже).

### Признака живости воркера нет: сутки тишины одинаково означают «записей не слали» и «все три воркера висят»

Пришло от `basics` (находка F), понижено: **изменением не создано**. Спека
нормирует молчание пустого прогона, но само молчание стоит на инварианте
`CLAUDE.md` «`NoopJobError` — не ошибка», который старше этой задачи.
Оракула на «воркер висит» нет — поднять SpeechKit в тесте нечем (см. «Недоступно
проверке»). Более того, свойство **уже заведено задачей**:
`tasks/items/service-observability.md`, критерий завершения 1 — «По метрикам
видно, что конвейер встал: задача висит в состоянии дольше обычного, и это
отличимо от «работы нет»». Новой находкой не считается; чинить в этом изменении
нечего, иначе это рост scope на целую тему наблюдаемости.

### Норма спеки пересказана прозой в `docs/conventions/errors.md` — второй дом факта

Пришло от `basics` («дешевле переделать», п. 3). Оракула нет: `openspec/config.yaml:36`
действительно предупреждает, что «второй дом факта расходится с первым молча»,
но новый абзац `errors.md:51-57` заканчивается словами «**Норма записана
требованием capability `pipeline`**», то есть первый дом назван явно. Разделение
здесь защитимо: спека говорит, что делает система, конвенция — как это пишут в
коде. Доказательства предстоящего расхождения у меня нет, а превентивная правка
свелась бы к спору о вкусе. Оставлено гипотезой; если расхождение когда-нибудь
случится, эта запись — след.

---

## Promote candidates

- **Форма `msg` в журнале.** `logger.Error("failed to close audio recognizer", …)`
  (`main.go:126`) — предложение, а не короткая категория, вопреки
  `docs/conventions/logging.md:32-37`. Пришло от `code` (находка C) и **отсеяно
  как находка**: `logging.md:44` уже несёт строку «*Расхождение:* сегодня `msg` —
  предложение вида `Starting conversion job`», и весь корпус журнала написан в
  этой форме; правка одной строки сделает журнал неоднороднее, а не однороднее.
  Это претензия на правило, а не на этот код: либо сканер формы `msg`, либо
  отдельная задача на разовую миграцию всего корпуса.
- **Устаревание утверждения внутри документа не механизировано.** `docs.py check`
  ловит битые ссылки и раскладку, но не ловит ситуацию находки 2: файл на месте,
  ссылка цела, неверным стало утверждение о его содержимом. Кандидат:
  проверка «строка «*Расхождение:*» и упоминание расхождения в чужом файле
  живут парой» либо явные якоря вместо ссылок на файл целиком.
- **Покрытие изменённых строк не считается ничем** (`CLAUDE.md`, «Гейт», сказано
  прямо). Именно поэтому проход `autotests` вынужден был звать `go tool cover`
  руками, а решение «покрывать или занести в границы» принималось на глаз.
  Кандидат в шаг гейта, а не в находку ревью.

---

## Границы покрытия

### План: темы, их дома и глубины

Все шесть тем плана (`requirements`, `autotests`, `conventions`, `architecture`,
`security`, `operations`) имели дом и вернули отчёт — см. таблицу в сводке. Тем
без дома в плане нет. Своих тем проекта нет.

### Какие проходы запускались и в каком режиме

Метка `medium`, режим «по графу». Запущены: `review-autotests`, `review-specs`,
`review-code`, `review-basics`, `review-triage`. Состав соответствует плану
`review-scope`.

### Какие проходы не запускались и почему

- Отдельные проходы **`architecture` и `operations`** — по метке: на `medium`
  эти темы разбираются внутри `review-basics` меньшей глубиной. `review-code`
  подал сигнал, что метка, вероятно, занижена (см. сводку); при `large` эти два
  прохода шли бы отдельно и глубже.
- Прохода **идиоматичности** в конвейере нет — упразднён.
- Прохода **независимой реализации** в конвейере нет — снят по стоимости.

### Что каждый запущенный проход не мог проверить в принципе

- `autotests` — судит оракулы и их способность краснеть, но не судит, верна ли
  сама норма, которую они проверяют; поведение под реальным потоком не
  воспроизводит.
- `specs` — судит соответствие нормы и кода, но не судит, нужна ли норма и не
  дорога ли она; альтернативной формулировки требования не строит.
- `code` — читает дифф; поведения системы целиком, в сборе и под нагрузкой, не
  наблюдает.
- `basics` — тремя темами на малой глубине; ни одну из них до дна не доводит по
  построению.
- `triage` (я) — **ничего нового не нахожу по определению**: я не читаю код в
  поисках дефектов, я работаю с чужими выводами. Пропуск любого прохода — мой
  пропуск тоже, и единственное, что я могу с этим сделать, — назвать его
  поимённо, что и сделано выше.

### Что осталось целиком на человеке

**Не проверит ни один проход** (`docs/review.md:159-166`):

- `operations`: поведение внешних сервисов под нагрузкой и на границах —
  SpeechKit и Object Storage поднять в тесте нечем;
- `operations`: реальный профиль нагрузки. Проект работает на единицах записей в
  день, и утверждения о росте остаются условиями, а не замерами;
- `security`: стойкость `ffmpeg` к вредоносному входу — разбор чужого формата
  отдан внешней программе, и она вне нашей границы.

**Перестали проверять сознательно** (`docs/review.md:168-175`):

- `autotests`: разбор вывода настоящего `ffprobe`. Проверки приёма звали его до
  2026-08-11 — правда, звали так, что он всегда отказывал, — а теперь получают
  длительность от подставного источника. Своего теста у
  `adapter/metaviewer/ffmpeg` нет; решение и его цена — в
  `docs/adr/ADR-2026-08-11-stub-adapters-in-tests.md`.

**Плюс этим прогоном:**

- `internal/adapter/recognizer/yandex` — покрытие **0.0 %**, тестовых файлов в
  пакете нет вовсе; новая ветка `errors.Join` в `newSpeechKitService:53-63` не
  исполняется ни одной проверкой. Заносится сюда сознательно вместо заведения
  шва (см. гипотезы);
- `main.go` — пакет `main` в проекте никогда не тестировался, шва нет; новый
  `defer` с логированием отказа закрытия проверен только чтением (`tasks.md`,
  2.4: между постановкой `defer` и завершением `main` нет `os.Exit`);
- реальный путь `NoopJobError` от SQLite-репозитория (не от подставного) —
  проверки обоих звеньев работают на заглушках.

**Общее, что не проверяет никто:** история инцидентов; поведение под реальным
потоком; поведение внешних систем в их версиях (утверждения о gRPC в находке 4
сняты с исходников `v1.74.2` — на другой версии их надо перепроверять);
завязка потребителей на текущее поведение; вопрос «а нужна ли эта
функциональность вообще».

### Каких документов проекта не хватило

- **`docs/adr/` по теме этого изменения — записи нет.** Решение «`NoopJobError`
  и `JobNotFoundError` остаются типизированными, а не становятся sentinel»
  принято в `design.md` (Решение 1) и живёт только там, в документе изменения,
  который после архивации уедет в `openspec/changes/archive/`. Через полгода
  обоснование придётся выводить заново.
- **Строка «*Расхождение:*» не имеет владельца и срока по построению**
  (`docs/conventions/README.md:27-29`). Из-за этого долг «одна запись отказа —
  две строки журнала» нельзя ни просрочить, ни закрыть — что и породило находку 1.
- Прочих пробелов проходы не заявили. `docs/security.md`, `docs/database.md`,
  `docs/passport.md`, `docs/conventions/*` на месте и периметр называют;
  инварианты в `CLAUDE.md` есть и снабжены severity — деградации по этому
  разряду на этом прогоне не было.

### Сработавшие потолки — по строке на проход

- `review-basics` — потолок **2/4**, показано 2 находки + 3 пункта «дешевле
  переделать до мерджа». Потолок не срабатывал, за срезом ничего не осталось.
  Проход сообщил это сам.
- `review-autotests` — **о своём потолке не сообщил**; показана 1 находка и 1
  отклонённая гипотеза. Это находка о прогоне: сколько осталось за срезом,
  установить нечем.
- `review-specs` — **о своём потолке не сообщил**; показано 2 находки. То же.
- `review-code` — **о своём потолке не сообщил**; показано 4 находки при
  раздельных потолках половин `conventions` и `техника`. То же.
- `review-triage` (я) — потолки 3 и 4, занято 2 и 3. **Потолок не срабатывал,
  ничего не срезано, ничего не выброшено молча.** Единственная отсеянная находка
  (форма `msg`) названа поимённо в `Promote candidates` с причиной отсева.

### Четыре строки триажа: чего в конвейере нет вовсе

1. **Решения проекта не сверялись.** `docs/adr/` — процессный документ, прогон
   его не открывает. Изменение вводит новую capability `pipeline` и меняет
   политику линтера на весь проект; расходится ли это с записанными ADR, ни один
   проход не проверял. Ловит такое сверка документации — скилл
   `av-dev-docs:healthcheck`, и звать его надо руками (`CLAUDE.md`, «Гейт»:
   «согласованность документов между собой и с кодом … звать его надо руками»).
2. **Записанные наблюдения проекта не использовались.** `docs/research/` — тоже
   процессный. Каждое число в этом отчёте снято на этом прогоне приложенной
   командой: `0 issues` от `golangci-lint run`, `0.0 % of statements` от
   `go test -coverprofile`, «2 записи ERROR» от прогона с `-overlay`. Чисел без
   команды замера в отчёте нет.
3. **Поимённая сверка с руководствами по стилю Go не задавалась ни одним
   проходом.** `errors.Join` в конструкторе, `errors.As` с указателем на
   указатель, `defer` с телом вместо голого вызова — все три конструкции судились
   по внутренним конвенциям проекта и по линтеру. Различение «идиоматично против
   просто распространено» не спрашивал никто с тех пор, как упразднён проход про
   идиоматичность.
4. **Альтернативной реализации, с которой можно сдиффить решения, у конвейера
   нет.** Проход независимой реализации снят по стоимости, а не по замеру. Вопрос
   «а не решается ли задача «признак не ломается обёрткой» иначе — например,
   sentinel-значениями, как сам `design.md` рассматривает в Решении 1» никем
   независимо не проверялся: рассмотрел и отверг его автор дизайна, и ревью
   сверялось с его же рассуждением.

Метка `medium`, поэтому пятая строка про `small` не применяется: темы `security`,
`operations` и `architecture` разбирались проходом `basics` по своим домам, а не
только по инвариантам `CLAUDE.md`.

---

**Формулировка «критичных проблем не обнаружено» в этом отчёте не употребляется
и не подразумевается.** `critical` не выставлен ни одной находке по конкретной
причине: построенного пути к потере данных, порче или утечке секрета ни один
проход не предъявил, а `critical` без оракула или построенного пути не
существует. Что именно осталось непроверенным — перечислено выше поимённо.
