доменные ошибки сравниваются через errors.As, отказ Close не теряется
- признаки «работы нет» и «задача не найдена» узнаются по смыслу, а не приведением типа: обёртка `%w` на пути больше не превращает пустой прогон воркера в отказ раз в секунду - отказ закрытия соединения с распознавателем доходит до вызывающего (`errors.Join`) либо до журнала; у `errcheck` включён `check-blank`, иначе критерий принимал реализацию, выбрасывающую отказ в пустоту - заведены первые тесты пакета worker и capability `pipeline`; долг из четырёх замечаний линтера закрыт, гейт зелёный целиком
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# ADR-2026-08-11. Границу распознавания доменного признака держит норма, а не код
|
||||
|
||||
- **Дата:** 2026-08-11
|
||||
- **Источник:** [openspec/changes/archive/2026-08-11-errors-as-instead-of-typecast/design.md](../../openspec/changes/archive/2026-08-11-errors-as-instead-of-typecast/design.md), раздел `Decisions`, Решение 3
|
||||
|
||||
## Решение
|
||||
|
||||
Признак «работы нет» узнаётся через `errors.As`, то есть на любой глубине цепочки
|
||||
ошибки. Встречный риск — отказ, к которому признак примешался по дороге, — закрыт
|
||||
**требованием спеки**, а не проверкой в коде воркера.
|
||||
|
||||
Дословно из источника:
|
||||
|
||||
> Граница ставится **нормой, а не кодом**: спека требует, чтобы признак рождался
|
||||
> только ответом хранилища на опрос этим же шагом, и запрещает слою сохранять
|
||||
> чужой признак в цепочке своей ошибки.
|
||||
|
||||
## Почему
|
||||
|
||||
Приведение типа видело только вершину цепочки — потому и ломалось от первой же
|
||||
обёртки. `errors.As` эту проблему устраняет, но устраняет симметрично: признак
|
||||
теперь виден и там, где его никто не клал осознанно. Отказ, к которому признак
|
||||
примешался обёрткой или `errors.Join`, воркер зачёл бы пустым прогоном — задача
|
||||
осталась бы в своём состоянии и переопрашивалась раз в секунду без единой записи.
|
||||
Это тот же класс, от которого защищает инвариант «Принятая запись не теряется
|
||||
молча», только с обратным знаком относительно чинимого дефекта.
|
||||
|
||||
Кодовый вариант рассмотрен и отвергнут по цене:
|
||||
|
||||
> **Рассмотрено и отвергнуто — научить воркер различать «признак на вершине» от
|
||||
> «признака в глубине».** Отвергнуто по цене: `errors.As` такого различения не
|
||||
> даёт вовсе, пришлось бы либо проверять вершину вручную (то есть вернуть
|
||||
> приведение типа, которое чинится), либо заводить свой обход цепочки. Код
|
||||
> усложняется ради случая, которого сегодня нет ни одного, а защита от него нужна
|
||||
> на входе — при написании нового слоя, — где норма работает, а проверка в
|
||||
> рантайме опоздала бы.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` код остался коротким: одна проверка вместо разбора цепочки вручную.
|
||||
- `+` защита стоит там, где ошибку совершают, — за письменным столом автора
|
||||
нового слоя, а не в рантайме, где она уже случилась.
|
||||
- `−` норма не механизирована: её нарушение поймает только ревью или чтение.
|
||||
Единственный `MUST` требования `pipeline` без машинного оракула — этот.
|
||||
- `−` правило живёт в двух документах: требованием в
|
||||
[openspec/specs/pipeline/spec.md](../../openspec/specs/pipeline/spec.md) и
|
||||
прозой в [conventions/errors.md](../conventions/errors.md), где оно нужно
|
||||
автору в момент письма. Второй адрес ссылается на первый и нормой не является —
|
||||
разойтись они могут только правкой, сделанной мимо спеки.
|
||||
@@ -0,0 +1,54 @@
|
||||
# ADR-2026-08-11. Отказ, который решено не проверять, объявляется поимённо
|
||||
|
||||
- **Дата:** 2026-08-11
|
||||
- **Источник:** [openspec/changes/archive/2026-08-11-errors-as-instead-of-typecast/design.md](../../openspec/changes/archive/2026-08-11-errors-as-instead-of-typecast/design.md), раздел `Decisions`, Решение 2
|
||||
|
||||
## Решение
|
||||
|
||||
У `errcheck` включена настройка `check-blank`: присваивание отказа в `_` больше
|
||||
не снимает замечание линтера. Место, где отказ решено не проверять, вносится в
|
||||
`exclude-functions` поимённо.
|
||||
|
||||
Дословно из источника:
|
||||
|
||||
> Правило `errcheck` сегодня молчит на `_ = conn.Close()`: настройка
|
||||
> `check-blank` не выставлена, а её умолчание — «пропускать». То есть
|
||||
> реализация, выбрасывающая отказ в пустоту, удовлетворяет критерию приёмки
|
||||
> «линтер не даёт замечаний `errcheck`», не удовлетворяя самому критерию —
|
||||
> «отказ возвращается либо попадает в журнал».
|
||||
|
||||
## Почему
|
||||
|
||||
Решение принято не ради строгости, а потому что **оракул не мог упасть**. Задача
|
||||
`errors-as-instead-of-typecast` закрывала два непроверенных `Close`, и её
|
||||
критерий приёмки опирался на молчание линтера. Ревью дизайна показало, что этому
|
||||
критерию удовлетворяет и негодная реализация: `_ = conn.Close()` теряет отказ
|
||||
целиком, а линтер молчит. Критерий, который нельзя уронить, не проверяет ничего —
|
||||
и вместе с ним в `CLAUDE.md` снималась запись о долге, то есть сигнал исчез бы
|
||||
навсегда и без следа.
|
||||
|
||||
Очевидный путь был другим и отвергнут намеренно:
|
||||
|
||||
> **Рассмотрено и отвергнуто — дописать оба типа в `exclude-functions`
|
||||
> `.golangci.yml`.** Соблазн сильный: список исключений там уже есть, и в нём
|
||||
> записана ровно эта политика […] Отвергнуто: политика в конфиге относится к
|
||||
> закрытию, у которого **отказ ничего не значит** […] Записав их в исключения,
|
||||
> мы бы расширили политику молча, самим фактом добавления строки, и потеряли бы
|
||||
> оба сигнала навсегда.
|
||||
|
||||
Включение проверено прогоном до правки кода: на тогдашнем коде правило не давало
|
||||
ни одного нового замечания, то есть включалось чисто и отдельного коммита
|
||||
приведения не требовало.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` критерий «отказ не теряется молча» стал проверяемым машиной: мутация
|
||||
(замена обоих мест на `_ = …Close()`) роняет линтер — проверено прогоном.
|
||||
- `+` умолчание сместилось в сторону заметности: спрятать отказ по месту больше
|
||||
нельзя, отказ от проверки виден в одном файле списком.
|
||||
- `−` осознанное игнорирование подорожало: вместо одного символа `_` нужна строка
|
||||
в `exclude-functions` с полным именем метода. Для одноразового случая это
|
||||
заметная церемония.
|
||||
- `−` список исключений будет расти, и каждая его строка — это политика на весь
|
||||
проект, а не на одно место. Разрастание списка — сигнал, что правило выбрано
|
||||
неверно, и повод пересмотреть эту запись.
|
||||
@@ -32,6 +32,8 @@
|
||||
|
||||
| Дата | Запись | Статус |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-11 | [Границу распознавания доменного признака держит норма, а не код](ADR-2026-08-11-domain-marker-boundary-by-norm.md) | |
|
||||
| 2026-08-11 | [Отказ, который решено не проверять, объявляется поимённо](ADR-2026-08-11-errcheck-check-blank.md) | |
|
||||
| 2026-08-11 | [Наружу расширение выходит только приведённым к перечню](ADR-2026-08-11-known-format-label.md) | |
|
||||
| 2026-08-11 | [Приложение пишем на Vue, а Node входит в гейт и в образ](ADR-2026-08-11-spa-on-vue.md) | |
|
||||
| 2026-08-11 | [Очередь остаётся своей таблицей, но коллекцией PocketBase](ADR-2026-08-11-queue-as-pocketbase-collection.md) | |
|
||||
|
||||
+15
-7
@@ -8,11 +8,19 @@
|
||||
[passport.md](passport.md) и в [tasks/ROADMAP.md](../tasks/ROADMAP.md); что из
|
||||
этого ещё не решено — в разделе «Открытые вопросы».
|
||||
|
||||
Заведена одна capability — [intake](../openspec/specs/intake/spec.md), и в ней
|
||||
описан **только приём по HTTP**: его нормируют проверки, написанные задачей
|
||||
`http-handler-tests-never-green` 2026-08-11. Поведение прочих узлов, включая
|
||||
приём из Telegram, по-прежнему живёт только в коде. Задача, которая его трогает,
|
||||
дописывает спеку своей capability.
|
||||
Заведены две capability, и каждая описана частично:
|
||||
|
||||
- [intake](../openspec/specs/intake/spec.md) — **только приём по HTTP**: его
|
||||
нормируют проверки, написанные задачей `http-handler-tests-never-green`
|
||||
2026-08-11;
|
||||
- [pipeline](../openspec/specs/pipeline/spec.md) — **только пустой прогон
|
||||
воркера**: задача `errors-as-instead-of-typecast` 2026-08-11. Переходы
|
||||
состояний, захват и срок его протухания, отмена контекста посреди шага в неё
|
||||
**не** переехали и остаются долгом; что именно не описано, перечисляет раздел
|
||||
`Purpose` самой спеки.
|
||||
|
||||
Поведение прочих узлов, включая приём из Telegram, по-прежнему живёт только в
|
||||
коде. Задача, которая его трогает, дописывает спеку своей capability.
|
||||
|
||||
## Принципы
|
||||
|
||||
@@ -24,7 +32,7 @@
|
||||
решено 2026-08-11,
|
||||
[ADR](adr/ADR-2026-08-11-queue-as-pocketbase-collection.md), сравнение
|
||||
кандидатов в [research/job-queue.md](research/job-queue.md).
|
||||
<!-- канон: поведение → openspec/specs/pipeline -->
|
||||
<!-- канон: поведение → openspec/specs/pipeline; ещё НЕ переехало: спека заведена, но это в ней не описано -->
|
||||
- **Шаг конвейера идемпотентен по повтору.** Задача, брошенная на середине,
|
||||
достаётся снова по истечении срока захвата и проходит шаг заново.
|
||||
- **Ядро зависит от интерфейсов.** `internal/service` знает только
|
||||
@@ -48,7 +56,7 @@
|
||||
| Отправитель Telegram | `internal/adapter/telegram` | Отправка текста, деление длинного по словам |
|
||||
| Репозитории | `internal/adapter/repo/sqlite` | Задачи и файлы, запросы через goqu |
|
||||
|
||||
<!-- канон: поведение → openspec/specs/pipeline -->
|
||||
<!-- канон: поведение → openspec/specs/pipeline; ещё НЕ переехало: спека заведена, но это в ней не описано -->
|
||||
|
||||
Конвейер: `created` → `converted` → `transcribe` → `done` либо `failed`. Три
|
||||
воркера двигают по одному переходу, каждый опрашивает базу раз в секунду.
|
||||
|
||||
@@ -18,7 +18,11 @@ severity — в [CLAUDE.md](../../CLAUDE.md).
|
||||
Четыре записи перенесены из проекта jellybit — тот же Go, тот же автор, те же
|
||||
задачи. Код transcriber написан раньше и **части правил не следует**: ключи —
|
||||
UUID вместо ULID, время берётся `time.Now()` по месту, лог пишется на каждом
|
||||
шаге и дублируется воркером, доменные ошибки проверяются приведением типа.
|
||||
шаге и дублируется воркером.
|
||||
|
||||
Из этого перечня одно уже закрыто: доменные ошибки проверялись приведением типа
|
||||
до 2026-08-11, задача `errors-as-instead-of-typecast`. Приведение типа на этом
|
||||
месте больше не долг, а регрессия.
|
||||
|
||||
Пятая, `web-ui.md`, тоже пришла оттуда, но не прижилась: jellybit работает на
|
||||
htmx, а здесь решено делать SPA — и перенесённый текст снят целиком.
|
||||
@@ -51,7 +55,7 @@ htmx, а здесь решено делать SPA — и перенесённы
|
||||
| Правило | Где механизировано |
|
||||
| --- | --- |
|
||||
| Сравнение ошибок через `errors.Is` и `errors.As`, не `==` и не приведением типа | `.golangci.yml` → `errorlint` |
|
||||
| Непроверенное возвращаемое значение ошибки | `.golangci.yml` → `errcheck` (кроме `defer Close` и `send`) |
|
||||
| Непроверенное возвращаемое значение ошибки | `.golangci.yml` → `errcheck`, включая присваивание в `_` (`check-blank`). Отказ, который решено не проверять, объявляют в `exclude-functions` поимённо — там сегодня `defer Close` и `send` |
|
||||
| Форматирование исходников | `.golangci.yml` → `gofmt` |
|
||||
| Подозрительные конструкции языка | `.golangci.yml` → `govet`, `staticcheck`, `ineffassign`, `unused` |
|
||||
| Секреты в коммите | `lefthook.yml` → `gitleaks git --staged` |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
**Взято из проекта jellybit.** Расхождения с сегодняшним кодом названы по месту.
|
||||
Главное: единой точки отображения доменной ошибки в ответ нет, обработчики
|
||||
решают сами, а доменные ошибки проверяются приведением типа, а не `errors.As`.
|
||||
решают сами.
|
||||
|
||||
**Механизировано:** приведение типа и `err == ErrX` ловит `errorlint` в
|
||||
`.golangci.yml`. Запрета сторонних пакетов ошибок (`depguard`) нет — сторонних
|
||||
@@ -48,14 +48,13 @@ transcriber — **приложение, а не библиотека**: внеш
|
||||
`sql.ErrNoRows` превращается в доменную ошибку в слое репозитория, чтобы выше
|
||||
по коду не торчал `database/sql`.
|
||||
- Проверяем `errors.Is` и `errors.As`, а не сравнением и не приведением типа.
|
||||
|
||||
*Расхождение, и оно опасно:* `NoopJobError` и `JobNotFoundError` проверяются
|
||||
приведением типа — `err.(*contract.NoopJobError)` в
|
||||
`internal/controller/worker/worker.go` и `err.(*contract.JobNotFoundError)` в
|
||||
`internal/service/transcribe.go`. Работает это только потому, что на этом пути
|
||||
ошибку никто не оборачивает. Первый же `fmt.Errorf("…: %w")` между ними сломает
|
||||
проверку молча: воркер перестанет отличать «задач нет» от отказа и начнёт
|
||||
считать пустой прогон ошибкой раз в секунду.
|
||||
- **Признак домена читается только из ответа того шага, который его породил.**
|
||||
`errors.As` распознаёт признак на любой глубине цепочки, а не только сверху,
|
||||
— поэтому слой, придающий отказу собственный смысл, чужой признак в свою
|
||||
цепочку не сохраняет. Иначе воркер примет отказ, к которому признак
|
||||
примешался, за этот признак: зачтёт настоящий сбой пустым прогоном, и задача
|
||||
продолжит переопрашиваться без единой записи в журнале. Норма записана требованием
|
||||
[pipeline](../../openspec/specs/pipeline/spec.md).
|
||||
|
||||
## Sentinel и типизированные
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ SpeechKit, Yandex Object Storage и `ffmpeg`. Мерить нужно то, чт
|
||||
|
||||
| Дата | Запись | О чём |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-11 | [gRPC-клиент SpeechKit: когда закрытие вообще может отказать](grpc-client-close.md) | Ленивое соединение и два исхода `Close` в grpc v1.74.2 |
|
||||
| 2026-08-11 | [Фреймворк приложения: Svelte, Vue и React на одном экране](spa-framework.md) | Размер собранной статики, цена шага сборки, что у трёх кандидатов одинаково |
|
||||
| 2026-08-11 | [Очередь задач: своя таблица против готовой библиотеки](job-queue.md) | Цена River и goqite в пакетах, захват одним запросом, чего нет для PocketBase |
|
||||
| 2026-08-11 | [PocketBase: что даёт панель администратора](pocketbase.md) | Записи, пользователи и файлы в панели версии 0.39.10 |
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# gRPC-клиент SpeechKit: когда закрытие вообще может отказать
|
||||
|
||||
Отвечает на вопрос, возникший по ходу задачи `errors-as-instead-of-typecast`: что
|
||||
означает отказ `Close` у клиента SpeechKit и стоит ли писать его в журнал.
|
||||
Наблюдение понадобилось потому, что первая редакция кода и обоснования описывала
|
||||
этот отказ неверно — как признак недоступности Yandex.
|
||||
|
||||
## Как снималось
|
||||
|
||||
Не замером, а **чтением исходников** зависимости, зафиксированной в `go.mod`:
|
||||
`google.golang.org/grpc` версии **v1.74.2**. Смотрел два места в
|
||||
`clientconn.go` — конструктор клиента и метод `Close`. К Yandex ни разу не
|
||||
обратился: ни на живых ключах, ни на тестовых.
|
||||
|
||||
## Что выяснилось
|
||||
|
||||
- **`grpc.NewClient` соединения не открывает.** Клиент создаётся в состоянии
|
||||
ожидания, сеть трогается при первом вызове (`clientconn.go:145`). То есть на
|
||||
пути отказа конструктора — когда первый клиент создан, а второй нет — закрывать
|
||||
ещё нечего.
|
||||
- **`(*ClientConn).Close` возвращает ровно два исхода** (`clientconn.go:1142-1156`):
|
||||
`nil` либо `ErrClientConnClosing` — `codes.Canceled`, «grpc: the client
|
||||
connection is closing» (`clientconn.go:67`). Второй наступает **только при
|
||||
повторном закрытии** уже закрытого клиента.
|
||||
|
||||
## Что из этого следует для нас
|
||||
|
||||
Отказ `Close` в этом проекте означает **нашу ошибку — закрыли дважды**, а не сбой
|
||||
или недоступность Yandex. Поэтому запись в журнале при остановке процесса
|
||||
адресует владельца к нашему коду; так она и сформулирована.
|
||||
|
||||
Обработка отказа при этом оставлена в обоих местах, хотя сегодня он практически
|
||||
недостижим: она стоит одну строку и переживёт смену клиента, а её отсутствие
|
||||
пришлось бы обосновывать заново каждому читателю. Решение и его цена —
|
||||
[ADR](../adr/ADR-2026-08-11-errcheck-check-blank.md), обоснование целиком — в
|
||||
архивном
|
||||
[design.md](../../openspec/changes/archive/2026-08-11-errors-as-instead-of-typecast/design.md),
|
||||
Решение 2.
|
||||
|
||||
**Наблюдение привязано к версии.** Сменится мажорная версия `grpc` — перечень
|
||||
исходов `Close` надо перечитать, а не считать его прежним.
|
||||
+48
-6
@@ -63,15 +63,30 @@
|
||||
структуру, чьи теги и составляют проверяемый контракт, меняется вместе с ним
|
||||
и никогда не ловит поломку; такое судят по сырому виду ответа. Признак ищется
|
||||
мутацией: сломай проверяемое свойство и убедись, что тест краснеет (журнал,
|
||||
запись 2026-08-11).
|
||||
запись 2026-08-11);
|
||||
- **то же и об оракуле критерия приёмки, не только о тесте.** Критерий, чей
|
||||
единственный оракул — молчание линтера, годится ровно тогда, когда линтер
|
||||
краснеет на **всех** негодных реализациях; проверяется той же мутацией.
|
||||
Прецедент: «отказ `Close` не теряется молча» принимался молчанием `errcheck`,
|
||||
а тот пропускал `_ = conn.Close()` — реализацию, теряющую отказ целиком
|
||||
(журнал, запись 2026-08-11 про недостижимую норму; закрыто
|
||||
[решением](adr/ADR-2026-08-11-errcheck-check-blank.md));
|
||||
- **требование без сценария не имеет оракула** и потому не может быть нарушено
|
||||
заметно. Норма, которую нечем уронить, расходится с кодом молча — и расходится
|
||||
тем вернее, чем убедительнее написана (журнал, запись 2026-08-11).
|
||||
|
||||
### Типовые ложноположительные
|
||||
|
||||
- **«Воркер глотает ошибку `NoopJobError`».** Не дефект: этот тип означает «задач
|
||||
в этом состоянии нет», и `internal/controller/worker/worker.go` намеренно не
|
||||
логирует его и не считает в метрику. Настоящий дефект рядом другой — проверка
|
||||
идёт приведением типа и сломается при первой же обёртке; он уже записан в
|
||||
[conventions/errors.md](conventions/errors.md).
|
||||
логирует его и не считает в метрику. Норма записана требованием
|
||||
[pipeline](../openspec/specs/pipeline/spec.md).
|
||||
|
||||
**Оговорка, и она тут главная:** ложноположительным считается только само
|
||||
молчание воркера. Проверка **формы** узнавания ложноположительной не является:
|
||||
приведение типа на этом месте — настоящий дефект, закрытый 2026-08-11 задачей
|
||||
`errors-as-instead-of-typecast`. Появилось снова — это регрессия, и выбрасывать
|
||||
её как известную нельзя.
|
||||
- **«Захват задачи не в транзакции — гонка двух воркеров».** По построению её
|
||||
нет: три воркера читают три разных состояния, и одну строку они не делят.
|
||||
Механика захвата и её слабые места — [database.md](database.md),
|
||||
@@ -110,8 +125,9 @@
|
||||
`createTranscribeJob` — сегодня через него идут оба входа
|
||||
([architecture.md](architecture.md), «Единые точки проекта»).
|
||||
- `architecture`: не поехало ли поведение в `architecture.md` вместо спеки —
|
||||
заведена одна capability (`openspec/specs/intake`), поведение прочих узлов
|
||||
живёт в обзоре под маркерами долга, и соблазн дописать туда ещё максимальный.
|
||||
заведены две capability (`openspec/specs/intake` и `openspec/specs/pipeline`),
|
||||
и каждая описана частично. Поведение прочих узлов живёт в обзоре под маркерами
|
||||
долга, а соблазн дописать туда ещё — самый большой.
|
||||
- `conventions`: новая колонка правится во всех четырёх местах репозитория
|
||||
(CLAUDE.md, «Инварианты»).
|
||||
- `autotests`: покрыт ли изменённый шаг конвейера хоть одним тестом — сегодня
|
||||
@@ -181,6 +197,32 @@ API и имя не откатываются обратной правкой по
|
||||
поймать их было некому. У восстановленных нет поля «Чем воспроизведён», и
|
||||
выдумывать его задним числом нельзя.
|
||||
|
||||
## 2026-08-11 — норма требовала от сервиса недостижимого [пойман ревью]
|
||||
|
||||
- **Где:** дельта-спека `pipeline` задачи `errors-as-instead-of-typecast`, абзац
|
||||
об отказе шага
|
||||
- **Симптом:** требование гласило «отказ MUST быть записан **ровно один раз**
|
||||
единственной логирующей точкой». Сервис пишет дважды — сначала шаг конвейера,
|
||||
следом воркер, — то есть норма не выполнялась бы с первого дня, а после
|
||||
архивации стала бы посылкой для следующих задач
|
||||
- **Причина:** дефект родился при починке соседнего. Первая редакция назначала
|
||||
логирующей точкой воркера и фиксировала уровень `ERROR`, чем закрепляла
|
||||
контрактом долг `conventions/logging.md`. Правка по этой находке ушла в
|
||||
противоположную крайность: вместо «норма молчит о числе записей» получилось
|
||||
«норма требует одной». Двойная запись — записанный системный долг, и обе
|
||||
редакции с ним расходились, только в разные стороны
|
||||
- **Чем воспроизведён:** прогоном пробы через `go test -overlay`: один отказ
|
||||
хранилища даёт две записи — `Failed to find and acquire job` из шага и
|
||||
`Worker error` из воркера
|
||||
- **Почему не поймали раньше:** требование не имело сценария, а значит и оракула
|
||||
— упасть ему было нечем. Ревью дизайна абзац читало, но код с ним не сверяло:
|
||||
кода тогда не существовало. Поймал проход `specs` на ревью кода, направлением
|
||||
`code → spec`, и поймал прогоном, а не чтением
|
||||
- **Что меняем:** норма говорит только проверяемое сегодня — отказ виден
|
||||
владельцу и засчитан в счётчик; число записей и уровень названы долгом с
|
||||
адресом. В критерии приёмки добавлена строка: норма не объявляет обязательным
|
||||
недостижимое — ни в ту, ни в другую сторону
|
||||
|
||||
## 2026-08-11 — хвост имени отправителя уезжал на открытую страницу метрик [пойман ревью]
|
||||
|
||||
- **Где:** `internal/service/transcribe.go`, метки `file_extension` у размера
|
||||
|
||||
Reference in New Issue
Block a user