Files
transcriber/openspec/changes/archive/2026-08-11-no-user-filename-in-log/review/triage.md
T
av bd6001cd8d имя файла отправителя убрано из журнала приёма
- расширение приводится к перечню известных форматов прежде метки метрики:
  страница метрик открыта, и хвост имени уезжал на неё дословно
- проверки приёма перехватывают все три потока журнала и читают реестр метрик,
  каждая падает при снятии того, что сторожит
2026-08-11 16:37:58 +03:00

803 lines
66 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Триаж ревью кода: `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.