- расширение приводится к перечню известных форматов прежде метки метрики: страница метрик открыта, и хвост имени уезжал на неё дословно - проверки приёма перехватывают все три потока журнала и читают реестр метрик, каждая падает при снятии того, что сторожит
66 KiB
Триаж ревью кода: 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.md2а.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.md2а.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, развилка, решение за человеком. Единственное, что блокирует мердж.
- Комментарий про
log.SetOutput— находка №2, инлайн, одно слово в двух местах. - Литерал
otherне привязан к спеке — находка №3, инлайн, одна строка. - Остаток «хвост расширения остаётся в журнале» задачей не заведён. Записан
прозой в
docs/security.mdи в Risksdesign.md. Не починено намеренно: заведение задач принадлежит скиллу задач, а не конвейеру ревью. Идёт урожаем (Promote candidates, пункт 4). - Конвенции о метриках нет — урожай, Promote candidates, пункт 3. Не находка об этом коде.
- Расширение в имени файла на диске не нормализуется — решение принято
человеком на контрольной точке и записано отвергнутым вариантом в
design.md. Раскладкаdata/filesобъявлена необратимой вCLAUDE.md. Закрыто как решение, не как пробел. - Сценарий «Известное расширение идёт как есть» назван неточно — тело
сценария (
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на предложении) отработали до кода и в этот прогон не входят.
Чего в конвейере нет вовсе
Четыре строки, которые не принесёт ни один проход:
- Решения проекта не сверялись.
docs/adr/— процессный документ, прогон его не открывает. Расхождение изменения с записанным решением ловит сверка документации (av-dev-docs:healthcheck), а не ревью. В каталоге лежат четыре ADR, включаяADR-2026-08-11-stub-adapters-in-tests.md, — ни один из них этим прогоном не читался как источник требований. - Записанные наблюдения проекта не использовались.
docs/research/— тоже процессный. Все числа в этом отчёте сняты на этом прогоне, команды приложены. - Поимённая сверка с руководством по стилю Go не задавалась ни одним
проходом. Различение «идиоматично против распространено» — например,
map[string]struct{}противslices.Containsвformat_label.go, или уместность тавтологичного сравнения с константой — не спрашивал никто. - Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет. Проход независимой реализации снят по стоимости, а не по замеру; «не знаю, чего не знаю» никто не достаёт.
Чего запущенные проходы не могли проверить в принципе
- Ни один проход не гонял сервис на настоящих данных:
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) — они отработали до кода, на этапе предложения, и в этот прогон не входят. - Чего в конвейере нет вовсе — четыре строки, которые не принесёт ни один
проход:
- Решения проекта не сверялись.
docs/adr/— процессный документ, прогон его не открывает. Расхождение изменения с записанным решением ловит сверка документации (av-dev-docs:healthcheck), а не ревью. - Записанные наблюдения проекта не использовались.
docs/research/— тоже процессный. Все числа в этом отчёте сняты на этом прогоне, команды приложены. - Поимённая сверка с руководством по стилю Go не задавалась ни одним проходом. Различение «идиоматично против распространено» не спрашивал никто.
- Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет. Проход независимой реализации снят по стоимости; «не знаю, чего не знаю» никто не достаёт.
- Решения проекта не сверялись.
Чего запущенные проходы не могли проверить в принципе
- Ни один проход не гонял сервис на настоящих данных:
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.