# Триаж ревью: record-centric-model

## Сводка

- **Режим прогона:** по графу. **Метка:** `large`, обоснование разметки — «крупное ×
  незнакомое» (смена модели очереди: захват, повторы и воркеры разом; конвейер задач
  трогается целиком). Размер и сложность числом в переданном плане не названы; свой
  замер объёма — 61 путь в рабочем дереве (`git status --short | wc -l`), изменение
  лежит некоммитнутым поверх `d079f03`.
- **Состояние гейта:** зелёный, `task gate` exit 0 (проход `autotests`, лог в
  `scratchpad/gate_run.log`).
- **Находок на входе:** 28 пронумерованных находок шести проходов плюс 14 пунктов в их
  дополнительных секциях (specs — 9 «поведение вне спеки», architecture — 3 «дешевле
  переделать до мерджа», ops — 2 ответа сверх перечня). После дедупликации по причине
  осталось 24 различимые причины; в первых двух секциях — **7**.
- **Сверка с «Типовыми ложноположительными»** (`docs/review.md`) выполнена: под пункт
  «файлы и объекты не удаляются, диск растёт» формально попадали две находки, обе
  оставлены — у обеих есть замер, которого пункт и требует. Пункт про молчание воркера
  на `NoopJobError` не выбрасывает находку №2, но **ограничивает её починку** — см.
  внутри находки. Пункты про гонку захвата и про запись без владельца отменены
  редакциями 2026-08-14 и к находкам этого прогона не применялись.

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

| тема | дом | глубина | кто закрывает | исход |
|---|---|---|---|---|
| requirements | `openspec/specs/` + дельты change | разбор | specs | закрыта, 6 находок + секция из 9 пунктов |
| autotests | CLAUDE.md, «Гейт» | — | autotests | закрыта, 3 находки; гейт зелёный, флаки не найдены (3 прогона `-race`) |
| conventions | `docs/conventions/` | разбор | code | закрыта, 8 находок (4 техника + 4 конвенции) |
| architecture | `docs/architecture.md` + источник `passport.md` | доказательство | architecture | закрыта, 3 находки + секция из 3 пунктов |
| security | `docs/security.md` | доказательство | adversary | закрыта, 2 построенных пути + 1 свойство без пути |
| operations | `docs/architecture.md` «Эксплуатация» + источник `database.md` | доказательство | ops | закрыта, 5 находок |
| темы проекта | дома нет | — | basics | **не запускался**: своих тем у проекта нет, все документы `docs/` разошлись по шести темам ядра |

- **Тем без отчёта нет.** Каждая заявленная тема отчиталась; единственная строка «дома
  нет» — `темы проекта`, и она заявлена такой в самом плане, а не потеряна на прогоне.
- **Сигнал о заниженной метке не пришёл ни от одного прохода.** `review-code`
  отработал и возражений по метке не подал; `review-basics` на этом прогоне не
  запускался, то есть его половина корректора не работала вовсе. Метку выбирал
  `review-scope`, и независимая проверка метки прошла в одном лице из двух.

---

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

### Архив хранит не то, что пришло от провайдера: неизвестные поля ответа исчезают молча

- Файл: `internal/adapter/recognizer/yandex/speechkit.go:200-235`
- Severity: major
- Confidence: high
- Оракул: зонд прохода `specs` — `proto.Marshal` сохраняет неизвестное поле (4 байта),
  пара `encodeResponses`/`decodeResponses` через `protojson` отдаёт 0 байт. Сверено
  чтением на месте: `encodeResponses` зовёт `protojson.Marshal(resp)`, а комментарий
  над ней обещает «ответ провайдера целиком, в том виде, в каком он пришёл».
  Норма — дельта `recognition`, `openspec/changes/record-centric-model/specs/recognition/spec.md:36-38`:
  «Сервис SHALL сохранять ответ распознавателя целиком, в том виде, в каком он пришёл».
- Последствие: `protojson` выбрасывает поля, которых нет в вендоренной схеме. Всё, что
  SpeechKit добавит в ответ (и всё, что уже есть в версии сервиса новее нашей
  go-genproto), в сохранённой попытке отсутствует, и узнать об этом нечем: разбор
  проходит успешно. Ради этого архива и заведена коллекция `recognitions` — «станут
  доступны, когда мы научимся их читать». Не станут. Повторное распознавание стоит
  денег (`CLAUDE.md`, «Запреты», Yandex Cloud за деньги), а исходное аудио к тому
  моменту может быть уже единственным, что осталось.
- Предложение: развилка, потому что решается формат файла на диске, а он в проекте
  необратим (`CLAUDE.md`, «Работа»: «формат файла на диске» спрашивается у человека
  всегда). Варианты: **(а)** хранить `proto.Marshal` — неизвестные поля переживают
  цикл, цена: содержимое перестаёт читаться глазами и в панели; **(б)** оставить
  `protojson` и переписать требование дельты, назвав цену прямо («храним разобранное
  нашей схемой, а не пришедшее»); **(в)** хранить оба представления — цена в объёме
  файла, вдвое.
- Найдено проходом: specs
- Действие: развилка

### Шаг сообщает исход одним `nil`, и остановка приговором засчитывается успехом наравне с откладыванием опроса

- Файл: `internal/service/transcribe.go:274-285`, `:634-644`, `:734-762`
- Severity: major
- Confidence: high
- Оракул: три независимых замера на живом хранилище (specs, code, ops) плюс сверка
  чтением на месте. `failStep` (`:735-739`) возвращает `nil` после `halt`, ветка
  «операция ещё идёт» (`:634-644`) тоже возвращает `nil`, а `RunStep` судит исход
  только по `stepErr != nil` (`:274-285`). Замеры: остановленная запись даёт два
  события — `halted`, следом `done`; счётчик `error=false` 1→2, `error=true` 0→0;
  halt по `stuck` — `error=true` 0→0; 3 откладывания дают 3 строки `poll/done`,
  50 циклов опроса — +50 строк `record_events`. Константа `EventOutcomeFailed`
  объявлена (`internal/entity/record_event.go:15`) и не пишется ни одной строкой кода.
  Нормы: `docs/architecture.md:156-161` — «Владелец — по метрике
  `transcriber_worker_job_count` с меткой `error="true"` … Отдельного оповещения нет»;
  дельта `pipeline`, `spec.md:264` — «Журнал MUST не писаться на каждое откладывание
  опроса» и сценарий `spec.md:280-284` «Откладывание строки не пишет».
- Последствие: три причины остановки — исчерпанные попытки, застревание, приговор шага
  — не двигают единственный канал владельца. Запись умерла, метрика показывает успех,
  журнал записи утверждает `done`. Отправителю сообщение уходит (`halt` зовёт
  `notify`), то есть инвариант «Принятая запись не теряется молча» формально держится
  ровно наполовину: пользователь знает, владелец — нет. Второй половиной та же причина
  забивает журнал: часовое распознавание кладёт ≈720 строк `done`, суточное — до ~17000
  на одну запись, и настоящие события в нём тонут.
- Предложение: исход шага должен называться, а не выводиться из `nil` — отдельным
  значением («сделано» / «отложено» / «остановлено»), и `RunStep` пишет `done` только
  на первом. Остановка пишет `EventOutcomeFailed` (либо оставляет один `halted`) и
  двигает `transcriber_worker_job_count{error="true"}` — тот, который назван каналом
  владельца. **Ограничение починки, нарушить его нельзя:** считать в метрику сам
  `NoopJobError`, которым `acquire()` возвращает остановленную запись воркеру,
  запрещено инвариантом `CLAUDE.md` («`NoopJobError` — не ошибка», major) и записано
  ложноположительным в `docs/review.md`. Значит счёт и событие ставит сам `halt`, а не
  воркер.
- Найдено проходом: specs (2 находки), code (2), ops (2) — шесть формулировок одной
  причины; оракулы независимые, `Confidence` от совпадения не растёт
- Действие: инлайн

### Единственный объявленный способ убрать запись оставляет полный текст речи на диске, а удаление учётной записи отвергается чужим сообщением

- Файл: `internal/adapter/repo/pocketbase/owner_guard.go:36-80`,
  `internal/adapter/repo/pocketbase/migrations/202608140002_record_centric_model.go:225-302`
- Severity: major
- Confidence: high
- Оракул: прогон прохода `adversary` на живом хранилище — удаление строки
  `audio_records` отвергается (связи приложений `Required:true` без каскада), удаление
  строки `files` проходит молча, на диске остаётся
  `storage/<recognitions>/<запись>/<имя>.payload` с текстом речи. Второй прогон:
  удаление учётной записи с архивом даёт наш отказ с причиной, с одной темой —
  «Make sure that the record is not part of a required relation reference». Сверено
  чтением: `countOwned` перебирает `RecordsCollection` и `FilesCollection`, а
  коллекций с колонкой `owner` в шаге схемы **три** — `topics` заводится с полем
  `owner` и уникальным индексом `idx_topics_owner_name`. Комментарий над функцией
  обещает «по обеим коллекциям». Нормы: `docs/security.md:335-341` — «единственный
  способ убрать запись — руками в базе и в каталоге на сервере», а задача
  `delete-record` обязана убирать «все уровни текста»; дельта `storage` требует, чтобы
  отказ называл причину.
- Последствие: владелец, выполнивший единственную записанную процедуру удаления,
  получает отказ на строке записи и удаляет файл — после чего считает данные
  удалёнными, а расшифровка речи человека остаётся на диске бессрочно. Второй путь:
  собственный страж, заведённый ровно ради того, чтобы владелец не пошёл удалять
  связи руками, на учётной записи с темой молчит и пропускает вперёд «ведущую»
  подсказку библиотеки — то есть ведёт владельца делать необратимое. `docs/security.md`
  этим изменением не тронут вовсе, хотя содержимое переехало в шесть коллекций и
  завелась вторая раскладка файла на диске.
- Предложение: перечень коллекций с владельцем — одно место, выводимое из шага схемы
  (инлайн-часть, `topics` добавляется сразу). Дальше развилка по удалению: **(а)**
  завести каскад/процедуру, убирающую запись со всеми уровнями текста и файлами, до
  мерджа; **(б)** оставить как есть, но переписать `docs/security.md` под новую
  раскладку и назвать процедуру поимённо, включая `recognitions/*.payload`, и
  дополнить «Затрагивает» задачи `delete-record`; **(в)** признать удаление
  недоступным до `delete-record` и сказать это в `security.md` прямо.
- Найдено проходом: adversary (2 пути), code (1 находка о `topics`) — один корень
- Действие: развилка

---

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

### Откат образа поверх применённого шага схемы не диагностируется: старый бинарь встаёт молча и ломает 100% очереди

- Файл: `internal/adapter/repo/pocketbase/migrations/migrations.go`,
  шаг `202608140002_record_centric_model.go`
- Severity: major
- Confidence: high
- Оракул: замер прохода `ops` двумя реальными бинарями — старый бинарь поднимается на
  каталоге новой схемы **без ошибки**, затем 100% обращений к очереди дают
  `failed to find collection transcribe_jobs`. `RunAllMigrations` накатывает
  недостающие из своего списка и шагов новее не видит; `down` коллекцию не
  восстанавливает.
- Последствие: это первый шаг схемы проекта, который убирает коллекцию, а не
  добавляет. Штатное средство владельца на инциденте — откатить образ — с этого
  момента делает хуже и не говорит об этом: сервис стартует зелёным и отказывает на
  каждой записи. Обратно чинится повторной выкладкой нового образа, то есть ущерб
  обратим, но обнаруживается он в худший момент и не тем сообщением. Шаг схемы после
  выкладки не переписывается (`CLAUDE.md`, инвариант, **critical**), поэтому дешёвая
  минута — сейчас.
- Предложение: развилка. **(а)** старт отказывается работать на схеме новее своего
  списка — явным сообщением «база новее бинаря, откат образа не поддержан»; цена:
  проверка версии схемы на подъёме, ~десяток строк плюс её норма;
  **(б)** записать в `docs/architecture.md`, «Эксплуатация», что откат образа через
  этот шаг невозможен и что делать вместо него; цена: только текст, ловушка остаётся;
  **(в)** признать осознанным и не делать ничего.
- Найдено проходом: ops
- Действие: развилка

### После жёсткого падения воркера запись невидима до восьми часов, а `own_work_limit_minutes` этим не управляет

- Файл: `internal/service/transcribe.go:296-339`,
  `internal/adapter/repo/pocketbase/record_repo.go:190-215`, `internal/entity/stage.go`
- Severity: major
- Confidence: high
- Оракул: замер прохода `ops` — после захвата без `Save` повторный захват записи не
  выдаёт; halt по `stuck` наступает только по истечении `acquire_expires_at`. Сверено
  чтением: сторож простоя `isStuck` проверяется **после** захвата (`:328`), а захват
  фильтрует по `acquire_expires_at` (`record_repo.go:215`), чей срок для приведения и
  отправки — 8 часов (`docs/database.md:274-275`).
- Последствие: настройка продана владельцу как сторож зависания — «предел простоя, своя
  работа, 60 минут, сторож ловит зависание» (`docs/database.md:279`), — но для
  крашнутого или убитого держателя реальный предел вчетверо с лишним больше и задаётся
  другим числом из другого файла. Запись человека молча стоит до восьми часов, и ни
  один документ этого расхождения не называет. Класс — «молчание»: владелец узнает
  только по отсутствию ответа.
- Предложение: свести к одному числу либо назвать оба и их роли — в
  `docs/database.md` и в дельте `pipeline`. Развилка: **(а)** срок захвата опустить до
  предела простоя (цена: многочасовое приведение начнёт терять захват и перезапускаться
  — именно та цена, ради которой 8 часов и стоят); **(б)** оставить два числа, но
  сделать протухший захват видимым (сторож простоя судит и по времени захвата);
  **(в)** оставить как есть и записать в `database.md` прямо, что для крашнутого
  держателя предел — срок захвата, а не `own_work_limit_minutes`.
- Найдено проходом: ops. **Понижено при триаже:** половина находки прохода
  `architecture` — «имя ключа в коде разошлось с `design.md`» — из основного списка
  снята: `config.example.toml`, `internal/config/config.go:35` и канон
  `docs/database.md:279` называют ключ `own_work_limit_minutes` согласованно, разошёлся
  один `design.md` изменения. Необратимости здесь нет, это дрейф документа — его дом
  `av-dev:doc-healthcheck`, не ревью.
- Действие: развилка

### Переписанный конвейер уехал без проверок: пять тестов снесено, четыре узла с нулевым покрытием

- Файл: `internal/controller/worker/worker.go:100-147`,
  `internal/adapter/recognizer/yandex/speechkit.go:206-280`,
  `internal/controller/http/transcribe.go:140-166`,
  `internal/adapter/repo/pocketbase/panel.go:33-99`,
  `internal/adapter/repo/pocketbase/owner_guard.go:36-80`
- Severity: major
- Confidence: high
- Оракул: `go test ./... -coverpkg=./...` (проход `autotests`) — `Pool`/`NewPool`/
  `Size`/`Start` 0.0%, `encodeResponses`/`decodeResponses`/`outcomeFromResponses` 0.0%,
  `GetTranscribeJobStatus` 52.9% (ветка выдачи готовой расшифровки, ветка 500 при
  отказе `textRepo` и новое поле `halted` без единого assert). `git status`: удалены
  `owner_test.go`, `transcript_job_repo_test.go`, `file_repo_test.go` — пять проверок
  снесено, а не переписано. Норма: `docs/review.md`, «Типовые узлы», «Любой узел» —
  «изменённое место покрыто хоть одним **проходящим** тестом»; журнал 2026-08-10
  показывает, чем это кончается.
- Последствие: пул одинаковых воркеров — предмет всего изменения — не поднимается ни
  одним тестом; разбор реального ответа SpeechKit не проверен ничем, хотя `Parse` —
  чистая функция и сети не требует; панельный возврат записи в работу и страж удаления
  учётной записи держатся на чтении глазами, и находка №3 показывает, что чтение уже
  один раз промахнулось. Зелёный гейт здесь не означает проверенного кода.
- Предложение: инлайн, четыре адреса. Приоритет — `encodeResponses`/`decodeResponses`
  (дешевле всех, чистые функции, и это оракул находки №1), `owner_guard` с тремя
  коллекциями, ветки `GetTranscribeJobStatus` включая `halted`, подъём пула.
  Панельный возврат — тестом против настоящего хранилища, как это делают
  `schema_test.go` и `ownership_test.go`.
- Найдено проходом: autotests (3), specs (1)
- Действие: инлайн

### Колонка `source_uri` уезжает необратимым шагом схемы, и читателя у неё нет

- Файл: `internal/contract/repository.go`, `internal/contract/contract.go`,
  шаг `202608140002_record_centric_model.go`
- Severity: minor
- Confidence: high
- Оракул: проход `architecture` — `grep` по рабочим путям: колонка пишется и не
  читается ни одним, адрес объекта пересчитывается `SourceURI(objectKey)` на месте
  употребления. Контракт распознавателя вырос с 3 методов до 9: `Upload`,
  `ObjectExists`, `SourceURI` вынесли в ядро ключ объекта и идемпотентность заливки.
- Последствие: шаг схемы после выкладки не переписывается (`CLAUDE.md`, инвариант,
  **critical**), поэтому снять колонку потом можно только новым шагом. Пока она есть,
  следующий читатель обязан гадать, что из двух — колонка или пересчёт — правда, а
  ядро обязано знать про объектное хранилище провайдера, чего оно знать не должно.
  Цена сегодня — минуты, после мерджа — новый шаг схемы и вопрос человеку.
- Предложение: развилка. **(а)** свернуть заливку в один метод `EnsureUploaded` и
  убрать `SourceURI`/`ObjectExists` из контракта; **(б)** оставить контракт и начать
  читать `attempt.SourceURI` вместо пересчёта — тогда у колонки появляется читатель;
  **(в)** признать колонку заделом осознанно и снять её из шага схемы **до** мерджа,
  вернув отдельной задачей.
- Найдено проходом: architecture
- Действие: развилка

---

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

Понижены: оракула нет либо путь не построен. Все — `major` и ниже по контракту.

- **Отказ чтения в `Put` подменяется заведением новой строки** (`text_repo.go:33-42,
  92-101`, было minor/high у specs и code). Код не различает `sql.ErrNoRows` от отказа
  базы: на кратком отказе хранилища вместо обновления заводится вторая строка текста.
  Зонда никто не снял — понижено до гипотезы, но починка дешёвая и очевидная
  (`errors.Is(err, sql.ErrNoRows)`).
- **Приговор шага выносится с первой попытки, и `maxAttempts` не работает никогда**
  (secция «поведение вне спеки» прохода specs). Отказ `ffmpeg` идёт через `failStep`
  сразу, то есть счётчик попыток не доживает до сторожа. Замера нет, дельтой вопрос
  «какие отказы приговор, а какие повтор» не решён вовсе — это **самый весомый из
  отложенного**: если гипотеза верна, кратковременный отказ внешней программы хоронит
  запись человека с первой попытки. Стоит зонда в следующем прогоне либо строки в
  дельте `pipeline`.
- **Признак «работы нет» сервис выдаёт сам за прогоны, в которых работа была**
  (`transcribe.go:244,258,319,332`). Дельта требует, чтобы признак рождался только
  ответом хранилища на опрос. Последствие — искажение наблюдаемости, не поведения;
  зонда нет.
- **Пул без верхней границы** (замер ops: 12.6k оп/с не растёт с N, латентность
  78.7µs → 7.93ms при 1→100 воркерах; `Validate()` проверяет только `Workers<0`).
  Замер настоящий, но `docs/review.md`, «Недоступно проверке», прямо говорит: реального
  профиля нагрузки у проекта нет, «утверждения о росте остаются условиями». Проект
  работает на единицах записей в день, ущерб сегодня нулевой — гипотеза, не находка.
- **Ручная правка рубежа в панели у остановленной записи не снимает признак остановки**
  (секция specs). Путь не построен, поведение панели проверено только чтением.
- **Пауза при переходе на `submitted` нигде не нормирована** и **мёртвая пара
  `location`/`object_key`** (секция specs). Расхождения без названного последствия.
- **Таймаутов у Telegram, S3 и SpeechKit по-прежнему нет ни одного, `FindAndAcquire` не
  принимает контекст** (ops). Не находка этого изменения: отсутствие таймаутов
  записано в `docs/review.md`, «Вопросы по темам», чтением от 2026-08-13 и старше
  задачи. Названо, чтобы не читалось как новое.

## Promote candidates

- **Сканер на пару «правило домена ↔ его повтор в адаптере».** `panel.go:33-99`
  повторяет `entity.AudioRecord.Resume()` колонками, а `grep '\.Resume()'` даёт
  единственное вхождение — в тесте. `internal/archrules` такую пару не держит, и
  разойдутся они молча. Правило механизируемо — значит это кандидат в сканер, а не
  находка ревью (контракт находок, `nit`).
- **Правило конвенции для новых `select`-перечислений.** Пять новых перечислений
  закрыты схемой, `docs/conventions/database.md` даёт изъятие только для `state`, ни
  одной строки «*Расхождение:*» не добавлено. Либо изъятие расширяется, либо каждая
  новая строка объявляется — сегодня не сказано ни то, ни другое.
- **`schemaFieldNames` в `internal/archrules` собирает поля из всех коллекций каталога
  шагов, включая снесённую `transcribe_jobs`.** Правило остаётся зелёным и при колонке,
  объявленной в чужой коллекции, — то есть страж инварианта про колонки ослаб. Это
  правка самого правила (не «проверка над проверкой», запрет `CLAUDE.md` сюда не
  достаёт), но она сама себе кандидат в конвенцию: перечень схемы читается по текущей
  схеме, а не по истории каталога.

## Урожай (отложено, сработал потолок)

Ни один пункт ниже не выброшен — они не поместились в семь и ждут своей задачи.

- **Опрос чужой операции пишется на `INFO` двумя строками за цикл**
  (`transcribe.go:623,637`). `docs/conventions/logging.md:60,77,183` называет «проверку
  готовности операции распознавания» уровнем `DEBUG` поимённо; ≈1440 строк `INFO` на
  часовую запись. Плюс записанное там же *Расхождение*: `DEBUG` включить нечем, уровень
  зашит в `main.go`. Починка — одна замена уровня, но она без предмета, пока уровень не
  настраивается.
- **Правило возврата записи в работу написано дважды** (`panel.go:33-99` против
  `entity.AudioRecord.Resume()`), и событие журнала пишется двумя способами —
  `appendEvent` через контракт и `appendResumeEvent` вручную через `core.NewRecord` в
  адаптере. Механизируемая часть ушла в promote выше; остаток — решение, какой из двух
  путей настоящий.
- **Поверхность без вызывающих:** `contract.Clock` (реализаций нет), `entity.Stages()`,
  `entity.WorkingStates()`, поле `recordRepo` в `TelegramController`, интерфейс `Worker`
  с единственной реализацией.
- **`docs/conventions/logging.md:103` называет поле `job_id`, код перешёл на
  `record_id`.** Записанная конвенция разошлась с кодом — дрейф документа.
- **`design.md` изменения называет ключи `own_work_limit`/`foreign_work_limit`, код и
  канон — `own_work_limit_minutes`/`foreign_work_limit_minutes`.** Дрейф документа,
  дом — `av-dev:doc-healthcheck`.
- **Словарь «job» пережил понятие** (`JobNotFoundError`, `CreateJobFromApi`,
  `TranscribeHandler.CreateTranscribeJob`). **Выброшено как вкусовщина**, а не отложено:
  поведения не меняет, стоимости следующего изменения не меняет заметно, записанной
  конвенции не нарушает; публичные имена полей API трогать всё равно нельзя. Названо,
  чтобы не всплыло третьим прогоном как новое.

---

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

**План: темы, дома, глубины.** requirements (`openspec/specs/` + дельты, разбор),
autotests (`CLAUDE.md` «Гейт», глубины нет), conventions (`docs/conventions/`, разбор),
architecture (`docs/architecture.md` + `passport.md`, доказательство), security
(`docs/security.md`, доказательство), operations (`docs/architecture.md`
«Эксплуатация» + `database.md`, доказательство), темы проекта — **дома нет**.

**Что запускалось.** Шесть проходов на метке `large`, режим «по графу»: specs,
autotests, code, architecture, adversary, ops. **Не запускался** `basics` — своих тем
у проекта нет, все документы `docs/` разошлись по шести темам ядра; это решение плана,
а не отказ прогона. Пятая строка про метку `small` неприменима: метка `large`, дома
`security`, `operations` и `architecture` открывались.

**Что не мог проверить каждый проход.** Уставы проходов мне дословно не переданы —
границы ниже выведены из их же выводов, и это деградация строкой: `autotests` судит
наличие и способность проверок падать, но не правильность самой нормы; `specs` судит
код против дельт и молчит там, где дельта молчит (раздел «поведение вне спеки» — ровно
этот остаток); `code` читает и не запускает боевых сценариев; `architecture` судит
форму решения и не мерит; `adversary` строит пути в границах прогона — настоящих
Telegram, SpeechKit и Object Storage в прогоне нет; `ops` мерит на своей машине и
одном каталоге данных, а не на сервере.

**Что осталось целиком на человеке** (`docs/review.md`, «Недоступно проверке»; списки
раздельные и не сливаются).

*Не проверит ни один проход:*

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

*Перестали проверять сознательно:*

- `autotests`: разбор вывода настоящего `ffprobe` — длительность даёт подставной
  источник (ADR-2026-08-11-stub-adapters-in-tests);
- работа сервиса с настоящими внешними собеседниками: живой прогон отвечает за подъём,
  отказ старта, маршруты и остановку; приём из Telegram, расшифровку и заливку он не
  проверяет — боевым токеном запускаться запрещено, ключи Yandex выдуманы,
  распознавание подменяется в коде.

Сверх этого никем не проверено: история инцидентов этого сервиса, поведение под
реальным потоком, поведение внешних систем в их сегодняшних версиях, завязка
потребителей на текущее поведение и вопрос «а нужна ли эта функциональность вообще».

**Каких документов не хватило** — строкой на каждый, с причиной:

- `docs/security.md` **есть, но не тронут этим изменением**: периметр в нём описан по
  прежней модели (задачи и файлы), про шесть коллекций и вторую раскладку файла на
  диске он не знает. Проход `adversary` судил новый периметр по старому документу;
- `design.md` изменения расходится с каноном и кодом по именам ключей конфига —
  документ был, но как источник имён недостоверен;
- `docs/conventions/database.md` даёт изъятие только для `state`: как объявлять новые
  `select`-перечисления, не сказано ни в одну сторону, и проход `code` судил по
  умолчанию;
- дельта `pipeline` не решает, какие отказы приговор, а какие повтор, — из-за этого
  самая весомая гипотеза осталась гипотезой;
- `docs/review.md`, «Типовые ложноположительные», **был и использован**; раздел не
  пуст, отсев шёл не вслепую.

**Сработавшие потолки — по проходу.** `code` объявил свой: конвенций 4 из 4, потолок
сработал ровно, что осталось за срезом — не названо. `architecture` объявил: 3 находки
плюс секция «дешевле переделать до мерджа». `autotests` (3), `specs` (6 + 9), `ops`
(5 + ответы на 9 вопросов) и `adversary` (2 пути + 1 свойство) **своего потолка не
сообщили** — то есть «находок больше нет» у них неотличимо от «больше не поместилось».
Это ровно то, что записано в `docs/review.md` от 2026-08-13, и класс всплыл снова.

**Потолок триажа.** В первые две секции не влезло семь причин; все они выписаны в
«Урожай» и «Гипотезы» поимённо, молча не выброшено ничего. Одна выброшена как
вкусовщина и названа там же.

**Четыре строки, которые не принёс ни один проход:**

1. **Решения проекта не сверялись.** `docs/adr/` — процессный документ, прогон его не
   открывает. Расхождение изменения с записанным решением (в том числе с
   ADR-2026-08-11 про архив и ADR-2026-08-12 про сессию) ловит скилл
   `av-dev:doc-healthcheck`, а не ревью.
2. **Записанные наблюдения проекта не использовались.** `docs/research/` не
   открывался. Всякое число в этом отчёте снято проходом на этом прогоне или прочитано
   в коде и каноне со ссылкой на строку.
3. **Поимённая сверка с руководствами по стилю Go не задавалась ни одним проходом.**
   Различение «идиоматично против распространено» не спрашивал никто.
4. **Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.**
   «Не знаю, чего не знаю» здесь не достаёт никто — на изменении с меткой «незнакомое»
   это самый дорогой пробел прогона.

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