Files
transcriber/CLAUDE.md
T
av bb9a67929c docs: правила о контексте и о токене записаны, две находки — в журнал
- в go-linters.md заведён раздел «Отмена и внешний собеседник», перечень
  механизированного пополнен девятью правилами и двумя шагами гейта, названы
  остатки: contextcheck не видит сигнатуру без контекста вовсе, шаг migrations
  судит только шаги, бывшие в базе диффа, rowserrcheck и sqlclosecheck
  профилактические — предмета в коде нет
- в logging.md и security.md чистка отказа Telegram описана по факту: точка одна
  и лежит на границе клиента, закрыты все пять путей вместе с логгером самой
  библиотеки. Прежнее «*Расхождение:* вычистки нет... она не логируется» было
  неверным дважды
- в журнал дефектов записаны две находки: отказ скачивания уносил токен бота
  (проскочил, жил с самого начала) и остановка сервиса хоронила конвертируемую
  запись в failed (поймано ревью до коммита)
- вопрос темы operations про отмену переформулирован: спрашивать надо не
  «доходит ли контекст», а «что шаг делает с задачей, деньгами и ответом
  отправителю»; вопрос про таймаут оставлен с оговоркой, что проброс контекста
  на него не отвечает
- в памятке: словарь кодов новых шагов, требование компилятора C у детектора
  гонок и оговорка, что «CGO не нужен» относится к сборке, а не к гейту
2026-08-13 10:28:31 +03:00

19 KiB
Raw Blame History

CLAUDE.md

Памятка для работы над transcriber. Перед задачей прочитай также docs/passport.md, docs/architecture.md и docs/conventions/.

Проект ведём по-русски.

Что это

Сервис расшифровки аудио в текст. Принимает запись двумя входами — Telegram-бот и HTTP API, — конвертирует её ffmpeg в ogg, отдаёт на отложенное распознавание Yandex SpeechKit и возвращает текст туда, откуда пришла запись. Состояние задач, метаданные и сами файлы лежат во встроенной PocketBase, и она же даёт владельцу панель администратора.

Чего не делает: сам речь не распознаёт и своих моделей не держит, текст руками не правит и в форматы документов не экспортирует, учётных записей не заводит, с живым потоком не работает и складом произвольных файлов не служит. Записи и расшифровки хранит бессрочно: решением от 2026-08-11 сервис — архив. Перечень выше — выжимка, границу домена целиком держит docs/passport.md.

Стек

Go 1.26 (сборке CGO не нужен; детектору гонок в гейте — нужен), встроенная PocketBase — хранилище, файлы записей и панель администратора, — go-telegram-bot-api, aws-sdk-go-v2 для Object Storage, gRPC-клиент Yandex SpeechKit v3, Prometheus, slog. Сборка — Taskfile, образ — Docker, выкладка — Ansible из pet-project-server.

Инварианты

Что нарушать нельзя.

  • Секрет не покидает конфиг. Токен бота, ключ SpeechKit, пара ключей Object Storage и секрет клиента OIDC не попадают в git, в лог, в ответ пользователю и в колонку error_text. Нарушение необратимо: утёкший ключ отзывают и меняют вручную во всех местах выкладки. critical Изъятие: секрет клиента OIDC живёт ещё и в настройках коллекции пользователей хранилища — туда его кладёт приведение настроек при каждом подъёме, потому что применённый шаг схемы не переписывается и не пережил бы ротации. Чтение файла базы равносильно чтению этого секрета; перечисленные места запрета это не отменяет.
  • Содержимое записи остаётся приватным. Текст расшифровки, имя файла пользователя и его сообщение в лог не пишутся — только длина и идентификаторы. Нарушение необратимо: строки уже уехали в журнал контейнера. critical Изъятие: расширение — хвост после последней точки — в журнал попадает собственным полем: по нему прослеживается путь записи. Имени файла в журнале нет вовсе (инвариант ниже). Изъятие узкое и кончается журналом: наружу, меткой метрики, расширение выходит только приведённым к перечню известных форматов. Границу держит спека intake, цена — adr/ADR-2026-08-11-known-format-label.md, остаток — docs/security.md.
  • Бот отвечает только тем, кто в белом списке. Бот проверяет отправителя до любой работы, включая скачивание файла. Нарушение обратимо правкой конфига, но чужие записи к тому моменту уже обработаны за наши деньги. critical
  • Принятая запись не теряется молча. Отказ на любом шаге либо оставляет задачу пригодной к повтору, либо переводит её в failed и сообщает пользователю. Молчаливый выход из шага без записи в лог и без смены состояния запрещён. Обратимо повторной отправкой, но пользователь об этом не узнает. major
  • NoopJobError — не ошибка. Значение «задач в этом состоянии нет» не логируется, не считается в метрику и не поднимает уровень. Нарушение даёт запись раз в секунду на каждый воркер. major
  • Миграция, уехавшая на сервер, не переписывается. Изменение — только новым файлом шага. Необратимо: хранилище считает применённое по имени файла. critical
  • Имя файла в хранилище задаёт сервис, а в журнал не идёт. Умолчание PocketBase строит имя из имени, данного отправителем, — оно не применяется. Само имя — последняя часть ссылки /api/files/..., поэтому в журнал пишется расширение, а не имя: строка журнала иначе стала бы бессрочным ключом к чужой записи. critical
  • Колонки очереди правятся в четырёх местах пакета хранилища — applyToRecord, recordToJob, константа acquireColumns и структура acquiredRow с её toJob, — плюс шаг схемы. Компилятор видит два из них. Колонка, забытая в паре acquireColumns/acquiredRow, приезжает из захвата нулевой, и первый же Save пишет этот ноль поверх сохранённого значения: поле теряется только у задачи, попавшей к воркеру. major
  • Результат пишет только держатель захвата. Шаг, чей захват за время работы достался другому, завершается без записи и без ответа отправителю. Иначе два воркера пишут в одну задачу по очереди, а отправитель получает два ответа. major

Команды

go build ./...            # CGO не нужен
go test ./...             # в гейте идёт с -race, и там нужен компилятор C
go vet ./...
gofmt -l .
golangci-lint run
go run . -c config.toml   # флаг -c или --config, по умолчанию config.toml
task image                # docker-образ; тег и раскладка — docs/architecture.md
task gate                 # весь набор проверок разом

Локальный запуск требует ffmpeg и ffprobe в PATH и своего config.toml — скопируй config.dist.toml и заполни; известные прорехи образца перечислены в docs/conventions/config.md строками «Расхождение:».

Гейт

  • Команда целиком: task gate. База диффа — переменная BASE, по умолчанию origin/master; переопределяется task gate BASE=<rev>.
  • Какое правило чем проверяется — конвенция docs/conventions/go-linters.md. Здесь семантика гейта, там перечень правил, подавлений и место настройки каждого; перечень здесь не повторяется.
  • Где логи шагов: вывод команды, отдельного файла нет.
  • Что означает каждый исход: ненулевой код любого шага роняет гейт. У docs.py check, tasks.py check, openspec.py check и scripts/check-go-version.sh словарь кодов общий: 0 сошлось, 1 дрейф, 2 ошибка употребления, 3 окружение (не корень проекта, каталог или файл не найден), 4 внутренний сбой. Последний своего словаря не заводит намеренно: четвёртый шаг с собственной семантикой сделал бы это утверждение неверным. Тому же словарю следуют обёртки шагов в Taskfile.yml — все, включая tests, migrations, shell, dockerfile и vulns: недостающий инструмент — отказ окружения, код 3. У tests это отсутствие CGO или компилятора C, без которых не работает детектор гонок — тесты он в этом случае всё равно гоняет, без -race, и краснеет уже после них. У migrations код 3 — неразрешимая база диффа, отсутствующий каталог шагов и каталог без единого шага; код 1 — переписанный шаг схемы. Сами чужие инструменты (shellcheck, hadolint, govulncheck, golangci-lint) держат свои коды, и гейту от них нужно только «ненулевой». Недостающий скрипт — отказ окружения, код 3. Наружу все эти коды приходят одним: сам task на любой отказ шага выходит с 201, а код шага печатает строкой («exit status 3»), поэтому словарь читается по коду скрипта.
  • Что красит безусловно и почему: отказ сборки, тестов, go vet, гонка, найденная детектором (go test -race), переписанный применённый шаг схемы, неотформатированный файл, находка golangci-lint, расхождение объявленных версий Go, дрейф раскладки документов, дрейф каталога задач, форма openspec/config.yaml, достижимая из кода уязвимость в зависимостях (govulncheck), находка shellcheck в скриптах оболочки и hadolint в Dockerfile. Машина проверяет всё перечисленное, и это не обсуждается. Шаг, чей скрипт не найден, краснеет с именем недостающего плагина, а не пропускается молча.
  • Что ловит pre-commit, а что только гейт. lefthook.yml гоняет на затронутых файлах дешёвую часть: gofmt (правит на месте и добавляет в коммит), golangci-lint по пакетам тронутых файлов, shellcheck, hadolint, gitleaks по индексу. Только гейту остаются сборка, go vet, тесты целиком, сверка версий Go, три сверки документов и govulncheck: они смотрят всё дерево либо требуют сети, а pre-commit обязан быть быстрым.
  • Шагу vulns нужна сеть, и он один такой: база уязвимостей живёт на vuln.go.dev. Без сети шаг краснеет, а не пропускается молча; сам инструмент ставится go install golang.org/x/vuln/cmd/govulncheck@latest. Судит он достижимость из кода: находка в модуле, чей уязвимый символ мы не вызываем, шаг не роняет. Такая сегодня одна — GO-2026-5932 в golang.org/x/crypto/openpgp, исправления у неё нет вовсе.
  • Чего в гейте намеренно нет и кто тогда обязан это гонять:
    • сборка образа — дорога, и отказ от неё сознательный. Дешёвая замена стоит шагом сверки версий: он сравнивает строки и ловит расхождение, из-за которого образ перестаёт собираться, но собираемости не проверяет. Собрать образ по-прежнему может только человек — task image, и на подъёме версии это обязательно;
    • собираемость Dockerfile: hadolint судит форму, а не сборку, и два его правила подавлены поимённо — DL3007 до задачи pin-runtime-image-base и DL3018 по существу (alpine не держит старые версии пакетов, закрепление ломает сборку через недели). Причины стоят строками в Taskfile.yml;
    • gitleaks — висит на pre-commit в lefthook.yml и смотрит только индекс коммита. Полную историю никто не проверяет;
    • согласованность документов между собой и с кодом — её судят агенты, зовёт их скилл av-dev-docs:healthcheck, и звать его надо руками;
    • покрытие изменённых строк не считается ничем.

Гейт на master сегодня зелёный целиком, и объявленных долгов у него нет. Красный шаг означает поломку — свою или чужую, но поломку, а не наследство. Списывать отказ на долг больше нельзя: списывать не на что.

Два прежних долга закрыты и здесь названы, чтобы отказ на их месте читался как новый:

  • golangci-lint run давал 4 замечания — два непроверенных Close и два сравнения ошибок приведением типа. Закрыто задачей errors-as-instead-of-typecast 2026-08-11; тогда же у errcheck включена настройка check-blank, поэтому _ = x.Close() больше не снимает замечание: отказ, который решено не проверять, объявляют в exclude-functions поимённо;
  • go test ./... чинила задача http-handler-tests-never-green.

Запреты

  • Боевой каталог данных не трогать. data/ на сервере целиком: под ним и база (data/data.db), и записи живых людей (data/storage/<коллекция>/<запись>/). Локальный каталог данных — свой, его ронять и пересоздавать можно свободно.
  • Боевым токеном бота не запускаться. Второй процесс с тем же токеном перехватывает обновления у работающего, и пользователь теряет ответы.
  • Yandex Cloud за деньги. Распознавание и хранение в Object Storage оплачиваются по факту. Прогон на реальных ключах ради проверки кода запрещён — подставляй internal/adapter/recognizer/memory.go.
  • Выкладку не запускать. inv pl -- transcriber из pet-project-server запускает человек.
  • testdata в проекте нет. Тесты, которым нужен файл, создают его во временном каталоге и убирают за собой.
  • Временноеt.TempDir() в тестах, /tmp вне их. В data/ временное не писать: этот каталог смонтирован на сервере.

Работа

  • Основная ветка: master. Коммиты идут в неё напрямую, веток и PR нет.
  • Сообщение коммита без трейлера Co-Authored-By.
  • Необратимое (спрашивается у человека всегда): применённая миграция, формат файла на диске и раскладка каталога данных, публичный контракт HTTP API, имя ключа конфига, любое действие с боевыми данными и с Yandex Cloud, ротация секрета.
  • Что считается сломанным — новый красный шаг гейта, которого не было до твоей правки. Такое чинится прежде любой другой работы. Два объявленных долга из раздела «Гейт» сломанным состоянием не считаются, пока их не закрыли задачами.
  • Ориентир по размеру порции: не замерялся.
  • Что такое «сделана»: task gate зелёный и критерии приёмки проверены поимённо.

Язык

  • Документация, комментарии, сообщения коммитов — русский.
  • Код и идентификаторы — английский.
  • Текст, который видит пользователь Telegram, — русский.