- убраны клиент бота, транспорт обновлений, отправитель сообщений, сборка входа при старте, секция настроек и зависимость go-telegram-bot-api; из конвейера ушла доставка ответа отправителю — исход виден опросом готовности. Колонки адресата и значение источника остались в схеме: применённые шаги не переписываются - шаг 202608140003 запрещает пустого владельца у аудиозаписи и у файла; существующие строки он не проверяет, и это принято сознательно — искать их надо запросом до выкладки - ревью нашло два пред-существующих дефекта, оба закрыты: пустой второй ответ распознавателя стирал сохранённую расшифровку, а пустая расшифровка перестала быть заметной вместе с убранной доставкой. Попутно поднят golang.org/x/image до v0.45.0 — красный шаг vulns, воспроизводился и на чистом master
18 KiB
Ревью remove-telegram-intake — триаж
Сводка
- Режим: по графу. Метка:
large(разметкаreview-scope). 50 файлов, −2394 строки, семь удалённых файлов кода, новый шаг схемы; необратимый элемент — шаг схемы, переписаны инвариантыCLAUDE.md. - База диффа:
origin/masterотстала на два закрытых изменения; сверка шла противHEADплюс незакоммиченное рабочее дерево. - Гейт: зелёный целиком (
task gate→ 0). - Сигнал о заниженной метке:
review-codeвозражений не подал.review-basicsне запускался — второго независимого голоса о метке нет. - На вход: 22 находки (specs 5, code 8, architecture 3, ops 2, adversary 4, autotests 0) плюс 4 замечания. После дедупликации по причине — 11 различных причин, одна выброшена как ошибочная.
План с исходом по каждой теме
| тема | дом | глубина | кто закрывает | исход |
|---|---|---|---|---|
| requirements | openspec/specs/{intake,pipeline,access,storage} + дельты |
разбор | specs | закрыта, 5 находок |
| autotests | CLAUDE.md «Гейт»/«Команды» |
— | autotests | закрыта, 0 находок + замечание о дрейфе go-linters.md |
| conventions + техника | docs/conventions/ |
разбор | code | закрыта, 7 находок + 1 ошибочная |
| architecture | docs/architecture.md (+ passport.md) |
доказательство | architecture | закрыта, 3 находки |
| security | docs/security.md |
доказательство | adversary | закрыта, 4 находки + 3 свойства без пути |
| operations | docs/architecture.md «Эксплуатация» (+ database.md) |
доказательство | ops | закрыта, 2 находки |
Тем без отчёта нет. Тем без дома нет. basics не запускался — своих тем сверх ядра план ему не дал.
Блокирует мердж
Ничья запись переживёт новый шаг схемы и станет незакрываемой
-
Файл:
internal/adapter/repo/pocketbase/migrations/202608140003_owner_required.go:20-30;openspec/changes/remove-telegram-intake/design.md, «Migration Plan», п. 1 -
Severity: major. Confidence: high
-
Оракул (переснят триажем,
go test ./internal/adapter/repo/pocketbase/ -run TestTriageProbeOwnerRequiredOverOwnerlessRow):PRE-STEP: ничья запись заведена, owner="" STEP 003 (Required=true) поверх ничьей записи: err=<nil> ПОСЛЕ ШАГА: строка на месте, owner="" FindAndAcquire: выдана запись Save остановленной ничьей записи: err=failed to update audio record: owner: cannot be blank. -
Последствие:
Requiredу поля связи PocketBase — проверка при сохранении записи, а не ограничение таблицы. Шаг проходит зелёным на базе с ничьей записью, и запись остаётся. Дальше она выдаётся воркеру (захват идёт сырым запросом мимо валидации), любое сохранение падает,halt()перехватывает отказ до строк, растящихWorkerJobCounterи пишущихrecord_events. Запись не останавливается, берётся снова по истечении срока захвата и повторяется неограниченно: ни метрики, ни журнала событий, только строка в логе контейнера. Предвыкладочная проверка, на которой стоит безопасность шага, не отличает чистую базу от грязной. -
Найдено проходами: specs, code, ops (три прохода, одна причина).
-
Действие: развилка. Три варианта: (1) шаг сам считает ничьи строки и отказывается; (2) шаг остаётся, утверждение «падает на живой базе» уходит из комментария и плана, а в порядок выкладки добавляется ручная проверка запросом; (3) шаг приводит данные — необратимо, решение владельца обязательно.
Второй ответ провайдера, приехавший пустым, стирает сохранённую расшифровку
- Файл:
internal/service/transcribe.go:716-733(poll→storeOutcome),internal/adapter/repo/pocketbase/text_repo.go:40-53 - Severity: critical. Confidence: high
- Оракул: падающий тест, переснят триажем —
expected: "Личный разговор." actual: "". - Последствие: поток gRPC SpeechKit, закрывшийся на первом
Recv, отказом не считается —outcomeпуст,err == nil.Texts.Putкладёт пустое поверх сохранённого безусловно, рубеж двигается, запись доходит доdoneбез текста. Сырой ответ уцелеет (Finishпишет вложение под условиемlen(raw) > 0), ноReadRaw/Parseв боевом коде не зовёт никто. - Регрессией этого изменения не является:
poll,storeOutcomeиtext_repo.goдиффом не тронуты. Изменение сняло последний видимый признак вырожденного ответа — заглушку «на записи нет текста», — но она уходила только в Telegram. - Найдено проходом: adversary.
- Действие: развилка. (1)
storeOutcomeне пишет пустое поверх непустого; (2) вырожденный ответ считается отказом шага; (3) задача, мердж как есть.
Стоит исправить сейчас
Пустая расшифровка перестала замечаться, а два документа обещают, что она замечена
- Файл:
internal/service/transcribe.go(finish),docs/conventions/logging.md:62,docs/architecture.md:142 - Severity: major. Confidence: high
- Оракул:
logging.md:62называет «пустой текст распознавания» поимённым примером уровняWARN;architecture.md:142утверждал «запись завершается заглушкой „на записи нет текста"». Заглушку изменение убрало вместе с доставкой, замены не было. - Действие: инлайн. Исправлено:
pollпишетWARNс идентификатором записи при пустом результате; строкаarchitecture.md:142переписана на фактическое поведение; заведён оракулTestEmptyRecognitionIsNamedInJournal.
Приёмка переписанного инварианта «остановленная запись несёт причину» не может упасть
- Файл:
internal/service/pipeline_test.go - Severity: minor. Confidence: high
- Оракул:
Halt()ставитHaltedAtиHaltReasonодним движением,IsHalted()читаетHaltedAt— значитrequire.NotNil(HaltReason)следует изrequire.True(IsHalted()). Независимый сигнал (sender.sent()) убран вместе с доставкой. - Действие: инлайн. Исправлено: вторым утверждением взята строка журнала событий — отдельное сохранение, способное упасть само по себе; добавлена третья причина остановки.
Удаление входа не доведено: подавление линтера, конвенции, фикстуры и комментарии
- Файл:
.golangci.yml:158;docs/conventions/go-linters.md:81,92,155;internal/contract/error.go;internal/config/config_test.go;internal/service/transcribe.go;internal/contract/repository.go;internal/metrics/format_label.go;internal/service/ownership_test.go;openspec/specs/pipeline/spec.md:104 - Severity: minor. Confidence: high
- Последствие: подавление
errcheckпо мёртвому символу — заряженная мина: вход убран временно, методsendвернётся под тем же именем и молча окажется без проверки отказов. Остальное — документы и комментарии, обещающие ответ отправителю, включая нормативный докладcontract/repository.go. - Найдено проходами: specs, code, architecture, autotests (одна причина, четыре прохода).
- Действие: инлайн. Исправлено целиком: снято подавление и строки про
send;controller/tgубран из таблицы архправил; удалёнErrDeliveryChannelDown; переписаны комментарии; фикстуры переведены с мёртвой секции; требование «Захват задачи неделим» внесено в дельту.
Анонимный запрос кладёт до мегабайта своего текста в журнал контейнера одной строкой
- Файл:
main.go:184-202 - Severity: major. Confidence: high
- Оракул: живой прогон — 900000 байт в пути → 404, журнал вырос с 1048 до 901194 байт.
- Последствие: длину строки журнала задаёт неузнанный посетитель; разбор инцидента по журналу
становится невозможен. Тот же класс назван недопустимым в
internal/controller/http/auth.go:277-284. - Дефект пред-существующий:
main.goэтим изменением правится только в части подъёма входов. - Действие: развилка. Чинить здесь или заводить задачей.
Гипотезы без доказательства
transcribed— лишний рубеж (architecture): готовая расшифровка платит отдельный захват и попадает под сторож застревания за проход, переставляющий одну колонку. Оракула, что это случалось, нет. Вопрос владельцу — место ему в задаче.- Отказ приёма по пустому владельцу приходит новому вызывающему как 500 (specs): путь сегодня недостижим, транспорт отвергает раньше.
http.status_code=0у всякого отвергнутого запроса (adversary,main.go:194-199). Пред-существующее.- Хвост имени отправителя уезжает в журнал внутри поля
error(adversary): инвариант приватности не нарушен, нарушена форма изъятия. Пред-существующее. - Три свойства без построенного пути (adversary): длительность из метаданных ничем не ограничена
(переполнение
int); непустой, но более короткий ответ провайдера затирает и вложение;POST /api/collections/users/request-verificationоткрыт анониму.
Выброшено как ошибочное: находка code о gofmt на pipeline_test.go — gofmt -l . пуст,
проверено дважды.
Выброшено как вкусовщина: переименование CreateJobFromApi.
Проектные ложноположительные: строка «Запись без владельца не достаётся никому» в docs/review.md
отменена дважды, последний раз этой же задачей — находка о ничьей записи ей подтверждается, а не
отсеивается.
Promote candidates
- В
docs/conventions/logging.md: длину поля журнала не задаёт вызывающий — всё, что приходит извне, уезжает в журнал усечённым до объявленного предела. Правила нет, случай второй. - Отклонённый кандидат: страж существования символов в
exclude-functions— заводить нельзя,CLAUDE.md«Проверок над проверками не заводить» запрещает стражей предмета у правил.
Границы покрытия
Что запускалось. Шесть проходов на метке large, режим по графу: specs, autotests, code,
architecture, adversary, ops. basics не запускался — план не дал ему тем сверх ядра. Корректор метки
(review-code) отработал, возражений не подал; второго корректора в прогоне не было.
Потолки проходов. Ни один проход не сообщил свой потолок и что осталось за срезом, хотя контракт
обязывает. Неизвестно, показал ли code все находки или только верхние.
Что не влезло в потолок триажа (названо, а не выброшено):
- откат образа после вычистки секции
[telegram]из боевого конфига роняет старт прежнего бинаря (ops, оракул — прогон исторического бинаря9a964f2); - ряд метрики
intake_up{telegram}исчезает, а не обнуляется — цена не названа в документах владельца; down202608140003не покрыт (0.0%) — так у всех четырёх шаговdown*, и они операционно недостижимы: cobra-команды PocketBase не подключены;- частичное покрытие двух требований дельты: «Поднятые входы видны наблюдателю» (оракул только живой) и «Приведённая копия получает владельца записи» (косвенно).
Что осталось целиком на человеке — из docs/review.md, «Недоступно проверке»: поведение внешних
сервисов под нагрузкой и на границах, реальный профиль нагрузки, стойкость ffmpeg к вредоносному
входу, поведение настоящей Authelia, поведение браузера с куками. Сознательно перестали проверять:
разбор вывода настоящего ffprobe; работа с настоящими SpeechKit и Object Storage; вход через живого
провайдера OIDC.
Четыре строки, которые в конвейере не закрывает никто:
- Решения проекта не сверялись —
docs/adr/процессный, расхождение ловитav-dev:doc-healthcheck. - Записанные наблюдения (
docs/research/) не использовались; всякое число снято на этом прогоне. - Поимённая сверка с руководствами по стилю Go не задавалась ни одним проходом.
- Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.
Чем работал триаж. Две пробы на копии дерева в скретчпаде, базы — временные каталоги. Боевых данных, боевых ключей и выкладки не касался.
Формулировки «критичных проблем не обнаружено» в отчёте нет: одна critical подтверждена падающим
тестом и живёт в сервисе прямо сейчас.