# Триаж ревью кода: `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`), а это имя файла в хранилище вида
  `<uuid>.<хвост имени отправителя>`. То есть в метку `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.
