доменные ошибки сравниваются через errors.As, отказ Close не теряется
- признаки «работы нет» и «задача не найдена» узнаются по смыслу, а не приведением типа: обёртка `%w` на пути больше не превращает пустой прогон воркера в отказ раз в секунду - отказ закрытия соединения с распознавателем доходит до вызывающего (`errors.Join`) либо до журнала; у `errcheck` включён `check-blank`, иначе критерий принимал реализацию, выбрасывающую отказ в пустоту - заведены первые тесты пакета worker и capability `pipeline`; долг из четырёх замечаний линтера закрыт, гейт зелёный целиком
This commit is contained in:
+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