tgbot: выбор кандидата метабазы inline-кнопками в карточке ревью

Когда для распознавания сохранены кандидаты метабазы, карточка подтверждения
бота показывает кнопку «🗂 База (N)». По ней двухшагово (как delete/dismiss)
разворачивается список кандидатов inline-кнопками; выбор пиннит источник через
worker.ChooseCandidate (ручной матч, без авто-раскладки) и обновляет карточку.
Веб остаётся точкой точных правок (ручной ввод id/URL, «без базы»).

Безопасность границы: id кандидата из callback_data валидируется как ULID
(ident.Parse) до доменного вызова, как в вебе. Текст inline-кнопок Telegram не
парсится как HTML — название кандидата в подписи не экранируется.

SDD: change telegram-vybor-nahodok — дельта notifications (ADDED «Выбор
кандидата метабазы из карточки подтверждения бота») + review (MODIFIED
«Разделение труда транспортов»: быстрый выбор кандидата — Telegram-действие).
Влито в specs, change заархивирован. Миграций БД нет (кандидаты уже в БД).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-18 16:02:47 +03:00
co-authored by Claude Opus 4.8
parent 10a6348d39
commit f75d6b1f91
12 changed files with 541 additions and 12 deletions
@@ -0,0 +1,39 @@
## 1. Интерфейс транспорта
- [x] 1.1 `internal/tgbot/bot.go`: добавить в интерфейс `Reviewer` метод
`ChooseCandidate(ctx context.Context, id, candidateID string) error`.
- [x] 1.2 `internal/tgbot/bot_test.go`: дополнить `fakeReviewer` методом
`ChooseCandidate` (запоминает выбор для проверки).
## 2. Клавиатуры (render.go)
- [x] 2.1 `reviewKeyboard`: когда `len(rd.Candidates) > 0`, добавить кнопку
«🗂 База (N)» с callback `sources:<id>`.
- [x] 2.2 Новая `candidatesKeyboard(rd)`: по кнопке на кандидата (дедуп по
`provider:provider_id`, потолок), текст `provider · Title (Year)`, у `Chosen`
префикс «✓ », callback `pick:<id>:<candidateID>`; нижняя кнопка «← Назад» с
callback `srcback:<id>`.
## 3. Обработка callback (bot.go)
- [x] 3.1 `handleCallback`: ветки `sources` (answer + `editMarkup`
списком кандидатов, перечитав `ReviewData`; домен не трогаем) и `srcback`
(answer «Отменено» + `refreshCard`).
- [x] 3.2 Ветка `pick`: валидировать `val` (id кандидата) через `ident.Parse`
(невалидный → answer «Кнопка устарела…», состояние не меняем), затем
`reviewer.ChooseCandidate(ctx, id, candidateID)`; успех → answer «Источник
выбран» + `refreshCard`; ошибка — общая ветка ошибки.
## 4. Тесты
- [x] 4.1 `internal/tgbot`: карточка ревью с кандидатами содержит кнопку
`sources:<id>`; без кандидатов — не содержит.
- [x] 4.2 `sources` разворачивает клавиатуру с кнопками `pick:<id>:<candID>` и
«← Назад»; выбранный кандидат помечен «✓ ».
- [x] 4.3 `pick` с валидным id зовёт `ChooseCandidate` с этим id и обновляет
карточку; `pick` с невалидным id — не зовёт домен.
## 5. Спека и проверки
- [x] 5.1 `openspec validate --strict telegram-vybor-nahodok`.
- [x] 5.2 `task test` и `task lint` — зелёные.