# Триаж ревью: 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//<запись>/<имя>.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` в нём нет потому, что ни одна находка не получила оракула, поднимающего её до нарушения инварианта необратимого класса, — а не потому, что таких свойств не искали и не нашли.