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,59 @@
## Why
Распознавание уже копит кандидатов метабазы (`recognize.Result.Candidates`
`metadata_candidate`), и веб даёт полный «единый список источников совпадения»
для точного выбора. В Telegram же карточка подтверждения показывает лишь
эффективный матч (строка «База: …» / «нет матча»), но **не даёт выбрать** между
несколькими найденными кандидатами: когда единичного сильного матча нет (частый
триггер ревью — «несколько кандидатов»), из бота нельзя быстро закрепить нужный,
приходится открывать веб. Ядро для этого уже готово: команда
`worker.ChooseCandidate` пиннит кандидата как источник (ручной матч), не
запуская авто-раскладку.
## What Changes
- **Карточка подтверждения (review)** в боте, когда для распознавания есть
сохранённые кандидаты метабазы, показывает кнопку выбора базы. По ней карточка
двухшагово (как подтверждение удаления) разворачивает **список кандидатов
inline-кнопками** — быстрый выбор из готового короткого списка.
- **Выбор кандидата** кнопкой вызывает `ChooseCandidate` (пиннинг источника,
ручной матч) и обновляет карточку на месте. Активный (уже выбранный) кандидат
помечается в списке. Кнопка «← Назад» возвращает карточку без изменений.
- **Веб остаётся точкой точных правок** (ввод id/URL вручную, «без базы»,
предпросмотр путей): бот — только быстрый выбор из уже найденных кандидатов.
- **Безопасность границы:** id кандидата приходит в `callback_data` — недоверенный
вход; перед доменным вызовом он валидируется как ULID (`ident.Parse`), как это
уже делает веб-транспорт. Выбор кандидата — ручной матч, авто-раскладку не
запускает (нужно явное «Применить»), инвариант авто-раскладки не двигается.
## Capabilities
### New Capabilities
Нет.
### Modified Capabilities
- `notifications`: добавляется требование к **поверхности** бота — предлагать
выбор среди сохранённых кандидатов метабазы в карточке подтверждения
(inline-кнопки, двухшаговое разворачивание), с валидацией id кандидата из
`callback_data` на границе и без авто-раскладки. Доменная механика выбора
(`ChooseCandidate`, пиннинг источника) уже покрыта capability `review` — здесь
не дублируется.
- `review`: MODIFIED требование «Разделение труда транспортов в ревью» — быстрый
выбор источника из готового списка кандидатов теперь и Telegram-действие; точный
ручной ввод id/URL и «без базы» остаются за вебом. Снимает межспековое
расхождение (прежде выбор источника целиком относился к веб-точным-правкам).
## Impact
- **Спеки:** дельта `notifications` — ADDED «Выбор кандидата метабазы из карточки
подтверждения бота»; дельта `review` — MODIFIED «Разделение труда транспортов в
ревью» (быстрый выбор кандидата — Telegram-действие).
- **Код:** `internal/tgbot/bot.go` (интерфейс `Reviewer` + `ChooseCandidate`;
callback-и списка/выбора/назад), `internal/tgbot/render.go` (кнопка выбора базы
в `reviewKeyboard`, клавиатура списка кандидатов).
- **Тесты:** `internal/tgbot` — карточка с кандидатами содержит кнопку выбора;
разворачивание даёт список; выбор зовёт `ChooseCandidate` с валидным id;
невалидный id из callback отклоняется.
- **Миграции БД:** нет (кандидаты уже в БД — таблица `metadata_candidate`).