---
name: delor-sport-outcome-notify
description: "Делор спорт — уведомления о результате пика (win/loss) + усвоенном уроке (postmortem/reinforce), зеркало CryptoPilot outcome-report (2026-07-13). ОБНОВЛЕНО 2026-07-14 (commit b94c00e): раньше void/unsettled молчали — теперь unsettled (счёт есть, рынок не грейдится) ТОЖЕ шлёт честную отбивку «🤷 НЕ СВЕРЕНО» вместо тупика; отбивка несёт кликабельную ссылку Fonbet (колонка match_url) как у рекомендации. ОБНОВЛЕНО 2026-07-16 (commit 0ce11e1): last-resort восстановление счёта (score_recovery.py) для сет-спортов, зависших pending навсегда за _MIN_WINNER_UNITS-гейтом на авторитетно завершённом матче — деньги-значимый fail-closed веб-поиск+LLM с кросс-валидацией ≥2 источников. ОБНОВЛЕНО 2026-07-16 (commit e346a16, v0.30.1): первый реальный (не синтетический) прогон вскрыл немые return-None ветки в score_recovery — добавлено логирование; расширены поисковые запросы (эмпирически не решило конкретный кейс — ограничение индекса research-gateway, не гейта). ОБНОВЛЕНО 2026-07-17 (commit b857076, v0.30.3): владелец «все матчи должны закрываться, по всем видам спорта» — вскрыт БОЛЬШИЙ невидимый канал `void` (30д: теннис 14 void vs 12 unsettled, void не уведомлял вообще); score_recovery расширен на эвристическую ветку (раньше требовал m.status=='finished', теперь достаточно check_still_live()==None); терпение 900с/3 попытки → 7200с/6 попыток; void теперь тоже честно уведомляет. ОБНОВЛЕНО 2026-07-17 (commit 3e67dff, v0.30.4): грейдинг тоталов/фор по геймам (теннис)/очкам (наст.теннис) через per-партийный subscores (kindId 1005XX) + merge-on-write (json_patch) вместо перезаписи — закрывает ЕЩЁ один класс «не сверено», см. [[delor_sport_set_unit_grading]]. ОБНОВЛЕНО 2026-07-17 (commit 206b92e, v0.30.7): владелец «раз и навсегда исправить на всех матчах и ставках» — score_recovery был жёстко заперт на 5 сет-видов И на пики с УЖЕ существующим счётом; пики без счёта ВООБЩЕ (любой вид спорта, включая футбол) никогда не попадали в цикл сеттла — теперь попадают (never_scored_after_s=4ч) и восстанавливаются тем же механизмом, см. [[delor_sport_set_unit_grading]]. ОБНОВЛЕНО 2026-07-17 (commit a3b00ad, v0.30.8): сразу же после universal-recovery прилетели ЕЩЁ 3 новых 'не сверено' — вскрыты 3 независимых корня: (1) Fonbet даёт альт factor-id на голевой тотал (не только 930/931) — добавлен лейбл-фолбэк, плюс baseball/rugby/cricket добавлены в _TOTAL_SPORTS (были вообще пропущены); (2) для сет-видов веб-recovery структурно бессилен для непубличных лиг (Setka/TT Cup нет в интернете) — добавлена _recover_set_score_from_subscores, считает истинный сет-счёт из УЖЕ ЗАХВАЧЕННЫХ собственных subscores ПЕРЕД веб-поиском; (3) mix-fights (MMA/бокс) — живой запрос подтвердил, что счёт для единоборств ВСЕГДА '0:0' (структурное свойство Fonbet, не баг) — был скрытый риск систематически неверного грейда moneyline-пиков как loss; добавлен _NO_SCORE_SPORTS guard. ОБНОВЛЕНО 2026-07-18 (commit 7c527a9, v0.30.12): сразу после v0.30.11 groq (3-й уровень) сам поймал 429 -- три контура (основной анализ+lessons+score_recovery) навалились на один слот. Добавлен 4-й уровень: notion-web2api (:1722, Enterprise-сиденье, мимо OmniRoute целиком, БЕЗ авторизации) -- модель deepseek-v4-pro (сознательно НЕ Claude, см. cebers_pulse_llm_rules). Переиспользован существующий адаптер ResearchGatewayLLM. Живьём: полная 4-уровневая цепочка отвечает за 31.5с при деградированном dashscope. ОБНОВЛЕНО 2026-07-18 (commit 60f87c3, v0.30.11): очередная порция unsettled с пустым 'LLM failed for X vs Y:' -- диагностика вскрыла TimeoutError от захлебнувшегося DashScope-пула OmniRoute (5 подключений × max_concurrent=1 = потолок 5 одновременных запросов, локальная очередь роняла запросы после 120с). Владелец подметил: у lesson_chain/score_recovery_llm primary И fallback ОБА на DashScope -- SPOF, 'фолбэк' бил в тот же забитый пул. Тот же класс бага, что D-sport-third-fallback уже чинил для основного анализа 2026-07-15, но пропустил здесь. Фикс: семафор sport_analyzer.py 6→4 (документированный план ревёрта) + 3-й уровень фолбэка на groq для lessons/score_recovery. Живьём подтверждено: цепочка проваливается через оба зависших dashscope-таймаута и отвечает через groq. Round-market recovery для единоборств — открытый пункт, ждёт живого боя для разведки формата данных. ОБНОВЛЕНО 2026-07-18 (commit a1f8447, v0.30.9): ночная порция unsettled вскрыла ещё один пропущенный рынок «N забьёт» (гол хотя бы 1 командой) + смежный скрытый баг — Fonbet непоследователен в капитализации 'Да'/'нет' ('Обе забьют'/'Тотал чет' сравнивались строго с маленькой 'д', реальный лейбл пришёл с заглавной); сверка стала регистронезависимой везде. ОБНОВЛЕНО 2026-07-18 (commit 3112d06, v0.30.10): владелец попросил довести MMA до конца — живая разведка (~35 мин опроса bulk-фида Fonbet раз в 10-30с, ~10 запланированных боёв) дала ОПРЕДЕЛЁННЫЙ отрицательный результат: ни один mix-fights матч ни разу не замечен в статусе 'live' — Fonbet, похоже, СТРУКТУРНО не ведёт live-линию по отдельным боям вообще. Построен отдельный путь без Fonbet-данных: score_recovery.recover_mma_outcome (веб-поиск победителя + отдельно раунда, fail-closed ≥min_sources доменов, decision-бой никогда не грейдится по тоталу раундов) + results_ledger._settle_mma_outcome/_try_mma_recovery (минует settle_outcome целиком). Без LLM — mix-fights закрывается unsettled немедленно (не ждёт вечно)."
metadata:
  node_type: memory
  type: project
  originSessionId: 6620ea3d-79ed-46e4-b5ea-65828f0cc0b8
---

Делор, commit 56965c8 (2026-07-13): владелец потребовал, чтобы спорт-советник уведомлял о результате пика и о том, что взял в обучение — «как делает cryptopilot» (`signals/outcome_loop.py::_report`: результат + LLM-разбор «сработало»/«почему не угадал+как правильно» + явное «🧠 закрепляю правило/усвоенный урок»).

**У спорта учебный контур уже существовал молча** ([[delor-sport-dashscope-lessons]]): `settle_pending` вызывал `postmortem_pick`/`reinforce_pick` на каждый win/loss, но возврат отбрасывался — пользователь никогда это не видел.

**Архитектурное ограничение, найденное при разведке:** `settle_pending` физически выполняется в ОТДЕЛЬНОМ процессе `delor-sport-analyst`, у которого по докстрингу нет доступа к Telegram Bot («Бот шлёт уведомления… аналитик — нет», см. [[delor-sport-dashscope-lessons]] про #5 разделение процессов). Владелец подтвердил: не давать аналитику свой Bot — передать факт «есть что отправить» через БД-очередь, новый loop в главном бот-процессе.

**Сделано:** `sport_pick_results.outcome_note`/`notified_ts` (новые колонки); `settle_pending` теперь форматирует текст урока (зеркало cryptopilot-блоков, с тем же фолбэком на LLM-сбой: «Учту этот результат в обучении.» / «Закрепляю этот результат.») и пишет в `outcome_note`. Новые методы `AdvisorDB`: `set_pick_outcome_note`, `pick_recipients(outcome_id)` (bridge через `notify_sent` — `outcome_id` уже включает `match_id`, cache_key не нужен благодаря `sport_analysis_once=True`), `picks_pending_outcome_notify`, `mark_outcome_notified`. Новый `sport_outcome_notify_loop` (главный бот-процесс, рядом с `sport_notify_push_loop`) читает очередь и шлёт `render_outcome_message`. Скоуп — только win/loss (там есть содержательный урок), void/unsettled молча.

**Проверено:** 1086 тестов (+11); прод рестарт обоих сервисов чисто; изолированный живой смоук (синтетический пик + реальный DashScope-вызов `reinforce_pick`) подтвердил всю цепочку settle→note→очередь→получатель→рендер; тестовые строки удалены из прод-БД.

**Не проверено (вопрос времени, не кода):** реальная доставка настоящего win/loss в Telegram — сегодня в проде почти нет активных пиков после смены на одиночную модель ([[delor-sport-dashscope-lessons]]), появится по мере накопления. → **ЗАКРЫТО 2026-07-14:** живая доставка подтверждена end-to-end через tg-web-e2e (синтетический win-пик + получатель=тестовый tg-id → живой `sport_outcome_notify_loop` доставил, лог `tick sent=1`, в чате корректный формат ✅ ПРИБЫЛЬ + команды-ссылка + счёт + урок; тестовые строки убраны). Приём: sport UI only-супер-админ, но САМА отбивка шлётся любому в `notify_sent` без advisor-гейта — поэтому получателем можно ставить тестовый аккаунт и читать. Грабли: `\n` в ручном SQL-INSERT не интерпретируется sqlite (боевой путь через `"\n".join` ок).

Связано: [[delor-sport-dashscope-lessons]], [[delor-sport-amateur-leagues-h2h]]. Детали, отвергнутые варианты — Obsidian Projects/Делор Changelog+Decisions `D-outcome-notify-cross-process`.

**2026-07-15, commit 21d971b — гейт замороженного сет-снимка.** Аудит вскрыл: `_plausible_final_score` отбраковывал только РАВНЫЙ счёт, теннис `1:0` после 1-го сета сеттлился «финалом» (3/3 внешне сверенных реально были 2:0; 6/13 win/loss сет-видов за 30д — снимки). Фикс: `_MIN_WINNER_UNITS` — финал сет-вида требует max(s1,s2)>=2 (Bo3-виды; н.теннис покрывает и Setka Bo3, и Лига Про Bo5), волейбол >=3; esports ИСКЛЮЧЁН (Bo1 легитимен + FIFA-счета 9:6). Снимок → pending → честный void 48ч. Единая точка защищает прод+shadow сеттлы и грейдеры «Кол-во/Фора по сетам». Инвариант-тест `set(_MIN_WINNER_UNITS)==_SET_SPORTS`. Исторические строки не трогали. Остаточный риск принят: Bo5 замороженный на 2:0 неотличим от финала Bo3. Детали — Decisions `D-sport-set-snapshot-final-gate`.

**2026-07-14, commit b94c00e — закрыт молчаливый тупик `unsettled` + ссылка Fonbet в отбивке.** Аудит по запросу владельца «все ли матчи прислал отбивкой»: из 61 пуша за ~2 дня 14 win/loss доставлены корректно, но 10 (~16%) заперты в `unsettled` навсегда. Корень: `settle_outcome`→None для негрейдимых из счёта рынков (теннисный «Кол-во сетов» и т.п.) → `result='unsettled'`, а `pending_picks_to_settle` берёт только `result='pending'` — строка больше не читается, матч каскадно удаляется. Тупик. **Фикс:** `settle_pending` пишет outcome_note для unsettled (`_UNSETTLED_NOTE` «🤷 Не смог сверить… проверь по ссылке; если матч ещё идёт — счёт снялся рано»); `picks_pending_outcome_notify` включает `'unsettled'`; `render_outcome_message`→«🤷 НЕ СВЕРЕНО». Без LLM-урока. **Плюс** (прямой запрос владельца): новая колонка `sport_pick_results.match_url`, снимается в `record_picks` (переживает удаление матча), оборачивает команды в `<a href>` в отбивке — как в рекомендации; мотив — свериться руками + заметить преждевременный результат. Оговорка: 10 старых unsettled НЕ добиваются (note NULL, матчи удалены). Живой TG-прогон невозможен: спорт only-супер-админ (`acl.is_advisor_user`), тестовый tg-web-e2e аккаунт молча отфутболивается — верификация по БД-журналу + живой рендер. Детали — Decisions `D-sport-unsettled-notify`.

**2026-07-14, commit f20d38d — грейдер расширен под «Кол-во сетов» (та часть unsettled ушла в реальные win/loss).** `settle_outcome` сверяет factor 917/918 + альт-id 10409/10410 как Over/Under по СУММЕ сетов (s1+s2), только для set-based видов `_SET_SPORTS`={tennis,badminton,table-tennis,volleyball,padel-tennis} (score = сеты, та же семантика, что 921/923-победитель). Маппинг сверен с живыми прод-данными: 917/10409=Б, 918/10410=М, param×100. Попутно фикс латентного бага: `_NO_DRAW_SPORTS` имел `table_tennis` (подчёркивание) при реальном `table-tennis` (дефис) — тай-гейт для н.тенниса молча не работал (застывший 0:0 мог засчитаться результатом); исправлено. Остальные негрейдимые рынки (угловые/карточки/точный тотал по геймам) остаются unsettled с честной отбивкой — расширять дальше без запроса не стал. 1131 passed (+15). Детали — Decisions `D-sport-set-count-grader`. ВАЖНО на будущее: другие имена видов в ленте Fonbet тоже через дефис (`am-football`, `australian-football`, `basketball-3x3`, `padel-tennis`) — при новых sport-гейтах сверять точное написание с БД.

**2026-07-14, commit 233dee9 — карты киберспорта + форы по сетам/картам; и ГРАНИЦА «сверять всё».** Запрос «сверять всё» → разведка показала: «всё» невозможно. Финальный score=«s1:s2» несёт только агрегат; статистика (углы `Score.Corners`, карты `Score.YellowCards`, удары, офсайды, счёт по таймам) есть в `liveEventInfos.subscores` ЖИВОГО фида, но исчезает при финише → нужен ЗАХВАТ при завершении (инфраструктура). Игроцкие пропсы/тайминг — данных нет вообще. Тройные комбо — хрупки, неверный грейд ОТРАВЛЯЕТ обучение (баг-класс тай-гейта). Правило: сверяем только детерминированно+безопасно выводимое, остальное — честная отбивка «не сверено», НЕ угадываем. Добавлено: **«Кол-во карт»/«Фора по картам» = КАРТЫ (maps) киберспорта** (не футбольные карточки!), score s1:s2=карты, грейдится как сеты — factor 3274/3275(+9886/9887), класс `_SET_OR_MAP_SPORTS`=сеты∪esports; + «Фора по сетам». 1146 passed. Следующее расширение (если попросят) — тайм-маркеты через захват subscores. Детали — Decisions `D-sport-grade-coverage-limits`.

**2026-07-14, commit df58366 — тайм-рынки через захват subscores (инфраструктура).** Финальный score тайм-рынки не выводит, но `liveEventInfos.subscores` несёт счёт по таймам (kindId 100201=1-й тайм, 100202=2-й) — живёт только в live-фиде, ЗАХВАТЫВАЕМ на каждом live-тике. Плюмбинг: `NormalisedMatch.subscores` (парсер `_extract_subscores_by_id`) → `sport_pipeline` → новая колонка `sport_matches.subscores_json` (upsert с `COALESCE` — последний live-снимок липнет, переживает notActive) → settle-SELECT → `settle_outcome(subscores=)`. Сверка `_settle_half_market` (голевые виды): 1-й тайм из h1, 2-й = финал−1-й (точно, т.к. h1 стабилен со 2-го тайма); снимок бракуется, если несогласован с финалом. Сверяем ОДНОЗНАЧНЫЕ: `N-й тайм будет гол`, `гол в обоих таймах`, `оба тайма больше/меньше X`. НЕ сверяем (honest-unsettled): `выиграет оба/хотя бы один тайм` (нет команды в лейбле), `сравнение таймов`, `одинаковый счёт` (конвенция), минутные интервалы (subscores = таймы, не минуты). Живьём подтверждён захват в проде (реальные матчи несут subscores_json = счёту). **На будущее:** та же инфра несёт углы/карты/удары/офсайды (`Score.Corners`/`Score.YellowCards`) — грейдинг футбольной статы добавится тем же паттерном. 1166 passed (+20). Детали — Decisions `D-sport-half-markets-subscores`.

**2026-07-16, commit 0ce11e1 — last-resort восстановление счёта (D-sport-score-recovery), закрывает «вечный pending» на сет-спортах.** Владелец заметил не пришедшее уведомление по тенниса (Серундоло—Кецманович) — расследование вскрыло дыру, которую `_MIN_WINNER_UNITS`-гейт (21d971b, см. выше) НЕ закрывал: гейт безусловно отклоняет неправдоподобный счёт даже когда `m.status='finished'` уже авторитетно (Fonbet `notActive`), а `fonbet_parser` СТРУКТУРНО пишет `score=None` для любого нефлайв-статуса — свежего счёта из Fonbet после этого не будет НИКОГДА. Раньше пик тихо ехал в 48ч `void` (не уведомляет, в отличие от `unsettled`) — то есть уведомления не было бы вообще. **Фикс:** новый модуль `score_recovery.py` — деньги-значимый fail-closed веб-поиск+LLM-экстракция счёта, требует ≥2 независимых доменов на ОДНОМ И ТОМ ЖЕ счёте (галлюцинированный индекс сниппета или сам неправдоподобный счёт обесценивает весь ответ). Триггерится только для сет-спортов на авторитетно-завершённом матче, подтверждённом `check_still_live` как explicit `None`. По исчерпании попыток (грейс 10мин/ретрай 15мин/макс 3) — честный `unsettled` с `_UNRECOVERABLE_NOTE` вместо тишины. Владелец выбрал 2 доп. решения при ревью плана: (1) `cleanup_sport_matches` больше не удаляет матч с pending-пиком (найден тайминг-риск — TTL от старта матча мог схлопнуть окно на восстановление для долгих матчей до ~1ч); (2) восстановленный счёт пишется обратно в `sport_matches.score` (идемпотентность + бесплатно чинит ту же болячку у shadow-ledger). Живая синтетическая проверка (реальный поиск+LLM на реальном матче) показала fail-closed работает НА ПРАКТИКЕ: 8 найденных источников, ни один явно не называл финал (прогнозы/лайв-трекеры/1-й сет) — LLM честно вернула null по всем, не угадала. Флаг включён в проде сразу. 362 sport-теста passed (+15). Детали — Decisions `D-sport-score-recovery`.

**2026-07-16, commit e346a16 (v0.30.1) — первый реальный прогон score_recovery вскрыл слепую зону в логах, не в логике.** Пик #205 (теннис, Шериф—Кеведо, WTA Яссы) прошёл все 3 попытки восстановления и честно закрылся `unsettled` — но `recover_final_score`/`_try_score_recovery` не оставили НИ ОДНОЙ строки в логе на большинстве `return None` веток (невалидный JSON от LLM, 0 корроборирующих доменов, сама сдача после `max_attempts`) — потребовались ad-hoc SSH-скрипты для диагностики вместо чтения лога. Добавлен `logger.info` на каждую немую ветку. Также расширены запросы поиска (+ угол «кто выиграл»/«match report», `max_results` 8→14) — **эмпирически проверено, что это НЕ помогло для этого конкретного нишевого матча**: research-gateway `/v1/search` просто не индексирует источники с прозой-счётом (tennismajors.com, sofascore.com — оба подтвердили реальный счёт 6-3,7-6 при ручной проверке через WebFetch) под такие запросы, только букмекерские/лайв-трекер сниппеты, которые прогноз/промежуточный счёт, не финал. Задокументировано как ограничение индекса поискового бэкенда, не баг гейта — `min_sources=2` НЕ ослаблен, честный fail-closed остаётся правильным поведением. Пик #205 закрыт вручную тем же боевым путём, что и первый случай (не прямым SQL): `settle_outcome`→`win`, `set_recovered_match_score`, `lessons.reinforce_pick`, сброс `notified_ts` → `sport_outcome_notify_loop` доставил исправленное уведомление. 362/362 тестов зелёные, ruff чист, рестарт чистый. Урок на будущее: если этот паттерн повторится на высокопрофильных матчах (где RU спортивная пресса пишет быстрые рекапы), success-rate должен быть выше — проблема специфична для нишевых турниров типа WTA 250 первого круга.

**2026-07-16 (без коммита, только данные) — аудит всех unsettled пиков за 7 дней по запросу владельца.** 14 unsettled пиков проверены через текущий `settle_outcome`+`_plausible_final_score`: 10 — структурно негрейдируемые рынки (тотал/фора по геймам и т.п., осознанное ограничение `D-sport-grade-coverage-limits`, не баг); 3 — грейдер появился ПОЗЖЕ в тот же день 07-14 (f20d38d/233dee9), счёт уже был правдоподобным → безопасный backfill; 1 (id 145, Альтмайер—Гастон) — ТА ЖЕ болезнь, что #205: заморожен на неправдоподобном «1:0» ДО обоих фиксов (плausibility-гейт 21d971b и score_recovery оба появились позже). Реальный счёт 2:0 (Альтмайер выиграл 6-2,7-5) подтверждён WebSearch+WebFetch sofascore.com. Все 4 пересеттлены боевым путём (`settle_outcome`→`reinforce_pick`/`postmortem_pick`→сброс `notified_ts`), уведомления доставлены (`tick sent=8`). Матчи всех 4 уже удалены cleanup — team/league/start_ts денормализованы на строке пика, этого хватило для ручной верификации без записи в sport_matches.

**2026-07-16, commit 970502d — фиксы по итогам аудита + важная самокоррекция.** Владелец выбрал 2 из 3 предложенных фиксов: (1) `check_still_live` теперь требует `packetVersion` в ответе перед тем как доверять пустому parse_events() как «матч завершён» (D-sport-check-still-live-schema-hardening); (2) `supervised_loop` watchdog на всех 7 sport-loop'ах — перезапуск с backoff при падении мимо try/except (D-sport-loop-watchdog), НЕ трогал общий `_spawn` в main.py (другие домены). (3) «Ослабить G27 для high-risk» — при реализации выяснилось, что гейт `confidence==5` живёт в МЁРТВОМ коде (`analyze_match`, никем не вызывается); реальный путь (`analyze_match_consensus`→`_validate_model_picks`) УЖЕ пропускает confidence>=4 — и всё равно 0 high-risk пиков за всю историю. Значит потолок — поведение модели, не софтверный гейт; фикс не применён, честно доложено владельцу как самокоррекция вместо тихого пропуска. 372/372 тестов, чистый рестарт.

**2026-07-16 — полный аудит всего sport-модуля (24 файла/8080 строк, 4 параллельных саб-агента по подсистемам) + фикс risk-tier в отбивке (commit e6e880a).** Топ-находка: `check_still_live`(fonbet_rest.py)→`get_event_details`→`_get_json`(fonbet_api.py) — если Fonbet когда-нибудь вернёт 200+невалидный JSON (протухшая сессия), пусто-парс трактуется как «матч подтверждённо завершён» вместо «неубедительно» — тот же класс риска, что и вся эта сессия, но на входе конвейера, не подтверждено живьём. Другие находки: нет watchdog на фоновых loop (мёртвый loop + зелёный heartbeat), self_audit.py тихо не замечает пропущенный LLM-индекс, odds_client.py frozenset даёт недетерминированный выбор sharp-book между рестартами (легаси-подсистема, не основной конвейер). Kelly/де-виг/edge-математика — чисто, багов нет. Отдельно: risk_tier=high НИКОГДА не встречается (0 из 228 пиков) — не баг, а гейт G27 «confidence==5 обязателен» структурно отсеивает контрарианские (high-odds) пики, т.к. модель реже уверена на 5/5 против рыночного консенсуса; тот же класс, что уже чинили для любительских лиг. Решения по фиксам (check_still_live hardening, watchdog, ослабление G27 для high-risk) — ждут владельца.

**2026-07-16 (без изменений в коде) — аудит кода грейдинга на другие классы багов** («сделай аудит, чтобы быть уверенными»). Вручную прочитан весь `results_ledger.py` (знаки гандикапов, push-ветки, диспетчеризация по типу спорта) — багов не найдено. Саб-агент независимо проверил `sport_analyzer.py` (1940 строк) — подтвердил, что грейдинг там отсутствует, весь централизован в `results_ledger.py`. `fonbet_parser.py`: team1/team2↔factor_index явно задокументирован и уже хардненый. Найден один СПЯЩИЙ (0 сеттленных пиков) риск: ориентация c1/c2 в `liveEventInfos.subscores` относительно team1/team2 не подтверждена явно кодом (в отличие от customFactors) — но ВСЕ рынки, читающие subscores, кроме одного, считают по сумме c1+c2 (порядок не важен, безопасны); единственный чувствительный — гандикап по жёлтым картам футбола, на нём пока 0 реальных сеттлов. Watch-item: сверить вручную при первом реальном сеттле этого рынка, код не трогали (ничего не подтверждено сломанным).

**2026-07-16 (без коммита, только данные) — расширенный аудит: проверил ВСЕ win/loss пики за всю историю (не только unsettled/не только 7 дней) на скрытый неверный грейд.** Владелец спросил «больше таких ошибок не будет?». Нашёл 7 win/loss строк на сет-спортах с неправдоподобным (замороженным после 1 сета) счётом — риск, что грейд «Исходы» мог совпасть со случайным лидерством, а не с реальным победителем матча. Каждую вручную сверил с реальным результатом (2 независимых источника на пик) — **все 7 оказались верно сгрейжены** (лидер 1-го сета в АТП/WTA туре статистически обычно и выигрывает матч, ~80-85%, но это НЕ гарантия — риск был реальным, просто не материализовался). Изменений в БД не потребовалось. Все 7 датированы ДО гейта `21d971b` (15 июля 21:44) — с этого коммита такой грейд физически невозможен (гейт стоит до `settle_outcome`). Ответ владельцу: для этого класса ошибки риск для будущих пиков закрыт архитектурно, не «повезло».

**2026-07-17, commit b857076 (v0.30.3) — «все матчи закрываются» — закрыт БОЛЬШИЙ, полностью невидимый канал `void`.** Владелец после жалобы на «не сверено» дал прямую директиву: «Бот должен все матчи закрывать, по всем видам спорта», затем уточнил приоритет: «нам требуется не просто, чтобы закрывались, а чтобы был понятен результат» (реальный win/loss важнее просто видимости). Разведка на проде вскрыла: `void` больше по объёму, чем видимый `unsettled` (30д теннис: 14 void vs 12 unsettled), и `void` НИКОГДА не уведомлял (`picks_pending_outcome_notify` фильтровал только win/loss/unsettled). Корень: `_try_score_recovery` был жёстко заперт на `m.status=='finished'` (авторитетная ветка) — пики, попавшие в очередь через эвристическую ветку («пропал из ленты»/«прошло много времени», `m.status` застрял на `'live'`) НАВСЕГДА исключались из восстановления, вечно пропускались на каждом тике, пока безусловный 48ч `void_stale_pending` не закрывал молча. **Фикс:** (1) `_try_score_recovery` больше не требует `match_status=='finished'` — единственная авторитетная проверка теперь независимый `check_still_live()==None` (свежее и надёжнее кэшированного bulk-поля, у которого уже была история неверной классификации, #182); (2) `sport_score_recovery_retry_interval_s` 900→7200, `max_attempts` 3→6 (было ~45мин до отказа, стало ~12ч — второй независимый источник для нижних турниров часто индексируется часами); (3) `void_stale_pending` теперь принимает опциональный `note`, ставит «🚫 БЕЗ РЕЗУЛЬТАТА», `picks_pending_outcome_notify` расширен на `void`. Старые void-строки без note не всплывают ретроактивно. **Осознанно не в этом проходе:** гейм-гандикапы тенниса/наст.тенниса и карта-маркеты киберспорта («Фора» без «по сетам», «Карта-матч П1П1») остаются unsettled — Fonbet не отдаёт эту гранулярность через уже используемый `subscores`-канал, нужен отдельный raw-перехват с живого матча. 1247/1253 тестов зелёные (+2), ruff чист, живой рестарт — 0 ошибок. Детали — Decisions `D-sport-void-notify`.

**2026-07-17, commit 206b92e (v0.30.7) — score_recovery стал универсальным (любой вид спорта + пики, у которых счёт вообще никогда не появлялся).** Сразу после серии фиксов по сет-видам владелец прислал ФУТБОЛЬНЫЙ пик («Фамаликан — Лузитания Лороза»), ушедший в `void` с полностью пустым счётом (`score_recovery_attempts=0`) — директива: «не лечить симптомы, а раз и навсегда исправить на всех матчах и ставках». Разведка вскрыла корень существенно крупнее всего починенного до этого: (1) `pending_picks_to_settle` требовал непустой `m.score` для отбора строки вообще — пик, чей матч НИКОГДА не получил ни одного live-снимка (коллектор не успел / матч сняли с линии до первого тика), не попадал в цикл сеттла НИ ДЛЯ ОДНОГО вида спорта — тихо доживал до 48ч void; (2) `_try_score_recovery` был жёстко заперт на `_MIN_WINNER_UNITS` (5 сет-видов) — рудимент от ПЕРВОНАЧАЛЬНОГО бага (застывший счёт по СЕТАМ), хотя сам `score_recovery.recover_final_score` никогда не был sport-restricted. **Фикс:** (1) новая OR-ветка в SQL — пики без счёта отбираются, если `start_ts` старше `never_scored_after_s`=4ч (запас дольше любого реального матча, но оставляет ~44ч на попытки до 48ч void); (2) ограничение по спорту в `_try_score_recovery` убрано целиком; (3) `settle_pending`'s цикл теперь одинаково триггерит восстановление на «застывшем» И на «вообще пустом» счёте (раньше пустой счёт молча пропускал блок восстановления); (4) сам механизм восстановления имел sport-bias — LLM-промпт и один из 4 поисковых запросов буквально говорили «счёт по сетам», бессмысленно для футбола — теперь sport-aware. 1281/1287 тестов (+13, включая точный кейс из жалобы: футбол, «Фора / Ф2 +1.5», восстановленный счёт «1:2» → win). Живое срабатывание на проде не поймано за сессию (never-scored пики появляются медленно) — подтверждено юнит-тестами с реальными числами жалобы, не догадкой. Детали — Decisions `D-sport-universal-recovery`.

**2026-07-17, commit a3b00ad (v0.30.8) — ещё 3 независимых корня сразу после universal-recovery.** Владелец прислал скриншот с ТРЕМЯ новыми свежими «не сверено» (MMA, футбол, наст. теннис) в тот же день: «мы же договаривались, что больше такого статуса не будет... требуется глобально избавиться от этой проблемы». Каждый оказался отдельной, ранее не тронутой причиной: (1) **футбол** («Тотал / М 4.5», счёт 5:0 плаузибельный, но unsettled) — Fonbet даёт альтернативный factor id (1791) на тот же тотал-рынок помимо документированных 930/931, а `_settle_by_label` не имел фолбэка на обычный текстовый лейбл «Тотал / Б|М X»; добавлен фолбэк (переиспользует `_RE_SET_TOTAL`); заодно baseball/rugby/cricket добавлены в `_TOTAL_SPORTS` (были полностью пропущены, хотя очки/раны — прямой аналог голов; один rugby-тотал уже успел уйти в void). (2) **наст. теннис** (сет-счёт застыл на невозможной ничье «2:2», 6 попыток веб-recovery исчерпаны) — диагноз структурный: Setka Cup/TT Cup физически нет в интернете, веб-поиск для них НИКОГДА не сработает; новая `_recover_set_score_from_subscores` считает истинный сет-счёт из уже захваченных СОБСТВЕННЫХ per-партийных subscores (см. [[delor_sport_set_unit_grading]]) ПЕРЕД веб-recovery — не нужно внешнее подтверждение вообще. (3) **mix-fights** (MMA/бокс, счёт «0:0») — живой запрос `FonbetApi.get_events_list` подтвердил: единоборства структурно НИКОГДА не несут содержательный счёт (не баг); скрытый риск похуже «не сверено» — 0:0 проходило «правдоподобным» после 45 мин и могло СИСТЕМАТИЧЕСКИ НЕВЕРНО грейдить будущие moneyline-пики как `loss` (0>0=False); добавлен `_NO_SCORE_SPORTS`-guard в `settle_outcome`. Round-market recovery для единоборств (тотал раундов/победитель) сознательно НЕ реализован — нужна отдельная живая разведка на реальном бое (0 live mix-fights матчей в момент аудита), зафиксировано как открытый пункт. 183/183 sport-тестов (+18), 1299/1305 полного набора, ruff чист, живой рестарт чист. Пик 290 (футбол) вручную пересверен на loss. Пики 267/208 (наст. теннис) — данные уже потеряны (matches-строка удалена до фикса), остаются honest unsettled. Детали — Decisions `D-sport-alt-total-factor-own-subscores-noscore`.

**2026-07-14, commit 6b5ea61 — грейд футбольной статы (углы/карты/удары/офсайды/фолы).** Расширил захват subscores на стата-kind'ы (`400100` corners, `400200` cards, `400400` shots_target, `400500` offsides, `400300` fouls) — кумулятивные `[c1,c2]`, БД-миграции нет (весь dict уже в `subscores_json`). Грейдер `_settle_stat_market` (football): тоталы `Тотал|Кол-во <стат> / Б|М X`, фора по картам (football=жёлтые; esports «Фора по картам»=maps через `_SET_OR_MAP`), присутствие `угловой|офсайд|фол / Да`, сравнения (`угловых больше чем ударов в створ`). Нужной статы нет в снимке → None (не угадываем). **Отличие от таймов, важно:** у статы НЕТ якоря валидации (таймы сверялись с финальным счётом), а снимок ~на 60с (тик коллектора) раньше FT. **Выбор владельца:** снимок=финал, симметричный грейд. Комбо (`победа/ничья + тотал`) и тайминг (`до 1-го X`) остаются honest-unsettled — отдельная ось риска ([[delor_sport_amateur_leagues_h2h]] нет; см. Decisions `D-sport-grade-coverage-limits`). NB: бот за всё время выбрал ~0 футбольных стата-пиков — ценность в основном future-proof. Живьём: прод-матчи несут стата-subscores, settle_outcome на реальных значениях грейдит верно. 1191 passed (+25). Детали — Decisions `D-sport-football-stat-grading`.
