спека: заведён change large-release-recognition для больших раздач

- контракт с моделью переводится на адресацию файла номером строки списка,
  усечение промпта снимается, лимиты уходят в конфиг
- раскладка показывает все файлы раздачи, покрытие плана блокирует авто
  только при непокрытом видеофайле
This commit is contained in:
av
2026-09-02 08:54:32 +03:00
parent 2ac26bf8ed
commit 01ac0430a9
6 changed files with 685 additions and 0 deletions
@@ -0,0 +1,92 @@
## Why
Раздача «Отчаянные домохозяйки» — один торрент на 8 сезонов, 180 файлов — распозналась
негодно: в план попало ровно 100 файлов, потому что промпт усекает список файлов жёсткой
константой `defaultMaxFiles = 100`. Остальные 80 файлов модель не видела, в интерфейсе они
не показаны никак, а причины ревью распухли до сорока строк «пропуск серий в сезоне N».
Порядок строк раскладки в UI — тот, в каком их вернула модель, то есть вперемешку по
сезонам.
Просто поднять лимит нельзя: каждый элемент плана обязан нести дословную копию пути
(~70 токенов на кириллице), 180 файлов дают ~15k выходных токенов при нашем же потолке
`defaultMaxTokens = 4000`. Обрыв генерации при этом неотличим от мусора — `finish_reason`
из ответа разбирается, но не проверяется, и обрыв уходит в три бессмысленных ретрая
с полным промптом. Вход тут не проблема (12–15k токенов промпта — ничто для современной
модели), проблема целиком на стороне ответа.
## What Changes
- **BREAKING (контракт с моделью, не с пользователем)**: элемент `files[]` в ответе LLM
адресует файл номером из напечатанного нами списка (`i`), а не дословной копией пути.
`src` заполняем сами резолвом индекса. Выход падает с ~85 до ~14 токенов на файл; путь,
которого нет в торренте, становится невыразимым. Старый формат с `src` принимается как
запасной, чтобы привычка модели не давала ретрая на пустом месте.
- Негодный элемент ответа (номер вне диапазона, повторная адресация файла, элемент без
номера и пути) отбрасывается поимённой причиной, а не роняет весь план: иначе одна
ошибка в одном из 180 элементов воспроизводит ровно тот отказ, против которого change и
затевается. Элемент, несущий и номер, и путь, резолвится по номеру, а расхождение между
ними становится причиной ревью — бесплатной сверкой против сдвига адресации.
- Жёсткое усечение списка файлов снимается: модели показываются все файлы раздачи.
`max_files` и `max_tokens` выводятся в `[recognition]` с дефолтами 500 и 8000 — сейчас
оба зашиты в код и на проде не крутятся.
- Повторная попытка перестаёт переприсылать список файлов: номера названы в первом
сообщении диалога, и цена неудачного распознавания больше не растёт вместе с числом
попыток.
- Порядок файлов, напечатанных в промпте, детерминирован и совпадает с порядком резолва
индексов — иначе повторное распознавание из ревью сдвинет привязку.
- Обрыв генерации (`finish_reason` = `length`) распознаётся как отдельная причина ухода
в review и не ретраится тем же промптом.
- В раскладке видны все файлы торрента: попавшие в план — с ролью и целью, не попавшие —
отдельной строкой. Молчаливый пропуск файла моделью перестаёт быть невидимым. Загрузки,
распознанные до этого изменения, списка файлов не имеют — и виджет говорит об этом
прямо, вместо того чтобы показать план как полный перечень.
- Строки раскладки упорядочены полностью — разложенные по (сезон, серия, путь), затем
файлы плана без цели, затем файлы вне плана, — а не по порядку ответа модели.
- Причины ревью по нумерации серий сворачиваются: перечисление недостающих серий по
сезону одной строкой и сводка покрытия плана вместо строки на каждый разрыв. Покрытие
показывается, когда покрыты не все файлы, но блокирует авто-раскладку только
непокрытый видеофайл: иначе опущенный моделью `.nfo` выключил бы авто-путь на типовых
раздачах. Отсюда же следствие для решения: `Reasons` перестают быть однородным
списком «причин не-авто» — в них появляются информационные строки, и решение
считается по блокирующим причинам, а не по длине списка.
Вне объёма: детерминированный разбор сезона/серии из имён файлов и сверка с ответом
модели — следующий change (этап B). Здесь модель по-прежнему единственный источник
разметки файлов.
## Capabilities
### New Capabilities
Нет.
### Modified Capabilities
- `recognition`: контракт ответа LLM (адресация файла индексом вместо дословного пути),
отказ от усечения списка файлов и вывод лимитов в конфиг, обрыв генерации как
отдельная причина review, форма причин по нумерации серий, решение auto/review
считается по блокирующим причинам (в `Reasons` появились информационные строки).
- `web-ui`: раскладка показывает все файлы раздачи, включая не попавшие в план, и
упорядочена по сезону и серии.
## Impact
- `internal/recognize`: `prompt.go` (нумерация и печать списка, схема в промпте,
correction-сообщение без повторной печати списка), `validate.go` (резолв индекса вместо
поиска пути, отбраковка негодных элементов, свёртка предупреждений, покрытие по
видеофайлам), `recognize.go` (упорядочивание входного списка в самом узле, лимиты,
обработка обрыва).
- `internal/llm`: `finish_reason` доводится до вызывающего в `Response`.
- `internal/config`: `[recognition].max_files`, `[recognition].max_tokens`;
`config.example.toml` и `docs/database.md` (настройки с числовым значением).
- `internal/httpapi/files.go` и `web/templates/partials/layout_widget.html`: сортировка,
файлы вне плана, колонка `#` (номер строки виджета обязан совпасть с номером адресации
либо исчезнуть), строка о недоступном списке файлов.
- Хранилище: миграция `0012` — колонка со списком файлов раздачи в таблице `recognition`,
и правка ER-схемы в `docs/database.md` (без неё краснеет шаг `canon` гейта).
- Совместимость: сохранённые планы в БД хранят `src` путями и не меняются. Записи,
созданные до миграции, списка файлов не имеют — виджет называет это явно. Уже
применённые раскладки не затрагиваются.
- Риск: ошибка модели в индексе становится тихой (валидный, но не тот файл). Смягчение —
требование уникальности индексов и сводка покрытия в причинах; перекрёстная проверка
разметки приходит в этапе B.