Веб-UI: режиссёр в блоке «Распознано как» на странице загрузки
На /download/{id} поле «Режиссёр» было захардкожено прочерком, хотя экран
ревью режиссёра уже выводит: слоистое разрешение полей ярлыка было заперто в
неэкспортируемом worker.effectiveDisplayName. Из-за этого билдеры вью видели
только слой распознавания+матч (rd.Plan.Director) без слоя контекста — то же
на экране ревью.
Вынес разрешение в экспортируемую naming.EffectiveFields(parsedContext, plan)
LabelFields с методом Label(): выбор слоя по сырым значениям (как прежде),
выбранные скаляры возвращаются очищенными (sanitize идемпотентен, display_name
побайтно тот же). effectiveDisplayName стал тонкой обёрткой; страница загрузки
и экран ревью берут режиссёра из той же функции — согласованно с заголовком.
OpenSpec: web-ui (ADDED «Режиссёр в блоке распознавания страницы загрузки»),
review (MODIFIED «Инфо и предпросмотр выбранного источника» — слоистое
разрешение с фолбэком на контекст). Change заархивирован. Беклог: закрыта
rezhisser-v-kartochke-zagruzki.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
## Context
|
||||
|
||||
`display_name` и поля блоков «Распознано как» / «инфо источника» — косметика
|
||||
отображения, не влияющая на пути и раскладку. Слоистое разрешение полей ярлыка
|
||||
(первый непустой слой `override` → `recognition`+матч → **контекст**) уже
|
||||
реализовано, но заперто в неэкспортируемом `worker.effectiveDisplayName`
|
||||
(`internal/worker/review.go:1145`), который:
|
||||
|
||||
1. читает `naming.Fields` из `download.parsed_context` (нижний слой «контекст»);
|
||||
2. для каждого поля берёт первый непустой слой: `plan.Title→ctxf.Title`,
|
||||
`plan.Director→ctxf.Director`, `plan.Year→ctxf.Year`, сводка сезонов
|
||||
`recognize.SeasonSummary(plan)→ctxf.SeasonLabel()`;
|
||||
3. рендерит строку `naming.Label(...)`.
|
||||
|
||||
Билдеры вью строят режиссёра **в обход** этой логики — прямо `rd.Plan.Director`
|
||||
(`internal/httpapi/review.go:122`), а на странице загрузки
|
||||
(`internal/httpapi/download.go`) поле вообще захардкожено прочерком в шаблоне.
|
||||
Итог — слой контекста теряется, и в заголовке страницы режиссёр есть, а в поле —
|
||||
прочерк.
|
||||
|
||||
Constraints (инварианты): вывод имени НИКОГДА не валит приём (деградация к
|
||||
пустому); `parsed_context` недоверен (битый JSON → пустой слой, best-effort);
|
||||
`director` — недоверенное косметическое поле, чистится на рендере (`naming.Label`
|
||||
уже санитизирует); без сети и без изменения схемы БД.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Один источник истины для слоистого разрешения полей ярлыка — экспортируемая
|
||||
функция в `naming`, переиспользуемая в `effectiveDisplayName` и в обоих
|
||||
билдерах вью.
|
||||
- Режиссёр на `/download/{id}` согласован с режиссёром в заголовке страницы.
|
||||
- Поведение `display_name` не меняется (побайтно тот же результат) — существующие
|
||||
тесты `worker`/`naming` зелёные.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Не меняем слои Title/Year/Season в блоке «Распознано как»: они остаются из
|
||||
плана (что распознано), как сейчас — расширение слоёв контекста на эти поля вне
|
||||
scope (это изменило бы отображение блока). Правится только режиссёр — поле,
|
||||
которое даже в норме приходит не из LLM, а из матча/override/контекста.
|
||||
- Не трогаем схему БД, метабазы, распознавание, раскладку.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1. Сигнатура: `naming.EffectiveFields(parsedContext string, plan recognize.Plan)`
|
||||
|
||||
Функция принимает **строку `parsed_context`**, а не `store.Download`. Так `naming`
|
||||
не тянет зависимость на `store` (пакет `naming` — низкоуровневый вывод имени; уже
|
||||
знает про `parsed_context` концептуально — его схема это `naming.Fields`). Импорт
|
||||
`naming`→`recognize` добавляется (нужен `recognize.Plan` и
|
||||
`recognize.SeasonSummary`); цикла нет — `recognize` не импортирует `naming`
|
||||
(проверено).
|
||||
|
||||
Возвращает не строку, а тип полей:
|
||||
|
||||
```go
|
||||
// LabelFields — эффективные скалярные поля ярлыка после слоистого разрешения.
|
||||
// Значения уже очищены (sanitize): управляющие символы вырезаны, пробелы
|
||||
// схлопнуты — те же, что попадут внутрь Label. Season — готовая строка-сводка
|
||||
// сезонов (для фильма пусто).
|
||||
type LabelFields struct {
|
||||
Title string
|
||||
Director string
|
||||
Year int
|
||||
Season string
|
||||
}
|
||||
|
||||
func (f LabelFields) Label() string { return Label(f.Title, f.Director, f.Year, f.Season) }
|
||||
|
||||
func EffectiveFields(parsedContext string, plan recognize.Plan) LabelFields
|
||||
```
|
||||
|
||||
`Season` — строка, а не `*int`: сводка плана многосезонна («Сезоны 1–3»,
|
||||
«Спецвыпуски»), в `*int` не выражается. Метод `Label()` даёт единый рендер, чтобы
|
||||
`effectiveDisplayName` остался тонкой обёрткой.
|
||||
|
||||
**Санитайзинг на возврате.** Выбор слоя (первый непустой) идёт по **сырым**
|
||||
значениям (`plan.Director != ""` и т.д. — как в исходном `effectiveDisplayName`),
|
||||
но выбранное значение возвращается уже прогнанным через `naming.sanitize` (в
|
||||
пакете `naming` он доступен). Причина — билдеры вью печатают `.Director` напрямую
|
||||
(`{{.Director}}`), в обход `Label`; без очистки на возврате (а) недоверенный
|
||||
`parsed_context.director` дошёл бы до страницы без санитайзинга, (б) режиссёр из
|
||||
одних управляющих символов дал бы `{{if .Director}}` истинным в поле, тогда как
|
||||
`Label` его выбросил бы из заголовка — то самое рассогласование «в поле есть, в
|
||||
шапке нет». `sanitize` идемпотентен, поэтому повторная очистка внутри `Label`
|
||||
ничего не меняет: `Label()` даёт **побайтно** прежний `display_name` (тесты
|
||||
`worker`/`naming` зелёные).
|
||||
|
||||
Альтернатива (вариант A из беклога — присвоить `view.Director = rd.Plan.Director`
|
||||
на странице загрузки) отклонена: оставляет ровно тот баг (нет слоя контекста →
|
||||
нестыковка с заголовком) и не чинит `review.go`.
|
||||
|
||||
### D2. `worker.effectiveDisplayName` — тонкая обёртка
|
||||
|
||||
```go
|
||||
func effectiveDisplayName(d store.Download, plan recognize.Plan) string {
|
||||
return naming.EffectiveFields(d.ParsedContext, plan).Label()
|
||||
}
|
||||
```
|
||||
|
||||
Логика перенесена дословно, поэтому результат идентичен. Обёртку и её тесты
|
||||
(`displayname_test.go`) сохраняем — они продолжают проверять инвариант «имя не
|
||||
изменилось».
|
||||
|
||||
### D3. Билдеры вью читают `.Director`
|
||||
|
||||
- `download.go`: новое поле `downloadDetailView.Director`; в `buildDownloadView`
|
||||
(внутри `if rd.Recognition != nil`) —
|
||||
`view.Director = naming.EffectiveFields(d.ParsedContext, rd.Plan).Director`.
|
||||
- `review.go`: `view.Director = naming.EffectiveFields(rd.Download.ParsedContext, rd.Plan).Director`
|
||||
вместо `rd.Plan.Director`.
|
||||
|
||||
Только `Director` берётся из слоистого разрешения; Title/Year/Season в блоке
|
||||
остаются как есть (см. Non-Goals).
|
||||
|
||||
### D4. Шаблон
|
||||
|
||||
`download_main.html:40` — прочерк заменяется на условный вывод по конвенции
|
||||
соседних полей: `{{if .Director}}{{.Director}}{{else}}<span class="faint">—</span>{{end}}`.
|
||||
Деградация без JS сохраняется (страница server-rendered, поле статично).
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Импорт `naming`→`recognize` расширяет зависимости пакета `naming`. Приемлемо:
|
||||
цикла нет, `recognize` — доменный тип плана, а `naming` уже оперирует его
|
||||
сводкой сезонов косвенно (через worker). Альтернатива — дублировать логику
|
||||
разрешения в трёх местах — хуже (дрейф).
|
||||
- Незначительный: `EffectiveFields` парсит `parsed_context` на каждый рендер
|
||||
страницы. Это дешёвый `json.Unmarshal` короткой строки, страница и так
|
||||
server-rendered без БД на рендере блока — некритично.
|
||||
Reference in New Issue
Block a user