доменные ошибки сравниваются через errors.As, отказ Close не теряется

- признаки «работы нет» и «задача не найдена» узнаются по смыслу, а не
  приведением типа: обёртка `%w` на пути больше не превращает пустой прогон
  воркера в отказ раз в секунду
- отказ закрытия соединения с распознавателем доходит до вызывающего
  (`errors.Join`) либо до журнала; у `errcheck` включён `check-blank`, иначе
  критерий принимал реализацию, выбрасывающую отказ в пустоту
- заведены первые тесты пакета worker и capability `pipeline`; долг из четырёх
  замечаний линтера закрыт, гейт зелёный целиком
This commit is contained in:
av
2026-08-11 18:04:25 +03:00
parent b7dd060ba0
commit 2559d09fc8
24 changed files with 1554 additions and 48 deletions
+6 -2
View File
@@ -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` |
+8 -9
View File
@@ -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 и типизированные