layout: непомещающееся целевое имя уводит задачу в review вместо failed

- предел длины компонента (255 байт) проверяется в BuildLinks до первой
  операции с ФС: ни каталога, ни ссылки при отказе не создаётся
- причина пустого предпросмотра считается на показе (ReviewData.PreviewError)
  и печатается в панели действий и в карточке Telegram: у задачи без
  записанной причины взять её больше неоткуда
This commit is contained in:
av
2026-08-10 12:16:44 +03:00
parent 1710e5a9d5
commit b9f0929d0c
31 changed files with 1511 additions and 17 deletions
@@ -0,0 +1,161 @@
# Ревью изменения `long-title-to-review` — триаж
Отчёт агента `review-triage`. Записан оркестратором: харнесс блокирует запись
файлов подагентами.
## Сводка
- **Размер:** 9 изменённых файлов (+180/−5) плюс новый
`internal/layout/name_length_test.go`. Продуктового кода ~47 строк.
- **Метка:** `medium`, назначена человеком; разметчик дал `small`. Режим — по графу.
- **Сигнал о заниженной метке:** от `review-code` — «`medium` адекватна, `small`
был бы занижен: дифф трогает четыре слоя». Триаж поддержал основанием, которого
сам сигнал не назвал: код причины `name_too_long` оседает в
`downloads.error_code` и обратной правкой после мерджа не откатывается.
- **Гейт:** зелёный целиком, подтверждён `autotests` независимо; ни одного `SKIP`,
`-race` реально прогонялся.
- **Находок на вход:** 14 плюс 1 promote-кандидат. **Осталось:** 5 (2 блокируют,
3 к исправлению), 3 понижены в гипотезы, 2 promote.
### План разметки с исходом
| тема | дом | глубина | кто закрывает | исход |
|---|---|---|---|---|
| requirements | `openspec/specs/` + дельты | разбор | specs | закрыта, 3 находки |
| autotests | `CLAUDE.md` → «Гейт» | — | autotests | закрыта, находок нет |
| conventions | `docs/conventions/` | разбор | code | закрыта, 5 находок (склеены в 3) |
| architecture | `docs/architecture.md` | разбор | basics | закрыта, 1 находка |
| security | `docs/security.md` | разбор | basics | закрыта, находок нет |
| operations | `docs/architecture.md` → «Эксплуатация» | разбор | basics | закрыта, 3 находки |
| темы проекта | — | — | — | своих тем нет |
Тем без отчёта нет.
## Блокирует мердж
### 1. На самом вероятном входе экран ревью не называет причину и теряет «Применить»
- Файл: `internal/worker/review.go:1085-1095`, `:240`,
`web/templates/partials/review_main.html:79`
- Severity: major. Confidence: high.
- Оракул: два дерева (`git archive HEAD` против того же среза с наложенным
`git diff HEAD`), один вход — задача в `review` без записанной причины,
название `strings.Repeat("ы", 200)`:
```
HEAD: preview_links=2 (HasLinks=true) → «Применить» показана
Apply → failed, msg=970 байт "…file name too long"
рабочее дерево: preview_links=0 (HasLinks=false) → формы /apply нет
StateError="" → «Подтверди источник…»
```
- Последствие: путь «распознавание без подтверждённого матча → `review`» —
штатный и самый частый вход в ревью. На нём экран советует подтвердить
источник, который ни при чём. Изменение, чья цель «человек узнаёт причину», на
этом входе делает диагностируемость хуже, чем была.
- **Действие: развилка.** Сюда же сняты два спутника той же причины: текст
печатается дважды (баннер + панель) и после смены источника в панели остаётся
причина от плана, которого больше нет.
- Найдено проходами: `specs`, `basics`; склеено триажем.
**Отработано:** причина пустого предпросмотра считается на показе и приезжает
отдельным полем (`ReviewData.PreviewError`), панель предпочитает её записанной.
Человек выбрал вариант (а) на повторном чекпоинте.
### 2. Карточка Telegram в `review` не называет причину вовсе
- Файл: `internal/tgbot/render.go:70-95`, `internal/tgbot/bot.go:396-415`
- Severity: major. Confidence: high.
- Оракул: рендер карточки на рабочем дереве, задача в `review` с записанной
причиной `name_too_long` и пустым предпросмотром — упоминает причину `false`,
кнопки «Применить» нет. Контроль: та же причина в состоянии `failed` (исход до
изменения) печатала текст и давала кнопку «Повтор».
- Последствие: владелец видит «Нужно подтверждение» без плана, без кнопки и без
слова о длине имени. `proposal.md` обещал обратное; `docs/architecture.md`
«Единые точки» называет обе границы трансляции, обновлена была одна.
- **Действие: развилка.**
**Отработано:** карточка печатает причину для любого случая (вариант (а)), плюс
своя ветка ответа в колбэке, как у коллизии.
## Стоит исправить сейчас
### 3. Штатный отказ краснит логи как сбой, и три предписанных реестра о новой ветке не знают
- Файл: `internal/worker/worker.go:962-968`, `docs/conventions/errors.md:81-97`,
`docs/database.md:210-214`
- Severity: minor. Confidence: high.
- Оракул: прогон ручного `Apply` с логгером Debug — `level=ERROR msg="command
failed"`. `docs/conventions/logging.md:154` относит штатный конфликт к `DEBUG`
и ставит `layout.ErrCollision` поимённо.
- **Действие: инлайн.** **Отработано** целиком: `logCmd`, таблица `errors.md`,
блок констант `database.md`, клауза 409 в дельте `review`.
### 4. Текст причины несёт непомещающееся имя целиком — 494 байта
- Файл: `internal/layout/name.go:29-31`
- Severity: minor. Confidence: high.
- Оракул: `Apply` с названием `strings.Repeat("ы", 200)` → `msg_len_bytes=494`.
- **Действие: инлайн.** **Отработано:** имя усекается серединой (`shorten`, 40
рун), точная длина остаётся числом.
### 5. Два сценария дельты не проверены ничем; комментарий у теста границы описывает не тот вход
- Файл: дельта `file-layout`, `internal/layout/name_length_test.go:109-113`
- Severity: minor. Confidence: high.
- **Действие: инлайн.** **Отработано, и по одному из сценариев — не так, как
предлагал триаж.** Попытка написать тест на «унаследованную базу» показала, что
случай **недостижим**: папка-якорь лежит на диске и потому ≤ предела, а хвост
имени файла (11 байт) не длиннее хвоста имени папки (≥ 11 байт). Сценарий
переписан на верное утверждение, свойство закреплено тестом
`TestApply_InheritedBaseAtLimitStillFits`. Комментарий у границы исправлен,
кириллическая граница добита.
## Гипотезы без доказательства
- Тупик с унаследованной базой не назван в тексте причины (`basics`, B-4).
**Снято разбором:** случай недостижим, см. находку 5.
- Рубрика R6 в `tasks.md` противоречит сценарию S6 дельты. **Снято:** после
переписывания сценария противоречия нет.
- Панель печатает любой `error_msg` без разбора кода, включая сырые тексты ошибок
хранилища. Не отработано: тот же текст уже печатал баннер до изменения, нового
канала не появилось.
## Promote candidates
- **Язык текстов sentinel-ошибок:** русский у тех, что уходят на операторскую
поверхность, английский у прочих; правила в `docs/conventions/errors.md` нет.
- **Внешний текст в персистентной диагностике — только усечённым.** В проекте
есть и `shorten`, и `naming.truncate`, но правила «то, что уезжает в
`error_msg`, усекается на границе» нет; отсюда находка 4.
## Границы покрытия
**Что запускалось.** Метка `medium`, режим по графу: `autotests`, `specs`,
`code`, `basics`, `triage`. Ревью дизайна (`specs` + `rubric`) отработало
отдельным чекпоинтом до кода.
**Что не запускалось и почему.** Всё, что требует запуска — построенные пути
атаки, замеры под нагрузкой, эксплуатационный постмортем, — даёт только метка
`large`. Живые qBittorrent, LLM, метабазы и Telegram не трогались; рабочая БД не
трогалась.
**Чего не мог каждый проход.** `autotests` — отличить исполнение строки от её
проверки. `specs` — судить о качестве распознавания. `basics` на `medium` —
строить пути и снимать замеры. `code` — сверять с руководствами по стилю языка.
Триаж — находить новое: он работает с чужими выводами.
**Потолки проходов.** Ни один проход не сообщил ни своего потолка, ни числа
находок за срезом. Это находка о прогоне: по молчанию нельзя отличить «показал
всё» от «показал верхушку».
**Не проверялось никем.** Решения проекта (`docs/adr/`) и записанные наблюдения
(`docs/research/`) — процессные документы, прогон их не открывает; расхождение с
записанным решением ловит `av-dev-docs:healthcheck`. Поимённая сверка с
руководствами по стилю Go никем не задавалась. Альтернативной реализации, с
которой можно сдиффить решения, у конвейера нет.
**Осталось на человеке.** История инцидентов на umbar; поведение SQLite под
реальным объёмом; завязка внешних потребителей на текущее поведение; суждение
«этой функциональности не должно существовать»; качество распознавания;
идиоматичность Go (проход упразднён 2026-08-04).