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

66 KiB
Raw Blame History

Триаж ревью кода: 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 --strictis 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:212WithLabelValues(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:6const 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; задачи на него нет.
  • Смена формы значения метки (.mp3mp3) записана в 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.MP3mp3) описывает приведение регистра, заголовок говорит «как есть». Выброшено по отсеву вкусовщины: поведения не меняет, на стоимость следующей правки не влияет, записанной конвенции не нарушает — текст требования выше разночтение снимает дословно.

Потолок первых двух секций не срабатывал: 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 и вкусовой характер вопроса о доме записи про .mp3mp3.
  • 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) → CreateJobFromApicreateTranscribeJobext (: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.