# Триаж ревью кода: `no-user-filename-in-log` Документ ведётся по кругам. Наверху — последний круг, ниже приложением — отчёт первого круга целиком, как он был написан. История не переписывается: исход каждой находки первого круга проставлен отдельным разделом, а не правкой её текста. --- # Круг 2 (после контрольной точки о метке метрики) ## Сводка - **Change:** `no-user-filename-in-log`. База диффа `origin/master`, судится рабочее дерево (`git diff`) плюс три файла не под git: `internal/metrics/format_label.go`, `internal/metrics/format_label_test.go`, `internal/service/format_label_test.go`. Итого 5 изменённых файлов (+258/−17) и 3 новых. - **Размер:** среднее. **Сложность:** знакомое. **Метка: medium** — максимум по осям, не менялась между кругами. Второй круг добавил файл в `internal/metrics`, но проектный триггер «новая метрика в `internal/metrics`» (`docs/review.md`, «Триггеры метки») опускает до `small` именно новую **метрику**; здесь новой метрики нет, добавлено приведение значения метки. Метка остаётся `medium`. - **Режим прогона:** по графу. Второй круг: `specs`, `code`, `basics`, `triage`. - **Гейт:** красный **только объявленным долгом**. Проверено мной самим: `go build ./...`, `go vet ./...`, `gofmt -l .`, `go test ./...` зелёные; `golangci-lint run` даёт ровно 4 знакомых замечания (`speechkit.go:55`, `main.go:124`, `worker.go:51`, `transcribe.go:395`); `openspec validate no-user-filename-in-log --strict` — `is valid`. `go test -race -count=5 ./internal/controller/http/` — `ok, 1.663s`. Новых красных шагов нет. - **Находок на входе второго круга:** 15 (specs 6, basics 3, code 6) + 1 кандидат в правила. За два круга — 32. **После дедупликации по причине и отсева:** 3 в первых двух секциях (1 блокирующая, 2 к исправлению), 3 гипотезы, 5 кандидатов в правила. ### Находка о прогоне: `autotests` на втором круге не запускался Второй круг прогнал `specs`, `code` и `basics`. Проход `autotests` — **нет**, и это его тема: круг добавил 130 строк в `internal/controller/http/transcribe_test.go` и два новых тестовых файла целиком. Дом темы (`CLAUDE.md`, «Гейт» и «Команды») второй круг не открывал никто. Это не абстрактный пробел: **блокирующая находка №1 ниже — ровно тот класс, который ищет `autotests`**, и нашёл её я мутацией, а не проход. Судить, сколько ещё такого осталось в добавленных проверках, нечем: я не читаю код в поисках дефектов, я проверяю чужие выводы, а выводов по теме `autotests` за второй круг не поступало. ### Сигнал о заниженной метке - **Круг 1:** `review-code` пришёл и метку заниженной **не** считает. От `review-basics` сигнала не было — ни «верна», ни «занижена». - **Круг 2:** сигнала не пришло **ни от одного** прохода — ни от `review-code`, ни от `review-basics`. Отличить «возражений нет» от «не сказано» по молчанию нельзя, и я этого не делаю. ### План разметки против исхода — оба круга План не менялся. Своих тем проекта сверх ядра нет, тем без дома нет. | тема | дом | глубина | закрывает | круг 1 | круг 2 | | --- | --- | --- | --- | --- | --- | | requirements | `openspec/specs/intake/spec.md` + дельта change | разбор | specs | **закрыта**, 4 находки | **закрыта**, 6 находок | | autotests | `CLAUDE.md`, «Гейт» и «Команды» | — | autotests | **закрыта**, 0 находок + 1 promote | **отчёта не пришло — проход не запускался** | | conventions | `docs/conventions/logging.md` + весь каталог | разбор | code | **закрыта**, 6 находок | **закрыта**, 6 находок | | architecture | `docs/architecture.md`, «Единые точки проекта» + `docs/passport.md` | разбор | basics | **закрыта**, ответ по вопросу темы + 1 находка | **закрыта**, 1 находка (дом правила) | | security | `docs/security.md`, «Куда уходит содержимое записи», «Из чего строятся пути и ключи» | разбор | basics | **закрыта**, 1 находка (главная) | **закрыта**, подтверждение: канал наружу перебран поимённо | | operations | `docs/architecture.md`, «Эксплуатация» + `docs/database.md` | разбор | basics | **закрыта**, ответы по вопросам темы | **закрыта**, 2 находки (умолчание, видимость настоящего формата) | --- ## Блокирует мердж ### 1. Второй употребитель приведения метки оракулом не закрыт: снятие `FormatLabel` на шаге конвертации не роняет ничего - **Где:** `internal/service/transcribe.go:212` — `WithLabelValues(metrics.FormatLabel(srcExt), "ogg", strconv.FormatBool(err != nil))`. Критерий `tasks.md` 2а.2 («Обе метки, несущие расширение, идут через приведение») отмечен выполненным. - **Severity:** major. Не критично тем, что код **сегодня верен**: приведение на месте, хвост наружу не идёт. Критично то, что удержано оно ничем, а чек-лист утверждает обратное. - **Оракул (мутация на копии дерева, прогнана на этом прогоне):** ``` # копия дерева, sed по строке 212: FormatLabel(srcExt) -> srcExt go test ./internal/... MUT[M2 приведение снято у метки конвертации]: ЗЕЛЁНАЯ (мутация не поймана) ``` Для сравнения — тот же приём на первом употребителе краснеет: ``` MUT[M1 приведение снято у метки размера приёма]: КРАСНЕЕТ --- FAIL: TestCreateTranscribeJob_MetricLabelCarriesNoSenderName ``` - **Что именно уходит этой меткой:** `srcExt` строится из `srcFile.FileName` (`transcribe.go:195`), а это имя файла в хранилище вида `.<хвост имени отправителя>`. То есть в метку `source_format` метрики `transcriber_conversion_duration_seconds` попадает тот же приватный хвост, что и в метку приёма, и уезжает он на тот же анонимный `GET /metrics` (`main.go:216`). Канал по свойствам идентичен закрытому. - **Почему это блокирует, а не «стоит исправить»:** `design.md` этого change сам объявил такую проверку недостаточной — дословно: «Проверка функции в отрыве от точек употребления зелёная и при снятом приведении: правка места вызова вернула бы хвост наружу молча». Решение принято, реализовано для одной точки из двух, а чек-лист отмечает обе. Это записанное свойство проекта — `docs/review.md`, «Типовые узлы / Любой узел»: «проверка **способна упасть**… Признак ищется мутацией», и журнальная запись 2026-08-11 об этом же классе. Ранжирую первым по правилу молчания: следующий, кто уберёт `FormatLabel` со строки 212 как лишний вызов, сузит оракул критического инварианта, и никто не узнает. - **Чего эта находка НЕ утверждает:** «`/metrics` открыт без аутентификации» — типовое ложноположительное проекта (`docs/review.md`, «Типовые ложноположительные», пункт про HTTP API). Новое здесь не открытость, а неудержанность приведения на второй точке. - **Действие: развилка.** > Приведение расширения к перечню стоит в двух местах, а оракул — только на > одном. Мутация на втором (`transcribe.go:212`, метка `source_format` шага > конвертации) зелёная: снятие приведения не роняет ни одной проверки. > Своего тестового окружения у `internal/service` нет вовсе — есть только > `format_label_test.go` на 19 строк. Что делаем до мерджа? > > **(а)** Мержим как есть: пробел записываем строкой в `design.md` разделом > Risks и вешаем на существующую задачу `tasks/items/pipeline-step-tests.md` > — она заведена ровно про непокрытые `FindAndRunConversionJob` и соседей. > Чек-лист `tasks.md` 2а.2 при этом надо переформулировать: «приведение стоит > на обеих точках, оракул — на одной». > **(б)** Строим окружение проверки для `FindAndRunConversionJob`: репозитории > SQLite, каталог хранения, подставной конвертер. Цена сопоставима с самой > задачей и вылезает за её scope. > **(в)** Убираем возможность обойти приведение по построению: прячем > `InputFileSizeHistogram` и `ConversionDurationHistogram` за функциями > `internal/metrics`, которые сами зовут `FormatLabel`. Тогда сырое расширение > передать меткой нечем, и обе точки закрывает уже существующая проверка > реестра. Меняется экспортируемая поверхность пакета `internal/metrics` — > пакет внутренний, публичного контракта это не трогает, но это правка сверх > заказанного объёма. --- ## Стоит исправить сейчас ### 2. Комментарий оракула ссылается на механизм, которого в коде нет - **Где:** `internal/controller/http/transcribe_test.go:492` — «снятие `log.SetOutput` из окружения оставило бы проверку зелёной». Тот же текст в `tasks.md`, пункт 2.1: «в него же перенаправляется вывод стандартного `log`». - **Оракул:** `grep -n "log.SetOutput" internal/controller/http/transcribe_test.go` отдаёт единственное совпадение — саму эту строку комментария. В окружении стоит `slog.SetDefault(logger)` (`:167`), как в `main.go:50`. Механизм заменён в этом же круге по находке прохода `code` — комментарий и чек-лист за правкой не поехали. - **Почему сейчас:** причина ровно та же, что у находки №3 первого круга (`src_ext`, метки не существует), и лечится так же — одним словом. Проза, называющая несуществующий механизм, сбивает следующего: он не найдёт `log.SetOutput`, решит, что утверждение мёртвое, и снимет его. Утверждение живое — мутация подтверждает: ``` MUT[M4 slog.SetDefault снят]: КРАСНЕЕТ --- FAIL: TestCreateTranscribeJob_SenderFileNameNotLoggedOnFailure ``` - **Действие: инлайн.** В обоих местах заменить `log.SetOutput` на `slog.SetDefault` и уточнить формулировку: вывод стандартного `log` уходит в буфер не перенаправлением, а тем, что `slog.SetDefault` сажает его в `msg` записи `slog`. ### 3. Значение `other` нормировано спекой дословно, но ни одна проверка к литералу не привязана - **Где:** `internal/metrics/format_label.go:6` — `const OtherFormatLabel = "other"`; дельта-спека, требование «Метка метрики несёт только известное расширение»: «всякое другое MUST заменяться единым значением `other`». - **Оракул (мутация, прогнана):** ``` # const OtherFormatLabel = "other" -> const OtherFormatLabel = "" MUT[M11 общее значение пустое]: ЗЕЛЁНАЯ (мутация не поймана) ``` Обе проверки — и `TestFormatLabel`, и `TestCreateTranscribeJob_MetricLabelCarriesNoSenderName` — сверяются с самой константой, то есть утверждают тавтологию. Значение метки — величина, которую видит человек в панели снаружи; спека называет её дословно, а код может сменить её молча, включая на пустую строку, при которой метка из выборок Prometheus фактически исчезает. - **Почему сейчас, а не в гипотезы:** это не догадка, а прогнанная мутация, и закрывается одной строкой в `internal/metrics/format_label_test.go`: `if OtherFormatLabel != "other" { t.Fatal(...) }` — либо заменой `want: OtherFormatLabel` на `want: "other"` в трёх случаях таблицы. - **Действие: инлайн.** --- ## Гипотезы без доказательства - **Запрет `t.Parallel()` держится комментарием** (перенесено с первого круга и остаётся верным; механизм только сменился). Теперь процессная подмена — это `slog.SetDefault`, и ничто, кроме текста рядом, не мешает следующей проверке в этом файле стать параллельной; тогда перехват уедет к соседу. Оракула нет: воспроизвести можно только внесением `t.Parallel()`, то есть той самой будущей правкой. `Confidence: medium`, severity не выше `minor`. - **Позитивное утверждение `require.Contains(journal, msgTransportErr)` привязано к расхождению, которое конвенция объявляет подлежащим устранению** (`log.Printf` в HTTP-транспорте мимо `slog`). Когда расхождение починят, проверка отказа покраснеет по причине, не связанной с приватностью. Вынос текста в константу с комментарием про долг (сделан вторым кругом) снижает цену правки, но событие в будущем оракулом не закрывает. `minor`. - **Приём из Telegram своего оракула не получил ни на одном круге.** Правка достаётся ему общим шагом `createTranscribeJob`, и обойти её со стороны Telegram нечем — но это рассуждение по коду, а не прогон. Объявлено риском в `design.md`. `minor`. --- ## Promote candidates - **Правило линтера на ключи `file_name`/`filename` в вызовах логгера** (`sloglint` либо `forbidigo`). Пришло от `autotests` на первом круге; `design.md` объявил это non-goal задачи. - **Свойство в `docs/review.md`, «Типовые узлы / Любой узел»:** оракул ставится **на каждой точке употребления**, а не на самой функции и не на одной из точек. Обобщение трёх находок подряд — неудержанного middleware (круг 1, №2), неудержанного `log`-потока (круг 1, починено) и неудержанной точки конвертации (круг 2, №1). Три повторения одного класса за один change — заявка на записанное правило, а не на разовую починку. - **Конвенции о метриках в проекте нет вовсе.** `docs/conventions/` — пять файлов: `config`, `database`, `errors`, `logging`, `web-ui`. Что можно класть в метку, кто отвечает за кардинальность, как объявлять смену формы значения — дома нет. Второй круг завёл строку в `docs/architecture.md`, «Единые точки проекта», но это указатель на реализацию, а не правило. Пришло от `code`. - **Нормализация расширения, взятого из имени отправителя, в журнале и в имени файла на диске** — отдельной задачей, с явным решением человека про раскладку `data/files` (`CLAUDE.md` называет её необратимой). Остаток записан прозой в `docs/security.md`; задачи на него нет. - **Смена формы значения метки (`.mp3` → `mp3`) записана в `docs/security.md`.** Дом спорный: это факт эксплуатации, а не модели угроз, и человек, у которого опустела панель, полезет в `docs/architecture.md`, «Эксплуатация». Не находка — правило о том, где объявляются несовместимые смены формы метрик, входит в предыдущий пункт. --- ## Что из починенного починено верно и не ослаблено Проверено мутациями на копии дерева (`git ls-files` + три файла не под git), не со слов. Базовый прогон копии зелёный, откат подтверждён. | мутация | исход | что этим удержано | | --- | --- | --- | | приведение снято у метки размера приёма | **КРАСНЕЕТ** (`…MetricLabelCarriesNoSenderName`) | круг 2, specs #1 — для точки приёма | | приведение снято у метки конвертации | **ЗЕЛЁНАЯ** | **не удержано — находка №1** | | имя отправителя вернулось под ключом `src` | **КРАСНЕЕТ** (обе проверки запрета) | круг 1: поиск идёт по значению, не по ключу | | `slog.SetDefault` снят из окружения | **КРАСНЕЕТ** (`…NotLoggedOnFailure`) | круг 2, code #4 — боевая цепочка журнала | | middleware не подключён к тестовому роутеру | **КРАСНЕЕТ** (`…SenderFileNameNotLogged`) | круг 1, находка №2 | | строка приёма понижена до `DEBUG` | **КРАСНЕЕТ** (`…JournalTracesRecord`) | круг 1: адресат записи | | умолчание сервиса выведено из перечня | **КРАСНЕЕТ** (`TestDefaultAudioExtIsKnownToMetrics` + `…DifferentFileExtensions`) | круг 2, basics #1 — дубль умолчания | | вторая строка приёма в журнале | **КРАСНЕЕТ** (`…JournalTracesRecord`) | круг 1, находка №4 — «ровно одна запись» | | приведение регистра снято | **КРАСНЕЕТ** (`TestFormatLabel`) | круг 2, specs #5 | | `oga`, `opus`, `mp4` выведены из перечня | **КРАСНЕЕТ** (`TestFormatLabel`) | круг 2, code #1 — голосовые Telegram | | `OtherFormatLabel` стал пустым | **ЗЕЛЁНАЯ** | **не удержано — находка №3** | Дополнительно проверено поимённо, а не со слов: - **Круг 1, находка №3 закрыта.** `docs/security.md` называет метки `file_extension` у `transcriber_input_file_size_bytes` и `source_format` у `transcriber_conversion_duration_seconds`. Сверено с `internal/metrics/metrics.go:24` и `:44` — совпадает. `grep -rn "src_ext"` по коду и документам совпадений не даёт (остались только упоминания в архиве этого же отчёта). - **Круг 2, specs #4 закрыта.** `design.md` держит решение о метке в Decisions, с записью развилки и двух отвергнутых вариантов (оставить как есть; приводить везде, включая диск). `proposal.md` согласован. - **Круг 2, specs #3 и basics #2 закрыты.** Перечень назван поимённо в требовании спеки (14 форматов + `audio`), дом правила — строка в `docs/architecture.md`, «Единые точки проекта». - **Ослаблений не найдено.** Ни одна проверка первого круга не стала слабее, `go test ./...` зелёный, `go test -race -count=5` зелёный, новых замечаний линтера нет, `openspec validate --strict` — valid. --- ## Что осталось непочиненным и почему Ничего не выброшено молча. Полный список: 1. **Точка конвертации не удержана оракулом** — находка №1, развилка, решение за человеком. Единственное, что блокирует мердж. 2. **Комментарий про `log.SetOutput`** — находка №2, инлайн, одно слово в двух местах. 3. **Литерал `other` не привязан к спеке** — находка №3, инлайн, одна строка. 4. **Остаток «хвост расширения остаётся в журнале» задачей не заведён.** Записан прозой в `docs/security.md` и в Risks `design.md`. Не починено **намеренно**: заведение задач принадлежит скиллу задач, а не конвейеру ревью. Идёт урожаем (Promote candidates, пункт 4). 5. **Конвенции о метриках нет** — урожай, Promote candidates, пункт 3. Не находка об этом коде. 6. **Расширение в имени файла на диске не нормализуется** — решение принято человеком на контрольной точке и записано отвергнутым вариантом в `design.md`. Раскладка `data/files` объявлена необратимой в `CLAUDE.md`. Закрыто как решение, не как пробел. 7. **Сценарий «Известное расширение идёт как есть» назван неточно** — тело сценария (`sample.MP3` → `mp3`) описывает приведение регистра, заголовок говорит «как есть». Выброшено по отсеву вкусовщины: поведения не меняет, на стоимость следующей правки не влияет, записанной конвенции не нарушает — текст требования выше разночтение снимает дословно. Потолок первых двух секций **не срабатывал**: 1 из 3 и 2 из 4. За срезом не осталось ничего. --- ## Границы покрытия ### План Шесть тем, у каждой есть дом и глубина; все шесть перечислены в таблице выше с исходом по каждому кругу. Тем без дома нет. Своих тем проекта сверх ядра нет. **Тема без отчёта одна: `autotests` на втором круге** — проход не запускался, дом темы (`CLAUDE.md`, «Гейт» и «Команды») второй круг не открывал никто, а добавленный кругом код — это на 100% тестовый код. ### Проходы - **Круг 1**, метка `medium`, режим «по графу»: `review-autotests`, `review-specs`, `review-code`, `review-basics` (темы `security`, `operations`, `architecture`, глубина разбор), `review-triage`. - **Круг 2**, та же метка и режим: `review-specs`, `review-code`, `review-basics`, `review-triage`. - **Не запускались:** `review-autotests` на втором круге — причина мне не сообщена (в задании сказано «`specs`, `code`, `basics` прогнаны заново», без обоснования пропуска). Проходы ревью дизайна (`specs`, `rubric` на предложении) отработали до кода и в этот прогон не входят. ### Чего в конвейере нет вовсе Четыре строки, которые не принесёт ни один проход: 1. **Решения проекта не сверялись.** `docs/adr/` — процессный документ, прогон его не открывает. Расхождение изменения с записанным решением ловит сверка документации (`av-dev-docs:healthcheck`), а не ревью. В каталоге лежат четыре ADR, включая `ADR-2026-08-11-stub-adapters-in-tests.md`, — ни один из них этим прогоном не читался как источник требований. 2. **Записанные наблюдения проекта не использовались.** `docs/research/` — тоже процессный. Все числа в этом отчёте сняты на этом прогоне, команды приложены. 3. **Поимённая сверка с руководством по стилю Go не задавалась ни одним проходом.** Различение «идиоматично против распространено» — например, `map[string]struct{}` против `slices.Contains` в `format_label.go`, или уместность тавтологичного сравнения с константой — не спрашивал никто. 4. **Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.** Проход независимой реализации снят по стоимости, а не по замеру; «не знаю, чего не знаю» никто не достаёт. ### Чего запущенные проходы не могли проверить в принципе - Ни один проход не гонял сервис на настоящих данных: `testdata` в проекте нет по запрету `CLAUDE.md`, реальные ключи Yandex Cloud под запретом, боевую БД и `data/files` трогать нельзя. Все оракулы — синтетический вход и мутации. Для находки про внешний формат это существенно: перечень из 14 расширений собран рассуждением о том, что выдаёт Telegram и что берёт `ffmpeg`, и **ни на одном настоящем файле не проверен**. - `specs` судит код против заказанного поведения и не ищет дефектов вне него; `code` судит технику и конвенции и не судит требования; `basics` идёт по трём темам и только по их записанным домам; `autotests` судит прогон и мутации, а не смысл проверок, — и второй круг не судил вовсе. - Приём из Telegram своей проверки не получил ни на одном круге (объявлено риском в `design.md`). - Шаг конвертации (`FindAndRunConversionJob`) не покрыт ни одним тестом вообще — это записано отдельной задачей `tasks/items/pipeline-step-tests.md`, а не находка этого прогона. - Триаж не читает код в поисках дефектов: пропуск любого прохода — мой пропуск тоже. Пропуск `autotests` на втором круге назван поимённо выше. ### Что осталось целиком на человеке Из `docs/review.md`, «Недоступно проверке», двумя отдельными списками — они не сливаются. **Не проверит ни один проход:** - `operations`: поведение внешних сервисов под нагрузкой и на границах — SpeechKit и Object Storage поднять в тесте нечем; - `operations`: реальный профиль нагрузки. Проект живёт на единицах записей в день, и утверждения о росте (в том числе о кардинальности метки) остаются условиями, а не замерами; - `security`: стойкость `ffmpeg` к вредоносному входу — разбор чужого формата отдан внешней программе, и она вне нашей границы. **Перестали проверять сознательно:** - `autotests`: разбор вывода настоящего `ffprobe`. Проверки приёма получают длительность от подставного источника; своего теста у `adapter/metaviewer/ffmpeg` нет. Решение и его цена — `docs/adr/ADR-2026-08-11-stub-adapters-in-tests.md`. **Плюс общее, вне зависимости от проекта:** история инцидентов, поведение под реальным потоком, поведение внешних систем в их версиях, завязка потребителей на текущее поведение и вопрос «а нужна ли эта функциональность вообще». Последнее здесь не пустое: панели и алерты, отобранные по `file_extension=".mp3"`, перестанут пополняться после выкладки, и знает об этом только человек. ### Каких документов проекта не хватило Строка на каждый, с причиной. Слить нельзя — чинится разным. - **`CLAUDE.md`, «Ориентир по размеру порции: не замерялся».** Разметка «инлайн против развилки» опирается на right-size, а мерки right-size в проекте нет. Пометки «инлайн» в находках 2 и 3 поставлены по объёму правки (одно слово, одна строка), и это моё предположение, а не сверка с записанным ориентиром. - **Конвенции о метриках в `docs/conventions/` нет** — каталог есть, файла нет. Правила «что можно класть в метку», «кто отвечает за кардинальность», «как объявляется несовместимая смена формы значения» дома не имеют. Отсюда развилка вместо однозначного вердикта в находке №1 и вкусовой характер вопроса о доме записи про `.mp3` → `mp3`. - **`docs/review.md`, «Типовые ложноположительные» — раздел есть и не пуст**, четыре пункта; применён пункт про открытый HTTP API (дважды: к находке №1 этого круга и к находке №1 первого). Деградации по нему нет. - Прочие нужные документы на месте и использованы: инварианты `CLAUDE.md`, `docs/review.md` целиком, `docs/security.md`, `docs/conventions/logging.md`, `docs/architecture.md`. ### Сработавшие потолки - **Ни один проход ни на одном круге не сообщил свой потолок** — ни сколько находок показал из скольких, ни что осталось за срезом. Это находка о прогоне, повторившаяся во второй раз: судить, полон ли вход триажа, нечем. «15 находок на входе второго круга» может означать «15 из 15», а может «15 из скольких-то». - **Потолок триажа не срабатывал:** 1 из 3 в «Блокирует мердж», 2 из 4 в «Стоит исправить сейчас». Ничего не выброшено из-за потолка. Выброшенное выброшено по дедупликации (basics #1 и code #3 — одна причина, дубль умолчания; specs #1 второго круга и моя находка №1 — одна причина, приведение без оракула на точке употребления) и по отсеву вкусовщины (пункт 7 раздела «Что осталось непочиненным»). ### Метка `medium`, не `small` Строка про `small` к этому прогону неприменима: `basics` отработал на глубине «разбор», дома тем `security`, `operations` и `architecture` открывались на обоих кругах. --- ## Можно ли мержить **Кода, который сейчас неверен, я не нашёл ни на одном круге второго прохода.** Приведение стоит на обеих точках, хвост имени отправителя наружу не выходит, инвариант приватности из `CLAUDE.md` соблюдён, гейт красный только объявленным долгом, `openspec validate --strict` — valid. **Мержить можно после того, как человек закроет развилку №1** — она про то, чем удержано верное поведение, а не про само поведение. Любой из трёх вариантов развилки делает состояние мерджабельным; вариант (а) — самый дешёвый и требует только переформулировать чек-лист `tasks.md` 2а.2, чтобы он не утверждал того, чего нет. Находки №2 и №3 — инлайн, вместе это одно слово в двух местах и одна строка в тесте; мерджу они не мешают, но чинятся дешевле сейчас, чем потом. Формулировка «критичных проблем не обнаружено» к этому прогону применима **только вместе с секцией границ покрытия выше**, и главная её строка — `autotests` на втором круге не запускался, а блокирующую находку нашёл я мутацией, а не проход. --- --- # Приложение: отчёт первого круга, полностью, как был написан > Ниже — текст триажа первого круга без правок. Исходы его находок проставлены > в разделе «Что из починенного починено верно» выше: находка №1 закрыта > решением контрольной точки (приведение метки), находки №2, №3 и №4 починены и > удержаны мутациями. ## Сводка - **Change:** `no-user-filename-in-log`. База диффа `origin/master`, судится рабочее дерево (`git diff`): 4 файла, +172/−15. - **Размер:** среднее. **Сложность:** знакомое. **Метка: medium** — максимум по осям. Разметчик сам назвал спорным неприменение проектного триггера «изменение, трогающее оба входа сразу»: правка лежит в единой точке `createTranscribeJob`, а не отдельной работой по каждому входу. Человек на чекпоинте согласился оставить `medium`. - **Режим прогона:** по графу. Состав ревью кода: `autotests`, `specs`, `code`, `basics`, `triage`. - **Сигнал о заниженной метке:** `review-code` пришёл и метку заниженной **не** считает. От `review-basics` сигнала в переданных мне выводах нет — ни «метка верна», ни «занижена»; отличить «возражений нет» от «не сказано» по молчанию нельзя, и я этого не делаю. - **Гейт:** красный **только объявленным долгом**. Проверено мной самим, не со слов прохода: `go build ./...`, `go vet ./...`, `gofmt -l .`, `go test ./...` зелёные; `golangci-lint run` даёт ровно 4 знакомых замечания (`speechkit.go:55`, `main.go:124`, `worker.go:51`, `transcribe.go:395`). Смещение `394 → 395` внесено самим диффом. Новых красных шагов нет. - **Находок на входе:** 17 (autotests 0 + 1 promote, specs 4, basics 6, code 6). **После дедупликации по причине и отсева:** 4 в первых двух секциях, 3 в гипотезах, 3 кандидата в правила. ### План разметки против исхода | тема | дом | глубина | закрывает | исход | | --- | --- | --- | --- | --- | | requirements | `openspec/specs/intake/spec.md` + дельта change | разбор | specs | **закрыта**, 4 находки | | autotests | `CLAUDE.md`, «Гейт» и «Команды» | — | autotests | **закрыта**, 0 находок + 1 promote | | conventions | `docs/conventions/logging.md` + весь каталог | разбор | code | **закрыта**, 6 находок | | architecture | `docs/architecture.md`, «Единые точки проекта» + `docs/passport.md` | разбор | basics | **закрыта**, ответ по вопросу темы + 1 находка | | security | `docs/security.md`, «Куда уходит содержимое записи», «Из чего строятся пути и ключи» | разбор | basics | **закрыта**, 1 находка (главная) | | operations | `docs/architecture.md`, «Эксплуатация» + `docs/database.md` | разбор | basics | **закрыта**, ответы по вопросам темы | Тем без дома нет. Тем без отчёта нет. Своих тем проекта сверх ядра нет. --- ## Блокирует мердж ### 1. Дельта-спека письменно узаконивает произвольный текст отправителя, а хвост уходит на анонимный `/metrics` - **Где:** `openspec/changes/no-user-filename-in-log/specs/intake/spec.md` («Расширение, взятое из этого имени, запретом не накрыто»); `internal/service/transcribe.go:95` и `:143`; `internal/metrics/metrics.go:24` и `:44`; `main.go:216`. - **Severity:** critical. Инвариант `CLAUDE.md`: «Текст расшифровки, имя файла пользователя и его сообщение в лог не пишутся — только длина и идентификаторы. Нарушение необратимо». - **Причина одна** на три носителя, поэтому это одна находка, а не три: `ext` берётся из недоверенного имени дословно. Её нашли три прохода независимо (`specs` #1, `basics` #1, `code` #1) — приоритет от этого выше, `confidence` нет: под всеми проходами одна модель. - **Оракул (построен и прогнан на этом прогоне):** ``` filepath.Ext("запись.тайное-слово") -> ".тайное-слово" filepath.Ext("Разговор с Петровым 11.08") -> ".08" filepath.Ext("отчёт.для Ивановой") -> ".для Ивановой" ``` Метка Prometheus отдаётся наружу дословно — программа с тем же выражением и тем же `HistogramVec`, что в `metrics.go:24`, на `GET /metrics`: ``` transcriber_input_file_size_bytes_count{file_extension=".тайное-слово"} 1 ``` Путь до этой строки достроен по коду: `header.Filename` (`internal/controller/http/transcribe.go:43`) → `CreateJobFromApi` → `createTranscribeJob` → `ext` (`:95`) → `WithLabelValues(ext)` (`:143`); `router.GET("/metrics", gin.WrapH(promhttp.Handler()))` (`main.go:216`) стоит за `sloggin` и `gin.Recovery` и ни за какой проверкой. - **Чего эта находка НЕ утверждает:** «HTTP API открыт без аутентификации» — типовое ложноположительное проекта (`docs/review.md`, «Типовые ложноположительные»), и открытость `/metrics` записана в `docs/security.md` строкой «Метрики и здоровье». Новое здесь не открытость, а то, что на эту открытую поверхность попадает значение, которым распоряжается анонимный отправитель, — и что дельта-спека это разрешает текстом. - **Почему первое место в ранжировании:** ущерб необратим по букве инварианта (строка уехала в собранные логи и в хранилище метрик), а канал шире того, что задача закрыла: журнал читает владелец, `/metrics` — кто угодно из интернета. Тем же концом это неограниченная кардинальность метрики, и множество значений метки задаёт анонимный отправитель — этот исход вероятнее утечки осмысленного слова и вредит эксплуатации. - **Действие: развилка.** > Хвост после последней точки в имени отправителя уходит дословно в журнал, в > метку `file_extension` и в метку `source_format` на анонимный `/metrics`. > Дельта-спека сейчас пишет это в канон фразой «расширение запретом не > накрыто». Что делаем до мерджа? > > **(а)** Мержим как есть: остаток записан в модель угроз, нормализация уходит > отдельной задачей. Канон при этом получает разрешающую фразу. > **(б)** Мержим, сузив формулировку требования (например: в журнал и в метку > идёт расширение из списка разрешённых, прочее заменяется на `.bin`), и в этой > же задаче нормализуем **только значение метки метрики**. Имя файла на диске > не трогается, раскладка `data/files` не меняется, правка локальна. > **(в)** Нормализуем `ext` целиком в `createTranscribeJob`. Тогда меняется > формат имени файла на диске — `CLAUDE.md` называет это необратимым и > требующим отдельного решения человека, то есть возврата на чекпоинт. ### 2. Третий журнальный поток в оракуле ничем не удержан: снятие middleware не роняет ни одной проверки - **Где:** `internal/controller/http/transcribe_test.go`, `setupTestEnv` и `TestCreateTranscribeJob_SenderFileNameNotLogged`. - **Severity:** major. Класс тот же, что проход `code` нашёл для стандартного `log` и что уже починено; для потока middleware дефект остался. - **Оракул (мутация на копии дерева, прогнана):** удаление `router.Use(sloggin.New(logger))` из тестового роутера — `ok git.vakhrushev.me/av/transcriber/internal/controller/http`, зелено. Для сравнения, три другие мутации краснеют как заявлено: снятие `log.SetOutput` роняет `…NotLoggedOnFailure`; имя под ключом `upload` роняет обе проверки запрета; `Info → Debug` на строке приёма роняет `…JournalTracesRecord`. - **Почему это важно, а не педантизм:** `design.md` включил middleware в тестовый роутер ровно затем, чтобы квантор требования («ни одна журнальная запись приёма») совпал с оракулом. Сегодня совпадение держится ничем: любой, кто уберёт строку как лишнюю, сузит оракул критического инварианта молча. Это записанное свойство проекта — `docs/review.md`, «Типовые узлы / Любой узел»: «проверка способна упасть», и там же журнальная запись 2026-08-11 об этом же классе. - **Что удержит:** в перехваченном журнале есть строка middleware, вот она: `msg="Incoming request" … request.path=/api/audio … response.status=201`. Достаточно `require.Contains(journal, "Incoming request")` в проверке успешного пути — рядом с уже стоящим `require.Contains(journal, "Err:")` в проверке отказа. - **Действие: инлайн.** --- ## Стоит исправить сейчас ### 3. Модель угроз называет метку `src_ext`, которой не существует - **Где:** `docs/security.md:200` — «`file_extension` у размера принятой записи и `src_ext` у длительности конвертации». - **Оракул:** `internal/metrics/metrics.go:44` — метки `{"source_format", "target_format", "error"}`. `grep -rn "src_ext"` по коду отдаёт единственное совпадение: локальную переменную Go в `internal/service/transcribe.go:194`. Журнальное поле рядом называется `src_format`. Метки `src_ext` нет ни в одной метрике. - **Почему сейчас:** эта строка — единственный носитель остатка в будущее. Задача про нормализацию будет искать по имени метки и не найдёт её. `file_extension` назван верно, ошибка ровно в одном слове. - **Действие: инлайн.** Заменить `src_ext` на `source_format`. ### 4. Критерий рубрики «ровно одна запись приёма» оракулом не закрыт - **Где:** `openspec/changes/no-user-filename-in-log/tasks.md`, критерий «в буфере ровно одна запись приёма на принятую запись, её уровень `INFO`»; проверка `TestCreateTranscribeJob_JournalTracesRecord`. - **Что есть:** уровень теперь привязан к строке самого приёма и мутацией проверен (`Info → Debug` краснеет). Числа записей не проверяет ничто. - **Почему сейчас, а не в гипотезы:** это не догадка, а разрыв между отмеченным как выполненный критерием и оракулом; закрывается одной строкой `strings.Count(journal, "Creating transcribe job") == 1`. Заодно это сторож на вторую строку приёма, в которую имя вернётся мимо нынешних проверок. - **Действие: инлайн.** --- ## Гипотезы без доказательства - **Запрет `t.Parallel()` держится комментарием.** `log.SetOutput` процессный, и ничто, кроме текста рядом, не мешает следующей проверке в этом файле стать параллельной; тогда перехват уедет к соседу. Оракула нет: воспроизвести это можно только внесением `t.Parallel()`, то есть той самой будущей правкой. `Confidence: medium`, severity не выше `minor`. - **Позитивное утверждение `require.Contains(journal, "Err:")` привязано к расхождению, которое конвенция объявляет подлежащим устранению** (`log.Printf` в HTTP-транспорте мимо `slog`). Когда расхождение починят, проверка отказа покраснеет по причине, не связанной с приватностью. Оракула нет — событие в будущем. `minor`. - **Кардинальность метки `file_extension` не замерена.** Утверждение «число временных рядов растёт неограниченно» верно по построению, но роста никто не мерил, а `docs/review.md` прямо велит не считать неизмеренный рост новой находкой (строка про «файлы и объекты не удаляются»). Вес — только внутри развилки №1. --- ## Promote candidates - **Правило линтера на ключи `file_name`/`filename` в вызовах логгера** (`sloglint` либо `forbidigo`). Пришло от `autotests`; `design.md` объявил это non-goal задачи. Претензия на правило проекта, а не на этот код. - **Свойство в `docs/review.md`, «Типовые узлы / Любой узел»:** каждый перехваченный в оракуле поток журнала удерживается **своим** положительным утверждением, иначе оракул сужается молча. Обобщение находки №2 и уже починенной находки прохода `code`. - **Нормализация расширения, взятого из имени отправителя** — отдельной задачей в урожай (список разрешённых расширений, `.bin` для прочего), с явным решением про имя файла на диске. Исход зависит от развилки №1. --- ## Что из уже починенного починено недостаточно Проверено мутациями на копии дерева, не со слов. - **Починено верно:** снятие `log.SetOutput` роняет проверку отказа; имя под чужим ключом роняет обе проверки запрета; `Info → Debug` роняет проверку прослеживаемости; утверждения о размере и уровне переписаны так, что переживают смену обработчика на JSON (`size["=:\s]+%d` и `level["=:\s]+INFO` разбирают и `size=33`, и `"size":33`); мёртвый `log.SetFlags` снят; тестовый роутер собирается той же цепочкой, что боевой; второй дом состава журнальной строки из `docs/conventions/logging.md` убран, строка «*Расхождение:*» на месте. - **Недостаточно — находка №2:** middleware в роутер добавлен, но не удержан; фикс закрыл поток `log` и не закрыл поток, ради которого роутер и меняли. - **Недостаточно — находка №3:** переписанный остаток в `docs/security.md` называет несуществующую метку `src_ext`. - **Недостаточно — находка №4:** критерий «ровно одна запись приёма» отмечен выполненным, оракула у него нет. - **Ослаблений не найдено:** ни одна прежняя проверка не стала слабее, `go test ./...` зелёный, новых замечаний линтера нет. Отдельно подтверждаю утверждения, на которых стоит `design.md`, — проверял сам: приём из Telegram имени, данного человеком, до сервиса не доводит (`internal/controller/tg/tg.go:276` берёт `file.FilePath`, выданный Telegram; `document.FileName` дальше проверки типа в `isAudioDocument` не идёт), а ключ объекта в Object Storage строится после конвертации и всегда имеет вид `идентификатор.ogg` (`internal/service/transcribe.go:190`). --- ## Границы покрытия ### План Шесть тем, у каждой есть дом и глубина, все шесть перечислены в сводке выше с исходом. Тем без дома нет, тем без отчёта нет. Своих тем проекта сверх ядра нет. ### Проходы - **Запускались** на метке `medium`, режим «по графу»: `review-autotests`, `review-specs` (код против спек), `review-code` (техника и конвенции, глубина разбор), `review-basics` (темы `security`, `operations`, `architecture`, глубина разбор), `review-triage`. - **Не запускались:** проходы ревью дизайна (`specs`, `rubric`) — они отработали до кода, на этапе предложения, и в этот прогон не входят. - **Чего в конвейере нет вовсе** — четыре строки, которые не принесёт ни один проход: 1. **Решения проекта не сверялись.** `docs/adr/` — процессный документ, прогон его не открывает. Расхождение изменения с записанным решением ловит сверка документации (`av-dev-docs:healthcheck`), а не ревью. 2. **Записанные наблюдения проекта не использовались.** `docs/research/` — тоже процессный. Все числа в этом отчёте сняты на этом прогоне, команды приложены. 3. **Поимённая сверка с руководством по стилю Go не задавалась ни одним проходом.** Различение «идиоматично против распространено» не спрашивал никто. 4. **Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.** Проход независимой реализации снят по стоимости; «не знаю, чего не знаю» никто не достаёт. ### Чего запущенные проходы не могли проверить в принципе - Ни один проход не гонял сервис на настоящих данных: `testdata` в проекте нет по запрету `CLAUDE.md`, реальные ключи Yandex Cloud под запретом, боевую БД и `data/files` трогать нельзя. Все оракулы — синтетический вход и мутации. - `autotests` судит прогон и мутации, а не смысл проверок; `specs` судит код против заказанного поведения и не ищет дефектов вне него; `code` судит технику и конвенции и не судит требования; `basics` идёт по трём темам и только по их записанным домам. - Приём из Telegram своей проверки не получил вовсе (объявлено риском в `design.md`): правка достаётся ему общим шагом кода, но оракула на него нет. - Триаж не читает код в поисках дефектов: пропуск любого прохода — мой пропуск тоже. ### Что осталось целиком на человеке Из `docs/review.md`, «Недоступно проверке», двумя списками — они не сливаются. **Не проверит ни один проход:** - `operations`: поведение SpeechKit и Object Storage под нагрузкой и на границах — поднять их в тесте нечем; - `operations`: реальный профиль нагрузки; проект живёт на единицах записей в день, и утверждения о росте остаются условиями; - `security`: стойкость `ffmpeg` к вредоносному входу — разбор чужого формата отдан внешней программе. **Перестали проверять сознательно:** - `autotests`: разбор вывода настоящего `ffprobe`. Проверки приёма получают длительность от подставного источника; своего теста у `adapter/metaviewer/ffmpeg` нет. Решение и его цена — `docs/adr/ADR-2026-08-11-stub-adapters-in-tests.md`. **Плюс общее, вне зависимости от проекта:** история инцидентов, поведение под реальным потоком, поведение внешних систем в их версиях, завязка потребителей на текущее поведение и вопрос «а нужна ли эта функциональность вообще». ### Каких документов проекта не хватило - **`CLAUDE.md`, «Ориентир по размеру порции: не замерялся».** Разметка «инлайн против развилки» опирается на right-size, а мерки right-size в проекте нет. Пометки «инлайн» в находках 2–4 поставлены по объёму правки (одна-две строки), и это моё предположение, а не сверка с записанным ориентиром. - **Дома у вопроса «что можно класть в метку метрики» нет.** `docs/security.md` описывает открытость `/metrics`, но правила состава меток нет ни в модели угроз, ни в конвенциях; отсюда развилка вместо однозначного вердикта в находке №1. - Прочие нужные документы на месте и использованы: инварианты `CLAUDE.md`, `docs/review.md` (включая «Типовые ложноположительные» — применён пункт про открытый HTTP API), `docs/security.md`, `docs/conventions/logging.md`. Деградации по ним нет. ### Сработавшие потолки - **Ни один из четырёх проходов не сообщил свой потолок** — ни сколько находок показал из скольких, ни что осталось за срезом. Это находка о прогоне: судить, полон ли вход триажа, нечем, и «на входе 17 находок» может означать «17 из 17», а может «17 из скольких-то». - **Потолок триажа сработал мягко:** 2 из 3 в «Блокирует мердж» и 2 из 4 в «Стоит исправить сейчас». Ничего не выброшено из-за потолка; выброшенное выброшено по дедупликации (одна причина на три носителя в находке №1, один класс на четыре формулировки в находке №2) и по отсеву — снятый `log.SetFlags` и второй дом в конвенции уже починены, а «изоляция буфера держится не тем, чем заявлено» после правки сведено к комментарию и уехало в гипотезы. Формулировка «критичных проблем не обнаружено» к этому прогону неприменима: критичная проблема обнаружена и стоит развилкой №1.