## 1. Ядро: общий билдер URL - [x] 1.1 `internal/worker/review.go`: добавить экспортируемую `func ProviderURL(provider, id, mediaType string) string` (канонический URL; пустой id / неизвестный провайдер → пусто) и метод `func (rd *ReviewData) MatchURL() string` (приоритет URL выбранного кандидата при совпадении provider+id с эффективными, иначе `ProviderURL` по `rd.Provider`/`rd.ProviderID`/`rd.Plan.Type`). - [x] 1.2 Перенести тесты `providerURL`/`matchURL` в `internal/worker` (из `internal/httpapi/providerurl_test.go`), поправив на новые имена/сигнатуры. ## 2. httpapi: делегирование - [x] 2.1 `internal/httpapi/review.go`/`download.go`: удалить локальные `providerURL`/`matchURL`; `view.MatchURL = rd.MatchURL()`; `sourceMatchURL` зовёт `worker.ProviderURL(src.Provider, src.ProviderID, src.Type)`. `parseProviderURL`/`parseManualSource` остаются в httpapi. ## 3. tgbot: показ матча - [x] 3.1 `internal/tgbot/render.go`: `escHref` — экранирование URL для значения атрибута `href` (поверх `esc` заменяет `"` → `"`). - [x] 3.2 `baseLine` принимает `*worker.ReviewData`, использует эффективные `rd.Provider`/`rd.ProviderID` и `rd.MatchURL()`: при непустом URL — `provider id ↗` (provider/id через `esc`, URL через `escHref`), иначе текст `provider id`; при отсутствии матча — пусто. - [x] 3.3 `reviewCard` зовёт `baseLine(rd)`, при пустом результате показывает «База: нет матча» (сохранение поведения); `renderDone` добавляет строку «База: …» только при непустом `baseLine(rd)` (без матча — опускает). ## 4. Тесты - [x] 4.1 `internal/tgbot`: карточка ревью с матчем содержит `