## 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:`. - [x] 2.2 Новая `candidatesKeyboard(rd)`: по кнопке на кандидата (дедуп по `provider:provider_id`, потолок), текст `provider · Title (Year)`, у `Chosen` — префикс «✓ », callback `pick::`; нижняя кнопка «← Назад» с callback `srcback:`. ## 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:`; без кандидатов — не содержит. - [x] 4.2 `sources` разворачивает клавиатуру с кнопками `pick::` и «← Назад»; выбранный кандидат помечен «✓ ». - [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` — зелёные.