---
name: avtero_hard_corrections_learning
description: avtero — надёжное обучение через жёсткие part-корректировки (одобрил урок → OEM правится детерминированно на сайте+боте); жалоба АДМИНА применяется авто (без ручного гейта); A (авто-supersession) отвергнут
metadata:
  node_type: memory
  type: project
  originSessionId: 4eea915e-3b47-4cd5-8a0e-bfa29b78a87c
---

**2026-07-10 — АВТО-ПРИМЕНЕНИЕ обучения для админов (юзер: 2 админа, их жалобы применяются сразу).** Дистилляция была **ленивой** (урок активируется только когда админ открывает карточку в боте). Теперь: жалоба админа (❌ в боте ИЛИ на сайте) → **сразу** active-урок + жёсткая коррекция, без ручного одобрения. Не-админы — прежний ручной гейт. Единый server-путь `app/services/auto_learn.distill_and_apply(feedback_id, decided_by)` (дистилляция→active-урок→part_correction→feedback=done, идемпотентно, на `no_lesson` фидбэк остаётся `new`). Вызов: бот через `POST /api/feedback/apply` (guard `_bot_authed`, `avtero_client.apply_feedback`) фоновой задачей в `bot/handlers/feedback.py._auto_apply_admin`; сайт in-process в `/api/feedback` при `tg_id ∈ _pk_admin_ids()`. **Экстрактор `part_key`+`correction_from_lesson` вынесен из admin.py в `app/services/corrections.py`** (единый источник; `admin._part_key`-алиас для тестов). **ФИКС:** код двигателя («с двигателем D5244T15») OEM-образен → брался как correct_oem; теперь вырезается (`двигател\w*|мотор\w*|engine + код`) до поиска OEM. Бэкапы `*.bak-autolearn-1783697725`. Бэкфилл 6 висящих Volvo XC70-жалоб: 5/6 применены (ремень 31325042, вкладыши 31251996, прокладка 31430317, подшипник 712076410, втулка AWD 30787669); fid13 масло 31259380 дистиллятор не берёт → остался pending. Проверено: 5 коррекций срабатывают в подборе + бот-e2e (admin 1005954706, реальная bot.db) + suite 1054+. См. [[Decisions#D-admin-feedback-autoapply]]. Ниже — история движка коррекций (2026-07-01):

**2026-07-01 — «обучение надёжно работает» (юзер: чтобы урок реально фиксил, а не мягкая подсказка ИИ).** Кейс: Максим (@M822BE, TG 983512693), Volvo XC90 2007 — «втулка AWD» выдавала OEM **9139539**, верно = **ремкомплект 31437982** (с 2 сальниками). Раньше урок = только текст в промпт LLM (на фолбэк не действует, можно проигнорить).

**B — движок жёстких коррекций (СДЕЛАНО, проверено живьём):**
- Таблица `part_corrections` в `bot.db` (make, model, part_kw, wrong_oem, correct_oem, note, status, source_lesson_id). Создаётся авто (init + бот CREATE IF NOT EXISTS).
- `app/services/corrections.py` — read-only из bot.db (TTL 60с, как lessons), `apply_to_part(make,model,name,oem)`: матч по make(+model если задан) И (wrong_oem==resolved OR part_kw in name) → {oem, note}.
- `orchestrator._apply_part_corrections(make,model,parts)` (dict+pydantic Part) врезан в **3 точки**: `_strict_catalog_report`, `_build_catalog_fallback`, LLM-путь после `_enrich_alternatives_from_tecdoc` → покрывает **оба пути и оба канала** (общий API). Переопределяет primary_oem, гасит needs_check, дописывает note. Бэкап orchestrator `*.bak-corr-*`.
- Сид Volvo XC90 awd 9139539→31437982 (owner-approved). **Проверено live /api/ask:** Volvo XC90 «Замена втулки AWD» → **31437982** + «Ремкомплект — включает 2 сальника», не 9139539.

**Петля обучения (СДЕЛАНО): одобрение урока → авто-создание коррекции.** `bot/handlers/admin.py` (бэкап `*.bak-learn-*`) callback `admin:les:ok`: `_correction_from_lesson(lesson_text, snapshot)` извлекает correct_oem (OEM в уроке, которого НЕТ в report_text) + wrong_oem (который есть) + part_kw (латинская аббревиатура/длинное слово из work_text), пишет строку в part_corrections (active) и сообщает админу что создал. Экстрактор юнит-проверен на уроке Максима → volvo/xc90/awd/9139539→31437982. Бот перезапущен (`local.avtero-bot` kickstart, polling ок). ⚠️ Полный TG-e2e кнопки «одобрить» НЕ прогнан (нужен админ-TG); экстрактор+insert+API-apply проверены по отдельности.

**2026-07-01 (продолжение) — 2 урока одобрены юзером, петля проверена E2E, но 2 хвоста.** Юзер одобрил уроки #1(AWD) и #2(масло в раздатке) в боте — но ДО рестарта бота с проводкой → авто-коррекции не создались. Бэкфилл (`/tmp/backfill_corr.py`, та же логика что `_correction_from_lesson`) добил: part_corrections теперь id=2 (volvo/xc90/awd 9139539→31437982, source_lesson_id=1, РАБОЧАЯ) + id=3 (volvo/xc90/part_kw=«разда»/wrong пусто→31259380, source_lesson_id=2, **ИНЕРТНА**). Дубль-сид удалён. Экстрактор работает → петля жива для будущих одобрений (бот перезапущен с handler'ом). **Кейс #2 НЕ починен** (task_2341f811): «замена масла в раздатке» — неверная ИНТЕРПРЕТАЦИЯ РАБОТЫ (выдаёт моторный фильтр 1275810 + пробку 986833 вместо одной жидкости 31259380); part→OEM коррекция не подходит, нужен work-level replace_all/суппресс. **task_621f0754 СДЕЛАНО (2026-07-01):** в карточке юзера бот-админки кнопка «👑 Сделать админом» / «⬇ Снять админа». Динамические админы в bot.db `admin_grants(user_id,granted_by,created_at)` (CREATE IF NOT EXISTS). `bot/database/operations.py`: grant_admin/revoke_admin/get_granted_admin_ids/is_granted_admin. `admin_guard.py`: env∪granted с TTL-кэшем 30с + invalidate_admin_cache(). `app/main.py._pk_admin_ids`: теперь env∪granted (была бага — при заданном env granted игнорились) → админ на боте И на сайте (когда TG-аккаунт слинкован). Env-админы неотзываемы из UI (кнопка «👑 из конфигурации»→noop). Хендлеры admin:grantadm/revokeadm/noop в `bot/handlers/admin.py`. Бэкапы `*.bak-admgrant-*`. Проверено: grant→is_admin True на боте+API (env-админ цел), revoke→False; кнопка корректна в 3 состояниях; бот polling без ошибок. ⚠️ Живой tap в TG НЕ прогнан — тест-аккаунт tg-web-e2e (8688355181) не админ; проверять самому юзеру (1005954706).

**2026-07-01 (кейс #2 ЗАКРЫТ СИСТЕМНО — «учитывать особенности авто»).** Не OEM-своп, а знание узлов. `work_groups._is_unit_oil_change()`: если работа называет агрегат трансмиссии/рулевого (раздат/редуктор/мост/диффер/АКПП/вариатор/CVT/DSG/МКПП/КПП/ГУР) И нет «двигател/мотор» → правило `oil_service` подавляется (движковый масляный фильтр + сливная пробка НЕ бандлятся). System prompt блок 4b «масло по узлу»: трансмиссионное GL-5 75W-9x / ATF / CVT-жидкость / PSF + сервис-фильтр для АКПП/вариатора; для узла без данных — честно primary_oem="". Бэкапы `work_groups.py.bak-unitoil-*`, `orchestrator.py.bak-unitoil-*`. **Проверено live** (VIN YV1CM595771400836, «Замена масла в раздаточной коробке»): 1 позиция = Масло раздаточной коробки OEM 31259380 1.0л, confidence high, движкового фильтра/пробки НЕТ. Юнит-тест 10 фраз ALL OK. Инертная коррекция id=3 удалена (осталась только id=2 AWD). task_2341f811 закрыт этим фиксом.

**2026-07-01 (matching-фикс v0.7.14 — коррекции матчатся в ОБОИХ путях + будущее обучение проверено).** Найден баг: `_requested_parts` строил имя детали из АНГЛ. ключа категории (`_CATEGORY_RU` не имел датчиков) и терял сторону лев/прав → в детерминированном (catalog-fallback) пути коррекция `датчик перед лев`/`прав` не матчилась (обученный фикс терялся при отказе LLM). Фикс (`orchestrator.py`, бэкап `*.bak-namesside-*`): датчики в `_CATEGORY_RU`; имя fallback на русский raw (не англ.ключ) + сторона (лев)/(прав) в имя; второй catalog-builder берёт готовое `r["name"]`. `admin._part_key` (будущие уроки) проверен на 10 фразах: склонения (колодок→колод, слева→лев, спереди→перед)+ось+сторона, AND не пересекает стороны 8/8. **Live через бот-путь:** XC60 «датчик ABS передний правый»→**32373144** (sources feedback_rules, needs_check=false); обе стороны подтверждены живьём (лев 30748149, прав 32373144). Мелкое огранич.: «левый И правый» одним куском без запятой в детерминированном фолбэке даёт только левый (LLM-путь ок). Связано [[avtero_validation_audit_axle]].

**2026-07-01 (хардненг «чтобы не повторялось», v0.7.14).** (1) **Side-honesty** зеркалит axle: category-фолбэк при одной запрошенной стороне → confident=False+note (бэкап `*.bak-sidehonesty-*`); коррекция поверх снимает флаг. (2) **Гард инертных/сверхшироких коррекций** в `_correction_from_lesson`: без noun/latin-якоря И без wrong_oem → None (мягкий урок); ключ только из ось/сторона → обнуляется, строка держится на wrong_oem (бэкап `admin.py.bak-hardening-*`). (3) `_PART_NOUNS` 26→62 существительных. (4) **Регрессионный suite `tests/test_learning_corrections.py` (13 тестов)** замораживает все инварианты (сплит ось×сторона, русские имена, token-AND без пересечения сторон, гарды). Полный suite **850 passed/0 failed**; 4 старых фейла были stale-тестами (rule-18 политика ужесточена осознанно: search_reviews/search_cross запрещены целиком; TestCrossOe не изолировал harvest-слой local_brands — по дизайну cross_oe мержит его и работает при выключенном MariaDB) → тесты обновлены (бэкапы `tests/*.bak-stale-*`).

**A — авто-само-определение через каталог ОТВЕРГНУТО (не безопасно на наших данных).** `supersession()` в TecDoc есть у ~5/7 OEM, НО возвращает **аффтермаркет-номера** (2630035503→CMB11341, 517121M000→1618886180 и т.п.), а не genuine-OEM замену; genuine-kit связь (9139539→31437982) в данных ОТСУТСТВУЕТ (проверено — supersession/kit_parents/kit_contents пусты для обоих). Авто-апгрейд primary_oem по этой таблице заменил бы заводской OEM на аффтермаркет = новые ошибки. Вывод: «чтобы ИИ сам определял, вообще без обучения» — НЕдостижимо для того, чего нет в каталоге; надёжный путь = B (коррекции держатся). Управление коррекциями (список/удаление из бота) — TODO. См. [[avtero_feedback_learning]] [[avtero_reliability_fallback]].
