внутренняя модель перестроена вокруг аудиозаписи
- audiorecords вместо transcribe_jobs: приложения (texts, structures, recognitions, record_events, topics) живут своими коллекциями, ссылки на исходник и на приведённую копию перестали переставляться - рубеж называет достигнутое, отказ стал признаком остановки с причиной, а сторожей стало двое: число отказов и время в рубеже - воркеры потеряли специализацию, их число задаётся [pipeline] workers, шаг выбирается по рубежу, а захват отдаёт идентификатор и признак захвата
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
# Триаж ревью: 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`
|
||||
в нём нет потому, что ни одна находка не получила оракула, поднимающего её до
|
||||
нарушения инварианта необратимого класса, — а не потому, что таких свойств не искали
|
||||
и не нашли.
|
||||
Reference in New Issue
Block a user