Отложенные Tier B/C по итогам ревью Fable (продолжение f8fb4fa): sentinel-
обёртки ErrConflict/валидации, classifyErr для layout.ErrCollision, единая
категория переходов, правила уровней и *url.Error/секретов в docs/conventions,
решение по сырому err.Error() в reasons/error_msg.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.5 KiB
Классификация доменных ошибок (500→409/400) и конвенции логирования
Приоритет: средний · Теги: review-fable, errors, logging
Продолжение коммита f8fb4fa (логирование ошибок: доменная граница + защита
секретов). Тот коммит закрыл «ядро + секреты» (Tier A) по итогам ревью двумя
сабагентами Fable (инфраструктурные и доменные ошибки). Здесь — отложенные
Tier B (классификация) и Tier C (конвенции + политика), не вошедшие в scope.
Tier B — классификация ошибок (сейчас штатные конфликты/валидация → 500)
Проблема: часть доменных отказов возвращается голым fmt.Errorf без sentinel,
поэтому classifyErr (httpapi) отправляет их в default → 500 «внутренняя
ошибка» вместо 409/400. Побочно: новый logCmd (worker) логирует такие
отказы как ERROR, хотя это норма (адресат — пользователь), должно быть
DEBUG.
- Обернуть
ErrConflictв командах, где конфликт состояния не обёрнут (в отличие отrequireReviewable/Apply/Undo/Delete, где уже сделано):worker.go:789—cancel: ... is already terminal;worker.go:809—retry: ... only failed/stuck are retriable;review.go:515—defer: ... is terminal;review.go:551—undo: nothing to revert(скорее конфликт).
- Sentinel для валидации ввода (
ErrInvalidInput→ 400) или дооборачивание, чтобы промах пользователя не выглядел сбоем ни в статусе, ни в уровне лога:review.go—refine: empty hint,set type: invalid type,ignore: empty path,add source: invalid provider/empty id,set provider: invalid provider/empty id,choose candidate: candidate ... does not belong.
classifyErrне знаетlayout.ErrCollision: ручной Apply с коллизией цели штатно уводит задачу в review с причиной (review.golinkPlan), но пользователь в вебе получает 500, а tgbot — «Не удалось выполнить действие». Добавить кейс (409 или сценарий «ушло в ревью: коллизия цели») и исключение в tgbot по аналогии сErrNotReady.
После sentinel-обёрток обновить logCmd (worker.go) — он уже относит
ErrConflict/ErrNotReady/ErrNotFound в DEBUG; добавить туда новый
ErrInvalidInput.
Tier C — конвенции и политика (docs/conventions)
Оба ревьюера предложили закрепить в logging.md/errors.md (чтобы дыры и
дубли не возникали снова — уже дважды выстрелило: ingest, затем команды):
- Команды воркера = доменная граница. Явно дописать в
logging.md, раздел «Ошибки»: публичные методы воркера (Apply/Refine/Cancel/…) — граница домена, логируют исход ровно один раз (реализовано вlogCmd); транспорты не логируют возвращённую ошибку. Сейчас формулировка «стадии воркера» двусмысленна. - Таблица уровней доменных отказов:
ErrConflict/ErrNotReady/валидация → DEBUG (адресат — пользователь); нарушенный инвариант хранилища → INFO/WARN; прочее (БД/ФС/зависимости) → ERROR. Правило: у каждой доменной ошибки ровно один логирующий, уровень — по адресату, а не по месту. - Правило про
*url.Error/секреты в URL (раздел «Безопасность»): ошибки HTTP-транспорта встраивают URL, который может нести секрет — санитизировать на границе клиента до лога и обёртки. Частично реализовано (logging.SanitizeErr, применён вExtCall/tgbot/metadata) — осталось задокументировать + правило «секрет не кладём в URL, если у API есть заголовок». - Уровень повторяющихся сбоев фоновых циклов:
poll/sweep/list failed= WARN, а та же ошибка БД вingest.Ingest= ERROR. Договориться о едином правиле (транзиентный сбой тика → WARN; эскалация в ERROR при устойчивом сбое N тиков) — сейчас уровень зависит от места. Смежно: ERROR-шторм при недоступном qBittorrent (qbtлогирует Failure на каждом тике поллинга). - Единая категория переходов состояния: свести к одному msg
state transition(from/to/code);download cancelled/download retried/relink re-recognizingне теряются из-подjq 'select(.msg=="state transition")'.
Развилка — сырой err.Error() на публичных поверхностях
reasons (review.go recognizeOne) и error_msg переходов (linkPlan)
показываются в review-экране и Telegram-карточке и могут нести детали
реализации (тело LLM/qBit, абсолютные пути). Решить: (а) узаконить в errors.md
как «операторская поверхность владельца» с явным запретом секретов; либо (б)
держать там reason-код + нейтральный текст, полную ошибку — в лог по
download_id.
Мелочь (по желанию, из тех же ревью)
httpapi/review.go— поле"id"вместо словарного"download_id"; msg «review data» → категория «review data failed».worker/review.goparseIgnored—_ = json.Unmarshalбез обязательного комментария «почему» (битый override молча обнуляет игнор).- Reason-коды переходов (
"resolve"/"build"/"persist"/"collision"/…) — свести в const-блок рядом сerrCode*(сейчас толькоreasonTitleFolderDesync). - Превью в
ReviewDataпри сбое — DEBUG, хотя это видимая деградация (пропадает кнопка «Применить»); уместнее WARN.
Вердикт: change (Tier B — правки кода + миграция статусов ошибок; Tier C —
правки конвенций + одно продуктовое решение по err.Error()).