Files
transcriber/openspec/changes/archive/2026-08-14-record-centric-model/review/triage.md
T
av 1576d06735 внутренняя модель перестроена вокруг аудиозаписи
- audiorecords вместо transcribe_jobs: приложения (texts, structures,
  recognitions, record_events, topics) живут своими коллекциями, ссылки на
  исходник и на приведённую копию перестали переставляться
- рубеж называет достигнутое, отказ стал признаком остановки с причиной, а
  сторожей стало двое: число отказов и время в рубеже
- воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг
  выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
2026-08-14 20:20:33 +03:00

43 KiB
Raw Blame History

Триаж ревью: 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
  • Оракул: зонд прохода specsproto.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 по stuckerror=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.mdNoopJobError — не ошибка», 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
  • Оракул: проход architecturegrep по рабочим путям: колонка пишется и не читается ни одним, адрес объекта пересчитывается 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 в нём нет потому, что ни одна находка не получила оракула, поднимающего её до нарушения инварианта необратимого класса, — а не потому, что таких свойств не искали и не нашли.