спека: заведён change large-release-recognition для больших раздач
- контракт с моделью переводится на адресацию файла номером строки списка, усечение промпта снимается, лимиты уходят в конфиг - раскладка показывает все файлы раздачи, покрытие плана блокирует авто только при непокрытом видеофайле
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-02
|
||||
@@ -0,0 +1,231 @@
|
||||
## Context
|
||||
|
||||
Распознавание собирает промпт в `internal/recognize/prompt.go` и разбирает ответ в
|
||||
`validate.go`. Список файлов печатается нумерованным (`1. путь (размер)`), но номер
|
||||
служит только человеку: модель обязана вернуть `files[].src` дословной копией пути, а
|
||||
`validateSchema` ищет эту строку среди реальных файлов торрента (`validate.go:75`). Это и
|
||||
есть главный инвариант безопасности недоверенного выхода: сослаться на посторонний путь
|
||||
нельзя.
|
||||
|
||||
Цена контракта видна на больших сериальных паках. Путь вида
|
||||
`Отчаянные домохозяйки (Сезон 1-8 из 8) Дубляж/Сезон 8/Desperate.Housewives.S08E13.WEB-DLRip.Rutracker.org.avi`
|
||||
— около 70 токенов на кириллице; с ролью и номерами элемент плана стоит ~85 выходных
|
||||
токенов. Раздача на 180 файлов требует ~15k токенов ответа при зашитом
|
||||
`defaultMaxTokens = 4000`, а список в промпте заранее подрезан `defaultMaxFiles = 100`.
|
||||
Ни то, ни другое в конфиг не выведено. Отсюда все наблюдаемые симптомы: ровно 100 файлов
|
||||
в плане, невидимые 80, сорок строк причин про пропуски серий.
|
||||
|
||||
Вход при этом не жмёт: 180 путей — это 12–15k токенов промпта, для рабочей модели
|
||||
(`deepseek-v4-flash` через bothub) это не проблема. Дорог и хрупок именно ответ, и дорог
|
||||
он ровно тем, что модель переписывает наши же строки.
|
||||
|
||||
Дополнительное ограничение задаёт следующий этап: разметку файлов будет проверять
|
||||
детерминированный разбор имён, а для этого источники должны остаться независимыми. Всё,
|
||||
что похоже на «подсказать модели наш ответ», в этом change недопустимо.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Раздача на сотни файлов распознаётся целиком, без усечения списка и без обрыва ответа.
|
||||
- Ошибка модели видна: непокрытые файлы показаны, покрытие названо числом.
|
||||
- Лимиты распознавания настраиваются на проде без пересборки.
|
||||
- Инвариант «`files[].src` — реальный файл торрента» не ослабевает.
|
||||
- Цена неудачной попытки не растёт вместе с размером раздачи.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Детерминированный разбор сезона/серии из имён файлов и сверка с моделью — этап B.
|
||||
- Батчирование запроса по пачкам файлов: при компактном формате ответа нужды нет, а
|
||||
склейка шапки между пачками добавляет свой класс расхождений.
|
||||
- Групповка раскладки по сезонам со свёрткой в UI: раздача на 180 строк читается, если
|
||||
строки упорядочены; свёртка — отдельная задача интерфейса.
|
||||
- Пересмотр модели уверенности и гейта авто-раскладки. Единственное исключение — новая
|
||||
причина покрытия, и она заведена так, чтобы гейт не двигать (см. решение ниже);
|
||||
всё остальное в решении auto/review остаётся как есть.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Файл адресуется номером строки нашего списка
|
||||
|
||||
`files[]` несёт `i` — номер из напечатанного нами перечня (1-based, в порядке печати).
|
||||
`src` заполняет резолв на разборе: `in.Files[i-1].Path`. Валидация вырождается в проверку
|
||||
диапазона, и посторонний путь становится невыразимым — инвариант усиливается, а не
|
||||
слабеет. Выход падает до ~14 токенов на файл: 180 файлов укладываются в 2.5k, то есть в
|
||||
нынешний потолок даже без его подъёма.
|
||||
|
||||
Альтернативы:
|
||||
|
||||
- *Оставить дословный `src`, подняв лимиты.* Отвергнуто: 15k выходных токенов — это минуты
|
||||
генерации при `[llm].timeout = 120s`, риск упереться в потолок ответа провайдера и
|
||||
сохранение главного источника отбраковки (одна опечатка в одном из 180 путей роняет
|
||||
весь план, дальше correction-ретрай целиком).
|
||||
- *Индекс файла из qBittorrent.* Отвергнуто: `qbt.File` отдаёт только `name` и `size`
|
||||
(`internal/qbt/qbt.go:86`), собственного индекса у нас нет, а порядок ответа API как
|
||||
контракт нигде не зафиксирован. Номер строки собственного списка не зависит от чужого
|
||||
API вовсе.
|
||||
- *Короткий хеш пути.* Отвергнуто: длиннее индекса в токенах и хуже читается человеком в
|
||||
логах и сыром ответе.
|
||||
|
||||
### Негодный элемент отбрасывается, план живёт
|
||||
|
||||
Номер вне диапазона, повторная адресация одного файла, элемент без номера и без пути —
|
||||
всё это отбраковывает **элемент**, а не план: остальные файлы остаются, задача уходит в
|
||||
`review` с причиной, называющей отброшенное. Первоначальный вариант «отклонять план
|
||||
целиком» отвергнут: он воспроизводит ровно тот отказ, против которого затевается change
|
||||
— одна ошибка в одном из 180 элементов роняет всю работу и запускает correction-ретрай.
|
||||
Решение приведено к действующей норме `recognition` («Роли файлов на краях раздачи»):
|
||||
неоднозначность нумерации эскалируется в `review`, а не разрешается молча и не роняет
|
||||
разбор. Порог здравого смысла — план, где не осталось ни одного годного файла, считается
|
||||
неразобранным.
|
||||
|
||||
При повторной адресации побеждает первый элемент: иначе два элемента претендуют на один
|
||||
файл, а раскладка сделала бы два хардлинка с одного источника на две серии.
|
||||
|
||||
### Номер главенствует над путём, а расхождение — сигнал
|
||||
|
||||
Элемент, несущий и `i`, и `src`, резолвится по номеру. Несовпадение присланного пути с
|
||||
резолвом даёт причину ухода в `review`, а не молчаливое затирание: модель, назвавшая обе
|
||||
стороны, бесплатно предъявляет нам сверку против главного риска этого change — сдвига
|
||||
индекса. Одна ветка в разборе покупает оракул, который иначе появился бы только в этапе B.
|
||||
|
||||
### Повторная попытка не переприсылает список файлов
|
||||
|
||||
Сегодня `Recognize` дописывает в диалог сырой ответ и `correctionMessage`, а тот печатает
|
||||
список файлов заново (`recognize.go:243`, `prompt.go:180`). При зашитом лимите в 100
|
||||
файлов это терпимо; со снятым лимитом четвёртая попытка несла бы четыре копии списка —
|
||||
50–60k входных токенов, то есть изменение, чья цель «раздача на сотни файлов
|
||||
распознаётся целиком», удорожало бы её неудачу. Индексная адресация это и снимает: номера
|
||||
названы в первом сообщении диалога и сохраняют смысл на всех попытках, поэтому
|
||||
correction-сообщение несёт только ошибку и схему.
|
||||
|
||||
### Порядок списка — свойство узла, а не вызывающего
|
||||
|
||||
`Recognize` приводит полученный список файлов к детерминированному порядку сам (сортировка
|
||||
по пути), и печать промпта с резолвом индексов идут по одному и тому же срезу. Сортировка
|
||||
у вызывающего отвергнута: сборка `recognize.Input` не единственная — кроме воркера
|
||||
(`internal/worker/review.go:126`) её делает CLI `jellybit recognize --dry-run`
|
||||
(`cmd/jellybit/recognize.go:101`), и путь, не получивший сортировки, печатал бы номера,
|
||||
означающие другие файлы. Свойство нормировано на узле — значит и механизируется в узле, а
|
||||
тест зовёт `Recognize`, а не его вызывающего.
|
||||
|
||||
Внутри торрента путь уникален, так что порядок воспроизводим между вызовами: повторное
|
||||
распознавание из ревью не сдвинет привязку. Полагаться на порядок ответа qBittorrent
|
||||
нельзя — он не обещан. Побочный эффект приятный: файлы одного сезона идут в промпте
|
||||
подряд.
|
||||
|
||||
### Старый формат принимается как запасной
|
||||
|
||||
Пришёл `i` — резолвим; пришёл `src` — валидируем как сегодня. Модель, повторившая формат
|
||||
из прежних примеров, не должна стоить нам correction-ретрая. Ветка с `src` остаётся и как
|
||||
страховка на случай, если индексная разметка окажется хуже на каких-то раздачах: откат —
|
||||
это смена схемы в промпте, без правки разбора.
|
||||
|
||||
### Обрыв генерации — отдельная причина, а не мусор
|
||||
|
||||
`llm.Response` доводит `finish_reason` до `recognize` (сейчас поле разбирается в
|
||||
`internal/llm/openai.go:97` и выбрасывается). Обрыв по длине даёт review с причиной
|
||||
«ответ модели обрезан» и не повторяется ни тем же промптом, ни его вариантом: обрыв
|
||||
означает, что ответ не поместился, а не что модель ошиблась.
|
||||
|
||||
### Лимиты — в конфиг, дефолты соразмерны промпту
|
||||
|
||||
`[recognition].max_files` и `[recognition].max_tokens`, дефолты 500 и 8000. Прежний
|
||||
`max_files` остаётся предохранителем от аномальной раздачи, а не рабочим ограничением, но
|
||||
его значение обязано соответствовать той же арифметике, которой обоснована безопасность
|
||||
изменения: 180 файлов — 12–15k входных токенов, значит 500 файлов — порядка 35k, что
|
||||
модель принимает. Дефолт 1000 отвергнут именно поэтому: предохранитель, пропускающий
|
||||
раздачу, которая упрётся в контекст, не предохраняет, а лишь меняет внятную причину
|
||||
«список усечён» на 4xx с текстом провайдера. Отказ по размеру запроса называется
|
||||
отдельной причиной.
|
||||
|
||||
### Покрытие плана показывается всегда, блокирует — только по видеофайлу
|
||||
|
||||
`Decision.Auto` — это `len(reasons) == 0` (`validate.go:124`), поэтому любая новая причина
|
||||
автоматически становится блокирующей. Промпт при этом требует покрыть «каждый значимый
|
||||
файл» (`prompt.go:55`): модель вправе опустить `.nfo`, скриншоты и `.txt`, и на типовой
|
||||
фильмовой раздаче покрытие штатно меньше сотни процентов. Причина покрытия в общем виде
|
||||
выключила бы авто-раскладку по всей системе — притом что её пересмотр объявлен Non-Goal.
|
||||
|
||||
Поэтому покрытие разделено: число «в плане N из M» показывается человеку всегда, а
|
||||
блокирует только непокрытый **видеофайл** — он означает потерянную серию или фильм.
|
||||
Файлы-спутники в блокирующую часть не входят. Отсюда требуется различать видео по
|
||||
расширению — в проекте такого места сегодня нет (`grep` по `internal` не находит ни
|
||||
одного упоминания `.srt`/`.mkv` как класса), поэтому перечень заводится один раз и рядом
|
||||
с ролями файлов, а не расползается по вызывающим.
|
||||
|
||||
Альтернатива «потребовать от модели полного покрытия, мусор — ролью `ignore`» отвергнута:
|
||||
она перекладывает на выход модели ещё сотню элементов на больших паках — ровно ту
|
||||
стоимость, которую change снимает.
|
||||
|
||||
### Файлы вне плана показывает раскладка
|
||||
|
||||
`buildFileRows` (`internal/httpapi/files.go:25`) обходит `plan.Files`; файл, который
|
||||
модель не включила в план, не отображается нигде. Обход разворачивается на файлы
|
||||
торрента, план подмешивается по `src`. Файл без записи в плане получает явную строку «не
|
||||
в плане». Порядок строк определён полностью: разложенные (сезон, серия, путь) → файлы
|
||||
плана без цели (`extra`/`sample`/`ignore`, по пути) → файлы вне плана (по пути). Пустые
|
||||
`season`/`episode` — указатели, и свёрнутые в ноль они поставили бы семплы и скриншоты
|
||||
перед первым сезоном; поэтому место таких файлов задано явно, а не выводится из
|
||||
умолчания.
|
||||
|
||||
Колонка `#` виджета сейчас печатает порядковый номер строки
|
||||
(`web/templates/partials/layout_widget.html:8`). После изменения номер файла несёт
|
||||
контракт: он попадает в сырой ответ модели и в логи, и человек, разбирающий «модель
|
||||
адресовала файл 13», обязан увидеть в раскладке тот же файл. Значит колонка либо
|
||||
показывает номер из списка распознавания, либо не показывается вовсе.
|
||||
|
||||
Отсюда зависимость по данным: странице нужен список файлов торрента, а не только
|
||||
сохранённый план. Живой запрос в qBittorrent на рендере — лишний поход в чужой сервис на
|
||||
каждый показ; список файлов приходит вместе с распознаванием и сохраняется рядом с
|
||||
планом (миграция `0012`, колонка в `recognition`). Записи, созданные раньше, списка не
|
||||
имеют — и виджет обязан это **сказать**, а не молча показать план как полный перечень:
|
||||
иначе неполнота неотличима от честного «файлов вне плана нет». Раздача, ради которой
|
||||
change делается, уже лежит в `review`, и полный список она получит при повторном
|
||||
распознавании из ревью.
|
||||
|
||||
### Причины ревью сворачиваются
|
||||
|
||||
`seriesWarnings` (`validate.go:186`) даёт строку на каждый разрыв нумерации — на паке из
|
||||
8 сезонов это 40+ строк. Вместо этого: одна строка на сезон с перечнем недостающих серий
|
||||
и одна сводная строка покрытия. Смысл причин не меняется, меняется форма: причина должна
|
||||
читаться человеком в ревью с одного взгляда.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Ошибка модели в индексе становится тихой.** Перепутанный путь сегодня чаще всего не
|
||||
совпадёт ни с одним файлом и ответ будет отбракован громко; сдвиг индекса на единицу
|
||||
даёт валидный, но не тот файл → серия уедет на соседнее место.
|
||||
*Смягчение:* отбраковка повторной адресации, сверка присланного `src` с резолвом номера,
|
||||
когда модель прислала оба, сводка покрытия в причинах и сверка числа серий с метабазой.
|
||||
Полное смягчение — перекрёстная проверка с разбором имени файла в этапе B, ради которой
|
||||
источники и держатся независимыми.
|
||||
- **Разделение покрытия по типу файла держится на перечне расширений.** Незнакомое
|
||||
расширение видео попадёт в спутники, и потерянная серия не заблокирует авто.
|
||||
*Смягчение:* перечень заводится в одном месте и покрывается тестом; сверка числа серий
|
||||
с метабазой ловит потерю независимо от расширения.
|
||||
- **Список файлов теперь хранится рядом с планом.** Рост объёма БД на больших раздачах и
|
||||
ещё одно место, которое может разойтись с реальностью: раздача, переименованная в
|
||||
qBittorrent после распознавания, даст раскладку по устаревшему снимку.
|
||||
*Смягчение:* хранится то, что и так получено при распознавании; это снимок момента
|
||||
распознавания, и реальная раскладка всё равно идёт от плана. Записи без снимка виджет
|
||||
называет явно.
|
||||
- **Схема в промпте меняется — качество разметки может просесть.** Модель разметит 180
|
||||
файлов хуже, чем 100, просто потому что их больше.
|
||||
*Смягчение:* мерить нечем — размеченного корпуса нет и не будет (`tasks/REJECTED.md`,
|
||||
2026-08-06), поэтому проверка ручная на той самой раздаче, а откат дешёвый (ветка `src`
|
||||
сохранена).
|
||||
- **Дефолт `max_tokens = 8000` может не поддержаться эндпоинтом.** Потолок ответа у
|
||||
провайдера ниже нашего дефолта — получим обрыв.
|
||||
*Смягчение:* обрыв теперь называется явной причиной, а лимит крутится в конфиге без
|
||||
пересборки.
|
||||
- **Множитель стоимости попыток остаётся.** `correctionMessage` снял рост промпта
|
||||
*внутри* диалога, но полный промпт всё равно уходит заново на каждой **транспортной**
|
||||
попытке: `internal/llm` держит зашитые `maxAttempts = 3` (сетевой сбой, 429, 5xx), и
|
||||
они умножаются на бюджет переразбора `[llm].max_retries` (дефолт 3, то есть 1 + 3
|
||||
запроса). В худшем случае — до 12 отправок, а промпт после подъёма `max_files` со 100
|
||||
до 500 стал впятеро тяжелее. Плюс `[llm].timeout` (120 s) при удвоенном `max_tokens`
|
||||
(4000 → 8000) не пересматривался: генерация вдвое длиннее ответа может не уложиться в
|
||||
прежнее окно, и тогда таймаут читается как транспортный сбой и уходит в ретрай.
|
||||
*Смягчение:* оба предела в конфиге и крутятся без пересборки; согласование `timeout` с
|
||||
`max_tokens` проверяется на живом кейсе (задача 9.3), а не выводится расчётом.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,249 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Разбор сигналов LLM в структурированный план
|
||||
|
||||
Система SHALL передавать LLM недоверенные сигналы (имя торрента, дерево файлов с
|
||||
размерами, текстовый контекст и накопленные подсказки, пред-парс) и получать
|
||||
структурированный план в схеме: `type` (`movie`|`series`), `title`,
|
||||
`original_title`, `year`, `provider_hint`, `files[]` и `confidence`. Каждый
|
||||
элемент `files[]` SHALL адресовать файл раздачи номером строки из напечатанного
|
||||
системой списка файлов, а не копией пути, и нести `role`
|
||||
(`main`|`episode`|`subtitle`|`extra`|`sample`|`ignore`) и, для сериала,
|
||||
per-file `season`/`episode` (отдельного скалярного `season` быть SHALL NOT — так
|
||||
выражаются мультисезонные паки и спецвыпуски). Путь файла (`src`) SHALL
|
||||
подставлять сама система резолвом номера по своему списку. Ответ, где элемент
|
||||
`files[]` несёт путь вместо номера, система SHALL принимать как запасной формат,
|
||||
проверяя совпадение пути с реальным файлом торрента. Элемент, несущий и номер, и
|
||||
путь, система SHALL резолвить по номеру; несовпадение присланного пути с
|
||||
резолвом номера SHALL давать причину ухода в `review` (сигнал сдвига адресации),
|
||||
а не молчаливое затирание.
|
||||
|
||||
Негодный элемент `files[]` — номер вне диапазона списка, повторная адресация уже
|
||||
адресованного файла, отсутствие и номера, и пути — система SHALL отбрасывать с
|
||||
причиной ухода в `review`, сохраняя остальной план; при повторной адресации в
|
||||
плане SHALL оставаться первый элемент. Отбраковка отдельных элементов SHALL NOT
|
||||
считаться ошибкой разбора и SHALL NOT порождать повторный запрос к модели. План,
|
||||
в котором после отбраковки не осталось ни одного файла, система SHALL считать
|
||||
неразобранным.
|
||||
|
||||
Порядок списка файлов SHALL быть свойством самого распознавания, а не
|
||||
дисциплиной вызывающего: распознавание SHALL приводить полученный список к
|
||||
детерминированному порядку само, печатать промпт и резолвить номера по одному и
|
||||
тому же упорядоченному списку. Повторное распознавание той же раздачи SHALL
|
||||
давать ту же нумерацию.
|
||||
|
||||
План MAY дополнительно нести опциональное скалярное поле `director` (режиссёр).
|
||||
Это поле НЕ требуется от LLM и НЕ участвует в структурной валидации или гейте
|
||||
авто-раскладки; его заполняют подтверждённый матч метабазы (авто) или закреплённый
|
||||
в ревью выбранный источник (через override, см. `metadata-match`/`review`) как
|
||||
недоверенное косметическое значение для вывода отображаемого имени. Как недоверенное
|
||||
человекочитаемое поле, `director` SHALL NOT входить в plan-санитайзинг (он чистит
|
||||
`title`/`original_title`/`provider_hint`); очистка режиссёра применяется при рендере
|
||||
имени. Пустой `director` SHALL быть штатным (режиссёр неизвестен).
|
||||
|
||||
#### Scenario: План сериала с per-file нумерацией
|
||||
|
||||
- **GIVEN** сезон-пак из 10 видеофайлов
|
||||
- **WHEN** LLM возвращает план
|
||||
- **THEN** `type` = `series`, а каждый видеофайл несёт свои `season`/`episode`
|
||||
|
||||
#### Scenario: Номер файла резолвится в путь
|
||||
|
||||
- **GIVEN** список из 180 файлов, напечатанный в промпте
|
||||
- **WHEN** модель возвращает элемент плана с номером 13
|
||||
- **THEN** система подставляет в `src` путь тринадцатого файла своего списка
|
||||
|
||||
#### Scenario: Номер вне диапазона отбрасывает элемент, а не план
|
||||
|
||||
- **GIVEN** раздача из 180 файлов и ответ модели, где один элемент несёт номер 181,
|
||||
а остальные 179 корректны
|
||||
- **WHEN** план разбирается
|
||||
- **THEN** негодный элемент отброшен, остальные 179 файлов остаются в плане
|
||||
- **AND** задача уходит в `review` с причиной, называющей отброшенный элемент
|
||||
- **AND** повторный запрос к модели не выполняется
|
||||
|
||||
#### Scenario: Повторная адресация одного файла
|
||||
|
||||
- **GIVEN** ответ модели, где два элемента адресуют один и тот же файл
|
||||
- **WHEN** план разбирается
|
||||
- **THEN** в плане остаётся первый элемент, второй отброшен
|
||||
- **AND** задача уходит в `review` с причиной, называющей повторную адресацию
|
||||
|
||||
#### Scenario: Все элементы негодны — план не разобран
|
||||
|
||||
- **GIVEN** ответ модели, где ни один элемент `files[]` не адресует реальный файл
|
||||
- **WHEN** план разбирается
|
||||
- **THEN** план не принимается как валидный
|
||||
|
||||
#### Scenario: Путь вместо номера принимается как запасной формат
|
||||
|
||||
- **GIVEN** ответ модели, где элемент `files[]` несёт путь файла вместо номера
|
||||
- **WHEN** план разбирается
|
||||
- **THEN** разбор успешен, если путь совпадает с реальным файлом торрента
|
||||
- **AND** correction-ретрай не выполняется
|
||||
|
||||
#### Scenario: Номер и путь одновременно — главенствует номер
|
||||
|
||||
- **GIVEN** элемент `files[]`, несущий и номер, и путь, которые указывают на разные файлы
|
||||
- **WHEN** план разбирается
|
||||
- **THEN** `src` берётся резолвом номера
|
||||
- **AND** задача уходит в `review` с причиной о несовпадении присланного пути с резолвом
|
||||
|
||||
#### Scenario: Несуществующий src отклоняется
|
||||
|
||||
- **GIVEN** ответ LLM, где `files[].src` не совпадает ни с одним файлом торрента
|
||||
- **WHEN** план разбирается
|
||||
- **THEN** такой элемент отбрасывается с причиной, а посторонний путь в план не попадает
|
||||
|
||||
#### Scenario: Нумерация не зависит от вызывающего
|
||||
|
||||
- **GIVEN** два вызова распознавания одной раздачи, получившие список файлов в разном порядке
|
||||
- **WHEN** собирается промпт
|
||||
- **THEN** напечатанная нумерация в обоих вызовах одинакова
|
||||
|
||||
#### Scenario: Режиссёр не требуется от LLM и не влияет на гейт
|
||||
|
||||
- **GIVEN** ответ LLM без поля `director`
|
||||
- **WHEN** план разбирается и оценивается
|
||||
- **THEN** разбор успешен, `director` пуст
|
||||
- **AND** отсутствие режиссёра не влияет на структурную валидацию и решение
|
||||
auto/review
|
||||
|
||||
### Requirement: Провайдер LLM за абстракцией со структурированным выводом
|
||||
|
||||
Доступ к LLM SHALL быть за интерфейсом с выбором реализации по полю `[llm].type`
|
||||
(первый тип — `openai-compat`). Система SHALL запрашивать JSON-режим
|
||||
(`response_format: {"type":"json_object"}`), срезать ```-ограждения и
|
||||
валидировать ответ в Go против схемы плана. При ошибке разбора система SHALL
|
||||
ретраить до `[llm].max_retries`, передавая модели саму ошибку и схему. Если после
|
||||
ретраев ответ не разобран, задача SHALL уходить в `review` (НЕ в `failed`) с
|
||||
причиной «ответ LLM не разобран».
|
||||
|
||||
Повторный запрос SHALL NOT переприсылать список файлов раздачи: список назван в
|
||||
первом сообщении диалога, и его номера сохраняют смысл на всех попытках. Размер
|
||||
запроса SHALL NOT расти пропорционально числу попыток.
|
||||
|
||||
Признак завершения генерации (`finish_reason`) SHALL доходить до распознавания.
|
||||
Ответ, оборванный по длине, система SHALL уводить в `review` с отдельной
|
||||
причиной «ответ модели обрезан» и SHALL NOT повторять запрос ни тем же промптом,
|
||||
ни его вариантом: обрыв означает, что ответ не поместился, а не что модель
|
||||
ошиблась.
|
||||
|
||||
#### Scenario: Неразобранный ответ уходит в review
|
||||
|
||||
- **GIVEN** LLM, чей ответ не проходит валидацию схемы после всех ретраев
|
||||
- **WHEN** завершается распознавание
|
||||
- **THEN** задача переходит в `review` с причиной «ответ LLM не разобран»
|
||||
- **AND** задача НЕ переходит в `failed`
|
||||
|
||||
#### Scenario: Повторная попытка не переприсылает список файлов
|
||||
|
||||
- **GIVEN** раздача из 180 файлов и ответ модели с ошибкой разбора
|
||||
- **WHEN** выполняется correction-ретрай
|
||||
- **THEN** список файлов раздачи повторно не печатается
|
||||
- **AND** размер запроса второй попытки сопоставим с размером первой
|
||||
|
||||
#### Scenario: Обрыв по длине не ретраится
|
||||
|
||||
- **GIVEN** ответ модели с признаком обрыва генерации по длине
|
||||
- **WHEN** завершается распознавание
|
||||
- **THEN** задача переходит в `review` с причиной «ответ модели обрезан»
|
||||
- **AND** повторных запросов к модели не выполняется
|
||||
|
||||
### Requirement: Модель уверенности и решение auto/review
|
||||
|
||||
Система SHALL раскладывать автоматически (без review) только при выполнении
|
||||
ВСЕГО: (1) подтверждённый единичный сильный матч в базе (`metadata-match`) с
|
||||
`provider_id`; (2) структурная валидация без предупреждений (фильм — ровно один
|
||||
основной видеофайл; сериал — число серий бьётся с базой, нумерация S·E
|
||||
консистентна); (3) согласованность пред-парса и LLM по типу/названию/году. Иначе
|
||||
задача SHALL уходить в `review` с явной причиной. Самооценку LLM (`confidence`)
|
||||
система SHALL учитывать лишь как вспомогательный сигнал, НЕ как единственный гейт.
|
||||
|
||||
Список причин распознавания SHALL быть неоднородным: кроме блокирующих причин он
|
||||
MAY содержать информационные строки, которые показываются человеку и сохраняются
|
||||
вместе с распознаванием, но авто-раскладку НЕ отменяют — такова сводка покрытия
|
||||
плана (см. «Причины по нумерации серий и покрытию плана»). Поэтому решение
|
||||
auto/review система SHALL считать по **блокирующим** причинам, а НЕ по длине
|
||||
списка причин: непустой список сам по себе SHALL NOT означать `review`.
|
||||
|
||||
#### Scenario: Нет матча в базе — всегда review
|
||||
|
||||
- **GIVEN** план без подтверждённого матча в базе (база выключена или матча нет)
|
||||
- **WHEN** принимается решение auto/review
|
||||
- **THEN** задача уходит в `review`, авто-раскладка не делается
|
||||
|
||||
#### Scenario: Матч и чистая валидация — авто
|
||||
|
||||
- **GIVEN** подтверждённый единичный матч, чистая структурная валидация и
|
||||
согласованность сигналов
|
||||
- **WHEN** принимается решение
|
||||
- **THEN** допускается авто-раскладка (при отсутствии `force_review`)
|
||||
|
||||
#### Scenario: Информационная причина авто не отменяет
|
||||
|
||||
- **GIVEN** подтверждённый матч и чистая валидация, но покрытие плана неполно
|
||||
только за счёт файлов-спутников
|
||||
- **WHEN** принимается решение auto/review
|
||||
- **THEN** список причин непуст — в нём сводка покрытия
|
||||
- **AND** авто-раскладка допускается
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Полный список файлов раздачи в промпте
|
||||
|
||||
Промпт распознавания SHALL включать все файлы раздачи, а не фиксированную их
|
||||
часть. Предельное число файлов и предельный размер ответа модели SHALL задаваться
|
||||
настройками `[recognition].max_files` и `[recognition].max_tokens`; предел числа
|
||||
файлов служит предохранителем от аномальной раздачи, а не рабочим ограничением, и
|
||||
его значение по умолчанию SHALL соответствовать размеру промпта, который заведомо
|
||||
принимает модель. Если список всё же усечён пределом, распознавание SHALL называть
|
||||
усечение отдельной причиной ухода в `review`. Отказ модели, вызванный размером
|
||||
запроса, распознавание SHALL называть причиной о размере запроса, а не текстом
|
||||
провайдера.
|
||||
|
||||
#### Scenario: Раздача на сотни файлов показана модели целиком
|
||||
|
||||
- **GIVEN** раздача из 180 файлов и `max_files` = 500
|
||||
- **WHEN** собирается промпт распознавания
|
||||
- **THEN** в списке файлов промпта присутствуют все 180 файлов
|
||||
|
||||
#### Scenario: Усечение названо причиной
|
||||
|
||||
- **GIVEN** раздача, число файлов которой превышает `max_files`
|
||||
- **WHEN** завершается распознавание
|
||||
- **THEN** задача уходит в `review`, и среди причин названо усечение списка файлов
|
||||
|
||||
### Requirement: Причины по нумерации серий и покрытию плана компактны
|
||||
|
||||
Причины ухода в `review`, порождённые нумерацией серий, SHALL быть свёрнуты: на
|
||||
сезон приходится не более одной причины, перечисляющей недостающие серии.
|
||||
|
||||
Распознавание SHALL называть покрытие плана — сколько файлов раздачи попало в
|
||||
план из общего числа, — когда покрыты не все файлы. Блокировать авто-раскладку
|
||||
SHALL только непокрытый видеофайл: файл, который модель не адресовала и который
|
||||
по расширению является видео, означает потерянную серию или фильм. Непокрытые
|
||||
файлы-спутники (субтитры, изображения, тексты, служебные файлы) SHALL
|
||||
показываться в покрытии, но SHALL NOT входить в структурную валидацию и SHALL NOT
|
||||
влиять на решение auto/review: модель вправе не перечислять то, что не
|
||||
раскладывается.
|
||||
|
||||
#### Scenario: Пропуски сезона свёрнуты в одну причину
|
||||
|
||||
- **GIVEN** план сезона, где недостают серии E05, E07 и E11
|
||||
- **WHEN** формируются причины ухода в review
|
||||
- **THEN** этому сезону соответствует одна причина, перечисляющая недостающие серии
|
||||
|
||||
#### Scenario: Непокрытый видеофайл блокирует авто
|
||||
|
||||
- **GIVEN** раздача, где один видеофайл не адресован ни одним элементом плана
|
||||
- **WHEN** принимается решение auto/review
|
||||
- **THEN** задача уходит в `review` с причиной о непокрытом видеофайле
|
||||
|
||||
#### Scenario: Непокрытые файлы-спутники авто не блокируют
|
||||
|
||||
- **GIVEN** раздача, где не адресованы только `.nfo`, скриншоты и текстовый файл,
|
||||
а все видеофайлы покрыты, матч подтверждён и валидация чиста
|
||||
- **WHEN** принимается решение auto/review
|
||||
- **THEN** авто-раскладка допускается
|
||||
- **AND** покрытие плана показано человеку, но причиной ухода в review не является
|
||||
@@ -0,0 +1,45 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Раскладка показывает все файлы раздачи
|
||||
|
||||
Виджет «файл источника → раскладка» на экранах загрузки и ревью SHALL показывать
|
||||
все файлы раздачи, а не только попавшие в план распознавания. Файл, которого нет
|
||||
в плане, SHALL отображаться отдельной строкой с явной пометкой «не в плане» и без
|
||||
целевого пути.
|
||||
|
||||
Порядок строк SHALL быть определён полностью и не зависеть от порядка файлов в
|
||||
плане: сперва разложенные файлы — по сезону, затем по номеру серии, затем по пути
|
||||
источника; за ними файлы плана без целевого пути (`extra`, `sample`, `ignore`) —
|
||||
по пути источника; последними файлы вне плана — по пути источника. Файлы без
|
||||
сезона и серии SHALL NOT попадать в начало списка.
|
||||
|
||||
Если список файлов раздачи для загрузки неизвестен (запись создана до того, как
|
||||
система стала его сохранять), виджет SHALL строить строки по плану и SHALL прямо
|
||||
называть, что список файлов раздачи недоступен и полнота не гарантирована.
|
||||
Молчаливый показ неполного списка как полного SHALL NOT допускаться.
|
||||
|
||||
Номер строки, показанный в виджете, SHALL совпадать с номером, которым файл
|
||||
адресуется в ответе модели, либо не показываться вовсе: расхождение сделало бы
|
||||
разбор ошибок адресации ложным.
|
||||
|
||||
#### Scenario: Файл, пропущенный моделью, виден
|
||||
|
||||
- **GIVEN** раздача из 180 файлов, из которых модель разметила 100
|
||||
- **WHEN** открывается раскладка загрузки
|
||||
- **THEN** показаны все 180 файлов
|
||||
- **AND** 80 неразмеченных помечены как «не в плане» и не имеют целевого пути
|
||||
|
||||
#### Scenario: Порядок строк — по сезону и серии
|
||||
|
||||
- **GIVEN** план мультисезонного пака, где модель вернула файлы в произвольном порядке
|
||||
- **WHEN** отображается раскладка
|
||||
- **THEN** строки идут по возрастанию сезона, внутри сезона — по номеру серии
|
||||
- **AND** семплы, допматериалы и игнорируемые файлы стоят после разложенных серий,
|
||||
а не перед первым сезоном
|
||||
|
||||
#### Scenario: Список файлов раздачи неизвестен
|
||||
|
||||
- **GIVEN** загрузка, распознанная до появления сохранённого списка файлов
|
||||
- **WHEN** открывается её раскладка
|
||||
- **THEN** строки построены по плану
|
||||
- **AND** виджет называет, что список файлов раздачи недоступен и полнота не гарантирована
|
||||
@@ -0,0 +1,66 @@
|
||||
## 1. Признак обрыва генерации из LLM
|
||||
|
||||
- [x] 1.1 Довести `finish_reason` до вызывающего: добавить поле в `llm.Response`, заполнить его в `internal/llm/openai.go` (сейчас разбирается и выбрасывается)
|
||||
- [x] 1.2 Тест `internal/llm`: ответ с `finish_reason: "length"` доносит признак обрыва до вызывающего
|
||||
|
||||
## 2. Компактный формат ответа модели
|
||||
|
||||
- [x] 2.1 Добавить `Index` в `recognize.PlanFile` (`i` в JSON), `Src` оставить для запасного формата
|
||||
- [x] 2.2 Переписать схему в `schemaText` (`prompt.go`) под адресацию номером: `i` вместо `src`, явно оговорить, что для ролей `sample`/`ignore`/`extra` номера сезона и серии не нужны, а `notes` — не более пары предложений
|
||||
- [x] 2.3 Поправить шапку списка файлов в `writeFileList`: номер строки — это то, что нужно вернуть в `i`
|
||||
- [x] 2.4 Резолв номера в `validate.go`: при `i` в диапазоне `1..len(in.Files)` заполнять `Src` из своего списка
|
||||
- [x] 2.5 Сохранить приём старого формата: пришёл `src` без `i` — проверять совпадение с реальным файлом торрента, как сегодня
|
||||
- [x] 2.6 Элемент с обоими полями резолвить по `i`; несовпадение присланного `src` с резолвом — причина ревью, а не тихое затирание
|
||||
- [x] 2.7 Отбраковка негодных элементов вместо отказа от плана: номер вне диапазона, повторная адресация файла (побеждает первый элемент), элемент без номера и пути — элемент отбрасывается, план живёт, каждая отбраковка названа причиной; план без единого годного файла считается неразобранным
|
||||
- [x] 2.8 Убедиться, что отбраковка элементов не порождает correction-ретрая (это не ошибка разбора)
|
||||
- [x] 2.9 Тесты `internal/recognize`: резолв номера в путь; номер вне диапазона; дубль адресации; элемент без `i` и `src`; запасной формат с `src`; `i` и `src` вместе — совпадающие и расходящиеся; план, где годных элементов не осталось. Отдельно: резолв не паникует ни на одном входе (`i` = 0, отрицательное, `len+1`, дробное или строковое в JSON, пустой `files[]`, `files[]` длиннее списка раздачи)
|
||||
|
||||
## 3. Порядок списка файлов — свойство узла
|
||||
|
||||
- [x] 3.1 Упорядочивать список файлов по пути внутри `Recognize`, а не у вызывающего: сборки `Input` две — `internal/worker/review.go:126` и `cmd/jellybit/recognize.go:101`
|
||||
- [x] 3.2 Печать промпта и резолв номеров идут по одному и тому же упорядоченному срезу на всех попытках, включая correction-ретрай
|
||||
- [x] 3.3 Тест: `Recognize` с перемешанным `Input` даёт ту же нумерацию, что и с упорядоченным (тест зовёт узел, а не воркер)
|
||||
|
||||
## 4. Лимиты в конфиг
|
||||
|
||||
- [x] 4.1 Добавить `[recognition].max_files` и `[recognition].max_tokens` в `internal/config` с валидацией и дефолтами 500 и 8000
|
||||
- [x] 4.2 Пробросить их в `recognize.Config` на сборке зависимостей (воркер и CLI)
|
||||
- [x] 4.3 Описать обе настройки в `config.example.toml` (назначение, диапазон, единицы) и в `docs/database.md` (настройки с числовым значением)
|
||||
- [x] 4.4 Тест конфига: дефолты применяются, невалидные значения отвергаются
|
||||
|
||||
## 5. Цена попытки не растёт с размером раздачи
|
||||
|
||||
- [x] 5.1 Убрать повторную печать списка файлов из `correctionMessage` (`prompt.go:180`): номера названы в первом сообщении диалога
|
||||
- [x] 5.2 Обрыв ответа по длине — review с причиной «ответ модели обрезан», без повторного запроса ни тем же промптом, ни его вариантом
|
||||
- [x] 5.3 Усечение списка пределом `max_files` — отдельная причина ухода в review; знаменатель покрытия считается по полному числу файлов раздачи, а не по усечённому списку
|
||||
- [x] 5.4 Отказ модели по размеру запроса называть отдельной причиной, а не текстом провайдера
|
||||
- [x] 5.5 Тесты: correction-ретрай не содержит списка файлов и сопоставим по размеру с первой попыткой; обрыв не порождает ни одного повторного запроса (тест считает вызовы фейкового клиента); снятие лимита не умножает число попыток сверх `[llm].max_retries`
|
||||
|
||||
## 6. Свёртка причин и покрытие плана
|
||||
|
||||
- [x] 6.1 Переписать `seriesWarnings` (`validate.go:186`): одна причина на сезон с перечнем недостающих серий вместо строки на каждый разрыв
|
||||
- [x] 6.2 Завести перечень видеорасширений в одном месте рядом с ролями файлов (в проекте такого места сегодня нет) и покрыть его тестом
|
||||
- [x] 6.3 Покрытие плана: число «в плане N из M» показывается, когда покрыты не все файлы; блокирующей причиной становится только непокрытый видеофайл, файлы-спутники в решение auto/review не входят
|
||||
- [x] 6.4 Тесты: сезон с тремя дырами даёт одну причину; непокрытый видеофайл уводит в review; раздача, где не покрыты только `.nfo` и скриншоты, авто-раскладку не теряет
|
||||
|
||||
## 7. Список файлов раздачи рядом с планом
|
||||
|
||||
- [x] 7.1 Миграция `0012`: колонка со списком файлов раздачи (JSON: путь и размер) в таблице `recognition`
|
||||
- [x] 7.2 Обновить ER-схему в `docs/database.md` — иначе краснеет шаг `canon` гейта
|
||||
- [x] 7.3 Заполнять колонку при создании записи распознавания из того же `Input`, из которого построен план (снимок и `src` плана обязаны происходить из одного среза); читать при рендере страниц загрузки и ревью
|
||||
- [x] 7.4 Записи, созданные до миграции: рендер строит строки по плану и явно называет, что список файлов недоступен и полнота не гарантирована — молчаливой деградации быть не должно
|
||||
|
||||
## 8. Раскладка показывает все файлы
|
||||
|
||||
- [x] 8.1 Развернуть `buildFileRows` (`internal/httpapi/files.go`) на файлы раздачи: план подмешивается по `src`, файл без записи получает пометку «не в плане»; ровно одна строка на файл раздачи
|
||||
- [x] 8.2 Полный ключ сортировки: разложенные (сезон, серия, путь) → файлы плана без цели (`extra`/`sample`/`ignore`, по пути) → файлы вне плана (по пути); пустые `season`/`episode` не поднимают файл в начало списка
|
||||
- [x] 8.3 Отобразить пометку «не в плане» и строку о недоступном списке файлов в шаблонах раскладки в единой дизайн-системе
|
||||
- [x] 8.4 Колонка `#` в `web/templates/partials/layout_widget.html:8` печатает порядковый номер строки — привести её к номеру адресации из списка распознавания либо убрать: расхождение сделает разбор ошибок адресации ложным
|
||||
- [x] 8.5 Тесты `internal/httpapi`: файл вне плана присутствует в строках без целевого пути; порядок строк не зависит от порядка `plan.Files` (тест с перемешанным планом); запись без сохранённого списка файлов даёт строку о недоступности
|
||||
|
||||
## 9. Проверка на живом кейсе
|
||||
|
||||
- [ ] 9.1 `task gate` зелёный
|
||||
- [ ] 9.2 Прогнать распознавание раздачи «Отчаянные домохозяйки» (180 файлов) на рабочем эндпоинте: план покрывает все файлы, причины ревью читаются с одного взгляда, раскладка упорядочена по сезонам
|
||||
- [ ] 9.3 Сверить фактический размер ответа модели с `max_tokens`: подтвердить, что запаса хватает, и при необходимости поправить дефолт
|
||||
- [ ] 9.4 Проверить, что сырой ответ модели и ключ LLM не попадают в лог (`docs/conventions/logging.md`), а отброшенные элементы и файлы вне плана видны числом в причинах
|
||||
Reference in New Issue
Block a user