- audiorecords вместо transcribe_jobs: приложения (texts, structures, recognitions, record_events, topics) живут своими коллекциями, ссылки на исходник и на приведённую копию перестали переставляться - рубеж называет достигнутое, отказ стал признаком остановки с причиной, а сторожей стало двое: число отказов и время в рубеже - воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
43 KiB
Триаж ревью: record-centric-model
Сводка
- Режим прогона: по графу. Метка:
large, обоснование разметки — «крупное × незнакомое» (смена модели очереди: захват, повторы и воркеры разом; конвейер задач трогается целиком). Размер и сложность числом в переданном плане не названы; свой замер объёма — 61 путь в рабочем дереве (git status --short | wc -l), изменение лежит некоммитнутым поверхd079f03. - Состояние гейта: зелёный,
task gateexit 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=false1→2,error=true0→0; halt поstuck—error=true0→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/Start0.0%,encodeResponses/decodeResponses/outcomeFromResponses0.0%,GetTranscribeJobStatus52.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, и класс всплыл снова.
Потолок триажа. В первые две секции не влезло семь причин; все они выписаны в «Урожай» и «Гипотезы» поимённо, молча не выброшено ничего. Одна выброшена как вкусовщина и названа там же.
Четыре строки, которые не принёс ни один проход:
- Решения проекта не сверялись.
docs/adr/— процессный документ, прогон его не открывает. Расхождение изменения с записанным решением (в том числе с ADR-2026-08-11 про архив и ADR-2026-08-12 про сессию) ловит скиллav-dev:doc-healthcheck, а не ревью. - Записанные наблюдения проекта не использовались.
docs/research/не открывался. Всякое число в этом отчёте снято проходом на этом прогоне или прочитано в коде и каноне со ссылкой на строку. - Поимённая сверка с руководствами по стилю Go не задавалась ни одним проходом. Различение «идиоматично против распространено» не спрашивал никто.
- Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет. «Не знаю, чего не знаю» здесь не достаёт никто — на изменении с меткой «незнакомое» это самый дорогой пробел прогона.
Формулировка «критичных проблем не обнаружено» к этому отчёту неприменима: critical
в нём нет потому, что ни одна находка не получила оракула, поднимающего её до
нарушения инварианта необратимого класса, — а не потому, что таких свойств не искали
и не нашли.