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,58 @@
## ADDED Requirements
### Requirement: Выбор кандидата метабазы из карточки подтверждения бота
Карточка подтверждения бота SHALL предлагать выбрать источник из сохранённых
кандидатов метабазы inline-кнопками, когда для распознавания загрузки есть хотя
бы один такой кандидат (типичный триггер ревью — несколько подходящих записей без
единичного сильного матча), а не молча оставлять загрузку с первым/лучшим или без
базы. Когда сохранённых кандидатов нет, карточка кнопку выбора базы показывать
SHALL NOT. Выбор SHALL быть двухшаговым: кнопка выбора базы разворачивает список
кандидатов (по кнопке на кандидата, выбранный помечен), а нажатие кандидата
пиннит его как источник и обновляет карточку. Отдельная кнопка SHALL возвращать
карточку без изменений.
Выбор кандидата из бота — **ручной матч** (пиннинг источника): он SHALL NOT
запускать авто-раскладку; раскладка выполняется отдельным подтверждением
(«Применить»). Бот SHALL оставаться быстрым выбором из готового короткого списка
кандидатов, тогда как точные правки (ручной ввод id/URL, «без базы») — на веб-
поверхности.
Id кандидата приходит в `callback_data` и является недоверенным входом: перед
доменным вызовом выбора система MUST валидировать его как ULID на границе
транспорта; невалидный id MUST отклоняться без изменения состояния загрузки.
Текст inline-кнопок Telegram не интерпретируется как разметка, поэтому название
кандидата в подписи кнопки инъекции разметки не создаёт.
#### Scenario: Карточка с кандидатами предлагает выбор базы
- **GIVEN** загрузка в `review`, для распознавания которой сохранены кандидаты метабазы
- **WHEN** бот рендерит карточку подтверждения
- **THEN** карточка содержит кнопку выбора базы, ведущую к списку кандидатов inline-кнопками
#### Scenario: Выбор кандидата пиннит источник без авто-раскладки
- **GIVEN** развёрнутый в карточке список кандидатов метабазы
- **WHEN** пользователь нажимает кнопку кандидата
- **THEN** этот кандидат закрепляется как источник (ручной матч), карточка
обновляется на месте, а авто-раскладка не запускается — для раскладки нужно
явное «Применить»
#### Scenario: Id кандидата из callback валидируется на границе
- **GIVEN** callback выбора кандидата, где значение id кандидата недоверенное/невалидное
- **WHEN** бот обрабатывает callback
- **THEN** id кандидата валидируется как ULID до доменного вызова, а невалидное
значение отклоняется без изменения состояния загрузки
#### Scenario: Возврат из списка не меняет состояние
- **GIVEN** развёрнутый в карточке список кандидатов
- **WHEN** пользователь нажимает кнопку возврата
- **THEN** бот возвращает исходную карточку подтверждения, не меняя выбранный источник
#### Scenario: Без кандидатов кнопки выбора базы нет
- **GIVEN** загрузка в `review`, для распознавания которой кандидаты метабазы не сохранены
- **WHEN** бот рендерит карточку подтверждения
- **THEN** карточка не содержит кнопку выбора базы
@@ -0,0 +1,24 @@
## MODIFIED Requirements
### Requirement: Разделение труда транспортов в ревью
Состояние ревью SHALL быть единым (в SQLite) для всех транспортов. Веб-UI SHALL
быть поверхностью точных правок (маппинг файлов, ручной ввод/выбор источника по
id или URL, «без базы», предпросмотр). Telegram SHALL давать быстрые действия
(одобрить, подсказать, переключить тип, **быстрый выбор источника из готового
списка кандидатов метабазы**, «Позже»/«Отклонить») и эскалацию в веб deep-link'ом
на ту же страницу; точечные правки, не помещающиеся в чат (ручной ввод id/URL,
маппинг файлов), SHALL делаться в вебе.
#### Scenario: Эскалация из Telegram в веб
- **GIVEN** загрузка в `review`, требующая точечного маппинга файлов
- **WHEN** пользователь в Telegram выбирает «В вебе»
- **THEN** бот даёт deep-link на страницу ревью той же загрузки
#### Scenario: Быстрый выбор кандидата в Telegram, точный ввод — в вебе
- **GIVEN** загрузка в `review` с сохранёнными кандидатами метабазы
- **WHEN** пользователь выбирает кандидата inline-кнопкой в Telegram
- **THEN** кандидат закрепляется как источник (тот же единый выбор источника, что
и в вебе), а ручной ввод id/URL и «без базы» остаются точными правками веба