# avtero — обзор проекта

**Актуальное состояние: v0.7.11 (2026-06-22).**

**v0.7.9-0.7.11 (2026-06-22) — полное покрытие через genuine emex-каталог.** Цель: убрать белые пятна, покрыть кузов/электрику/всё. **v0.7.9**: словарь синонимов в `resolve_by_name` (вкладыш↔подшипник/BEARING, шатун↔шток, корен↔коленвал) — вкладыши коренные 210202B000, шатунные 230602B913. **v0.7.10**: аудит 93 деталей вскрыл, что прошлый «полный охват» был на ходовке; resolve_by_name давал ложные совпадения по родовым словам (фильтр АКПП→воздушный, ремень ГРМ→поликлин) → ужесточил: матч валиден только если покрыт ≥1 НЕ-родовой концепт (`_GENERIC_STEMS`). **v0.7.11**: общий резолвер — **LLM-выбор из реального каталога** (PASS2 gap-резолвера): LLM выбирает раздел из 350 листьев дерева → выбирает деталь из реальных позиций (genuine-OEM, без галлюцинаций, на любой словарь). Покрыло кузов (бампер 866111M010, фара 921011M020, зеркала, ручка двери 826551M000), электрику (замок зажигания 819002M710, катушка 273252B000, моторчик стеклоподъёмника 824211M010). Слои: PASS1 stem/synonym → PASS2 LLM-каталог → PASS3 LLM-кандидат+emex-verify. Доп: type-reject исключает квалификаторы (зажига/наружн/внутре) — иначе «замок зажигания» получал свечу IZFR6K13. **Механизм make-agnostic** (Volvo VIN→cat VOLVO201410 441 раздел, нашёл колодки 31262874/фару 30744015/радиатор 8699208), но hit-rate ниже на больших каталогах (Volvo ~4/12: разделы до 649 позиций обрезаются капом pool[:150]; pre-filter пробовал — хуже, откатил). RU-VIN кроме Kia (Solaris/Polo/Renault/Lada) emex в genuine-каталог НЕ резолвит → им TecDoc+аналоги. **Пустых деталей (без OEM И аналогов) НЕТ.** commits f-цепочка→4116f7b. ⚠️ PASS2 = 2 LLM-вызова/gap-деталь (кэш дерева 7д); 5-мин LLM-кэш (vin,question) маскирует правки — варьируй текст. Decisions D-gap-oem-resolution.

**v0.7.8 (2026-06-22) — OEM деталей вне свободного TecDoc (убрана «уточните у дилера»).** Внутренности ДВС (маслосъёмные колпачки, направляющие клапанов, прокладка клапанной крышки) НЕТ в свободном дампе → primary_oem пустой. LLM-знания для таких номеров НЕНАДЁЖНЫ. Решение — genuine emex-каталог по VIN (API, БЕЗ Playwright): `emex_catalog.resolve_by_name(vin,name)` находит группу ГБЦ/клапанного механизма (своя категория + tree-листы по stem-overlap + valvetrain-якоря) и сопоставляет ИМЯ детали с именами деталей каталога по **5-символьным стемам** (сплит дефис/слеш, устойчиво к рус. окончаниям). Gap-пасс `orchestrator._resolve_gap_oems_via_knowledge(parsed,veh,vin)` (после всех каталог-пассов, только пустые primary): PASS1 genuine-каталог по имени; PASS2 LLM-кандидат оставляем ТОЛЬКО если emex.search_by_oem подтверждает бренд+тип; расходники (смазка/паста/…) пропускаются. Плюс `_reject_type_mismatched_primary` — гасит реальный-но-не-того-типа primary (масляный фильтр 26300-35503 на колпачки: бренд совпал, тип нет). Живьём+TG (Kia Cerato G4FC): колпачки→222242B010, направляющие→221142B000, прокладка крышки→224412B002, без dealer-note. commit 920ddb2, Decision D-gap-oem-resolution. ⚠️ деталь, которой нет даже в genuine-каталоге VIN → честная пометка (фабрикации нет). 5-мин LLM-кэш (vin,question) маскирует правки — варьируй текст запроса.

**v0.7.2 (2026-06-21) — подбор по НОМЕРУ КУЗОВА (JDM chassis number) без VIN.**

**v0.7.2 (2026-06-21) — подбор по НОМЕРУ КУЗОВА (JDM chassis number) без VIN.** Японские авто (СТС «Кузов № GP23035779», VIN отсутствует) подбираются ТОЧНО: префикс кузова (`GP2`, `NZE141`, `K13`) — шасси-код, TecDoc хранит его в `models.fulldescription` (конкретно или wildcard-семейством `GE_`/`_E14_`). `_resolve_ktypes_by_chassis_sync` (tecdoc_local): кандидаты (отрезать 7/6 цифр серийника) × bidirectional wildcard-матч (`_`→`.`; ⚠️ `re.escape` НЕ экранирует `_`, строить по-символьно) × отбор по самому длинному core (убирает Toyota over-match). `chassis` протянут через `_resolve_ktypes2018_sync`→`vehicle_fitment_set` (один funnel). `VehicleDecoded.frame`, OCR извлекает `frame`, API `confirm_frame`, бот (фото+текст-regex `_extract_frame`), UI-поле «Номер кузова». recon 15/15; live GP23035779→3 KType/121 articles; **E2E реальный TG**: «подберу по номеру кузова GP2-3035779»→колодки+фильтр fitment-verified, success=1. commit bb5bbfd. Decision D-chassis-number-2018.

**v0.7.1 (2026-06-21) — точный режим (catalog-only) + TG web-auth + UI.** Rule 2 промпта: `primary_oem` ТОЛЬКО из каталога, никакой памяти LLM (пустой+аналоги честнее угаданного). Убрана пометка «ориентировочный» для авто без VIN. TG web-login без домена (deeplink `?start=login_<token>`+HMAC, cookie `pk_session` 30 сут, залогиненные без trial). Полный редизайн index.html. commits 7076752+abe56fa.

**v0.6.23 (2026-06-20) — авто БЕЗ VIN + фикс года.** (1) **Год по VIN:** буквенный код 30-летнего цикла больше не уезжает в будущее — отсечка по текущему году (`V`→1997, не 2027); НЕ через позицию 7 (ломает евро/азиат, Kia `D`→1983). (2) **Подбор без VIN:** марка/модель/год → `_resolve_ktypes2018_sync`→KType→`fitting_articles_by_category` (движок backfill v0.6.19); вход через `predecoded_vehicle`+`confirm_make/model/year` (уже были). Проверено живым `/api/ask_async`: Honda Fit Shuttle без VIN → 8 аналогов/деталь. (3) **Мост JDM** `_MODEL_ALIAS` в tecdoc_local: Fit Shuttle→Jazz, Vezel→HR-V, Vitz→Yaris, March→Micra (TecDoc евроцентричен, JDM-имён нет). (4) **OCR СТС:** `ocr.extract_vehicle_identity`+`POST /api/ocr_identity` (vision→JSON). (5) **Бот:** фото СТС→определяем→подтверждение/правка текстом; текст «Honda Fit Shuttle 2012»→`_parse_vehicle_text` (нужен год). (6) Честная пометка «без VIN — сверьте». git commit 0587296; 790+12 тестов. ⚠️ НЕ автотестом: TG-walk кнопок (тест-аккаунт не approved у @avtero_bot) + vision на реальном фото — юзер проверит фото. Decision D-novin-coverage-2018. Бэкапы `*.bak-pre-novin`.

**infra (2026-06-18):** avtero теперь **локальный git-репо** (`~/Services/avtero`, commit 1edf038, 135 файлов; `.env`/`data/`/`*.db`/`*.bak`/`.attic` в .gitignore, `.env.template` хранится; remote НЕ добавлен — как у соседей) и **в ops-дашборде** (3 записи: `avtero` API :8201, `avtero_bot`, `avtero_tunnel` в `~/Services/ops_dashboard/dashboard_config.json`). Грабли дашборда: бот match `python@3.14.*bot.main` (НЕ `bot.main` — коллизия с zemlya-bot); process_match НЕ должен начинаться с `--` (pgrep примет за опции). ⚠️ **IP мини меняется** — `192.168.0.107` сегодня отказал, ходи через `cebers_mini@Cebers-mini.local`. Детали: Obsidian Changelog → infra (2026-06-18).

**v0.6.22 — независимый резерв deepseek-chat (gemini-cli-устойчивость).** Из CebersResearch (:20130, 11 моделей) проверены кандидаты на tool-calling: `deepseek-chat`/`gpt-5.4-mini`/`gpt-5.5`/`deepseek-reasoner` УМЕЮТ tools (~3-4с лёгкий пинг), `qwen-max` мёртв. `deepseek-chat` проверен под РЕАЛЬНОЙ нагрузкой (форс единственным провайдером): радиатор 54с/тормоза 29с, fb=0, без ошибок — НЕ захлёбывается. Добавлен **#2** в цепочку: `nvidia:openai/gpt-oss-120b → cebersresearch:research/deepseek-chat → nvidia:meta/llama-3.3-70b → gateway:cerebras/gpt-oss-120b → gateway:kiro/claude-haiku-4.5`. Зачем: оба прежних надёжных = nvidia-direct (один ключ = единая точка отказа); deepseek-chat — независимая инфра (CebersResearch web2api), бесплатен, не зависит от gemini-cli/nvidia. gemini-cli из цепочки УЖЕ убран (v0.6.21). ⚠️ `nvidia/gpt-oss-120b` (primary) интермиттентно ReadTimeout-ит под нагрузкой (~1 из 4 тяжёлых) → failover на deepseek/llama (стоит ~45с таймаут+fallback); цепи resilience это ловит (live: primary завис → выдало 12 аналогов с fb=1). Если захочется убрать failover-задержку — кандидат повысить deepseek-chat (2/2 чисто) в primary, но он web2api (фрагильнее официального nvidia). gpt-5.4-mini/gpt-5.5 — годные кандидаты на будущее (под нагрузкой не гонял). Только плисты, провайдер `cebersresearch` уже в app/llm.py.

**v0.6.21 — надёжность LLM-цепочки (убрала таймауты):** замер показал, что в боте ПЕРВЫМ стоял мёртвый `gateway:cerebras/zai-glm-4.7` (0/4) → всё валилось на медленный nvidia/llama. Плисты бота и API имели РАЗНЫЕ цепочки. Главный урок: **лёгкий пинг ≠ нагрузка** — gateway-модели (cerebras/kiro через OmniRoute :20128) пингуются <1с, но под реальным tool-loop отдают 503/ReadTimeout; стабильно тянет только `nvidia:openai/gpt-oss-120b` (direct). Новая цепочка (оба плиста): `nvidia:openai/gpt-oss-120b → nvidia:meta/llama-3.3-70b → gateway:cerebras/gpt-oss-120b → gateway:kiro/claude-haiku-4.5`. **`LLM_ROTATE_CHAIN=0`** (новый флаг, orchestrator:1925 — ротация рассылала на упавший gateway), **`LLM_TIMEOUT_SEC=45`** (было 60). Радиатор 149с→39с, тормоза 112с→25с, оба fb=0. Мёртвые провайдеры (zai-glm, gemini-cli, gemini-flash, qwen3.5-спайки) убраны. Бэкапы плистов `*.bak-pre-chainreorder`. ⚠️ Инфра-долг: OmniRoute :20128 503-ит под нагрузкой — почини → верни cerebras-gpt-oss первым (тогда тяжёлые ~7с). **Перезагрузка плистов = `launchctl bootout` + `bootstrap` (kickstart НЕ перечитывает env!); после bootout проверяй `launchctl list | grep avtero` — сервис мог не подняться.** Decision D-llm-chain-reliability. Раньше: Полная история — Obsidian `Projects/avtero/Changelog.md` + `Decisions.md` (читать ПЕРЕД изменениями). Ключевое:
- **applicability tecdoc2018 (v0.6.18)** — отдельная БД `tecdoc2018` на MariaDB :3307 (схема `td_1.2018`), полный дамп 1Q2018 с реальной `article_links` (216.5М, KType↔артикул). Фитмент-гейт верифицирует аналог в первую очередь по тому, что TecDoc явно перечисляет его подходящим KType авто (`vehicle_fitment_set`); OE-sharing (v0.6.17) — резерв.
- **backfill аналогов из применимости (v0.6.19)** — когда у детали верифиц. аналогов <2 (emex не якорит OEM / cross пуст / LLM не дал): `tecdoc_local.fitting_articles_by_category(ktypes, like_patterns)` тянет реальные бренд+номер по категории (`article_links`⨝`prd`, матч по `prd.description`), добавляет как `fitment_verified=True`. Карта `_TD_PRD_LIKE` (63 категории). Это закрыло провалы: рычаг→LEMFÖRDER/FEBI, катушка→BERU/BOSCH (cross 0), радиатор→HELLA, шаровая→MOOG. Decision D-analog-backfill-2018.
- **язык вывода (v0.6.19)** — правило 9 «ЯЗЫК» в SYSTEM_PROMPT: названия деталей/описания на русском, англ. каталожные имена переводить, бренды/номера латиницей. [[avtero_russian_output]].
- **баг-фикс v0.6.19** — `tools.py` не импортировал `asyncio` на уровне модуля → `_run_search_web` всегда падал NameError (даже в except). Фикс: `import asyncio` в шапку.
Прежнее (v0.6.16-17): pre-flight гейт подтверждения авто; фитмент-гейт `fitment_verified`; make-guard (emex_catalog НЕ brand-verified by construction, кейс VW VIN→Toyota колодки). LLM-цепочка: …→ nvidia/llama-3.3-70b →… (llama бывает медленный/ReadTimeout на тяжёлых запросах типа «радиатор» — fail-over работает, но долго; известный intermittent). Грабли верификации: 5-мин LLM-кэш `resp_*` (redis db2) — варьируй region чтобы сбросить; trial-лимит чистить trial:/iprate:/burst: в db2. БД-пароль `TECDOC_DB_PASSWORD` только в plist (extract: `set -a; . /tmp/pk_env.sh`). venv: `~/Services/avtero-venv` (python3.14, playwright есть). Бэкапы кода `*.bak-pre-v0619`.

---

# (ниже — историческое описание v0.4.9)

**Что это.** Сервис подбора запчастей по VIN + описанию работ. Принимает «KNAFW411BD5677381 + замена колодок ручника» → отдаёт точные OEM-номера, до 5 аналогов на каждую запчасть, техжидкости, и сопутствующие части по конвенциональному «комплекту по уму» для каждой работы.

**Прод.** mac mini (LAN-only, RU): `http://192.168.0.107:8201/`. Доступ к мини: `ssh -i ~/.ssh/id_ed25519 cebers_mini@192.168.0.107`. App-dir `/Users/cebers_mini/Services/avtero/`. Service: launchd `local.avtero`, рестарт `launchctl kickstart -k gui/501/local.avtero`.

## Стек

- **FastAPI** + uvicorn :8201, Jinja2-templates, нативный fetch + SSE (HTMX оставлен в `<script>` тегe но не используется)
- **SQLite WAL** для логов запросов (`data/avtero.db`)
- **Redis DB 2** + file-cache (`data/cache/`) для кешей скрапера (`exist_vin`, `exist_cat`, `parts`, `emex_oem`, `reviews`, `web`)
- **LLM chain** через `PROVIDER_CHAT_FALLBACK` в `.env`:
  - `gateway:cerebras/zai-glm-4.7` (primary, через локальный OmniRoute :20128)
  - `nvidia:meta/llama-3.3-70b-instruct`
  - `gateway:cerebras/gpt-oss-120b`
  - `gateway:gemini-cli/gemini-3.1-flash-lite-preview`
  - `nvidia:openai/gpt-oss-120b`
  - Битый `gemini-3-flash-preview` удалён в v0.4.2
- **NIM vision** для OCR (`meta/llama-3.2-90b-vision-instruct`, NVIDIA_API_KEY)
- **Playwright** установлен но НЕ нужен в hot-path (exist парсится httpx-ом, серверный HTML)

## Ключевые файлы

- `app/main.py` — FastAPI endpoints. `/api/ask` (sync, backward-compat), `/api/ask_async` (SSE-job), `/api/jobs/{id}/events` (SSE stream), `/healthz`, `/`.
- `app/services/orchestrator.py` (~1700 строк) — main pipeline: VIN decode → `_llm_with_tools` → tool loop с **parallel asyncio.gather диспатчем** → boundary overrides → validation. SYSTEM_PROMPT и `_PLAYWRIGHT_DISABLED_OVERRIDE` тут же.
- `app/services/tools.py` — `search_oem`/`search_cross`/`search_reviews`/`search_web` инструменты для LLM
- `app/services/ocr.py` — NIM vision OCR (v0.4.4)
- `app/services/jobs.py` — in-process SSE job registry (v0.4.5)
- `app/scrapers/exist.py` — главный VIN-каталог. `resolve_vin()` для decode, `search_oem()` для парт-категорий. Полностью httpx, без Playwright. Категории матчатся LLM-категоризатором (`_pick_category_llm`) с fallback на fuzzy `_pick_category` + synonyms.
- `app/scrapers/emex.py` — OEM lookup + **`resolve_vin()` (v0.4.8)** через `POST /api/parts/vin` (secondary VIN-каталог после exist). Парсит brand/name/date/engineno/options.
- `app/scrapers/zzap.py`, `app/scrapers/reviews.py`, `app/scrapers/web_search.py`
- `templates/index.html` — SPA-форма, SSE прогресс-лог, render result. **PWA-manifest + install prompt + «Гараж» (v0.4.8)**.
- `static/manifest.webmanifest`, `static/icon-192.png`, `static/icon-512.png`, `static/icon-512-maskable.png` — PWA assets (v0.4.8)
- `.attic/` — backup'ы старых версий orchestrator/exist/tools/env (v0.4.2 уборка)

## Поток обработки

1. POST `/api/ask_async` → возвращает `{job_id}`, спавнит background task
2. Background task: read photo bytes → OCR (если есть) → `orchestrator.run(progress_cb=emit_to_job_queue)`
3. `_decode_vin(vin)`: exist.resolve_vin → **emex.resolve_vin (v0.4.8)** → LLM-decode (vinformer убрана в v0.4.9 — captcha-gated)
4. `_apply_vin_overrides`: deterministic year (ISO 3779 pos-10), WMI brand, VDS-family model, transmission strip
5. `_llm_with_tools`: tool-calling loop с **parallel dispatch** (16 tool calls в 4 раунда — asyncio.gather)
6. Boundary overrides: year user-stated, OEM brand mismatch guard (VAG), hallucination-pattern guard (KIT-XXX, FAKE-, etc), backfill primary_oem из alternatives для химии
7. `PartListResponse.model_validate` → если упало → retry → recovery-response
8. SSE `complete` event с payload → клиент рендерит

## Универсальное покрытие брендов (v0.4.7)

**WMI таблица ~150 префиксов**, разделены Lexus/Infiniti/Acura/Genesis от parent-брендов. Покрытие:
- exist-каталог: 95% иномарок РФ (Korean, VAG, Japan, German premium, French, Italian, UK, US, China)
- LLM-fallback (WMI + VDS family + LLM-decode): Russian (Lada/UAZ/GAZ), Iran (Iran Khodro/Saipa), редкие

**VDS-family pre-LLM override** (`_VDS_FAMILY`): Kia (KNA/KND) F=Cerato/Forte, E=Ceed, J=Sportage/Sorento, P=Rio, B=Picanto, M=Soul, etc. Hyundai (KMH): C=Solaris, J=Tucson, S=Santa Fe, etc. Lada (XTA): G=Granta/Vesta, K=Kalina, L=Largus, X=Xray, N=Niva.

**Multi-word makes** в exist car_info parsing: Land Rover, Mercedes-Benz, Alfa Romeo, Aston Martin, Great Wall, SsangYong.

**Rule 16a** в системном промпте: известные дефекты массовых моторов (BMW N47 цепь, VAG TSI EA888 Gen2 масложор, Hyundai/Kia Theta II задиры, Renault K9K турбина, Ford 1.6 EcoBoost, Mazda Skyactiv-D, Toyota 1NZ/1ZZ цепь, Nissan HR16DE цепь).

## Эволюция v0.3.20 → v0.4.7

| ver | что добавлено |
|---|---|
| v0.3.20 | year user override, handbrake-shoes в rule 14 |
| v0.3.21 | pre-LLM VDS-family fix, primary_oem backfill из alts |
| v0.3.22 | real PNG logo, transmission strip для non-vinformer |
| v0.4.0 | **exist VIN catalog**: search_oem через httpx (4-шаг pipeline) |
| v0.4.1 | exist как **authoritative decoder**, sources_used dedup |
| v0.4.2 | **LLM-категоризатор** вместо fuzzy, engine code (G4FC), multi-word makes, .bak cleanup |
| v0.4.3 | **параллельный диспатч** tool calls (asyncio.gather) |
| v0.4.4 | **OCR через NIM vision**, file input в UI |
| v0.4.5 | **SSE async UI**, real-time progress log |
| v0.4.6 | **anti-hallucination guard** (KIT-XXX patterns), collapsed progress |
| v0.4.7 | **universal coverage** — WMI ~150 prefixes, rule 16a known engines |
| v0.4.8 | **emex VIN-fallback** через POST /api/parts/vin (secondary к exist, парсит brand/name/date/engineno/options), **PWA manifest+install prompt**, **«Гараж» (recent VINs localStorage 5 max, chips fill VIN)**, htmx-script cleanup |
| v0.4.9 | **vinformer dropped from decode chain**. Captcha-gated (Google reCAPTCHA v2), эксперимент с NIM vision (Llama 3.2 90B + cosmos-nemotron-34b) показал 0/10 success rate — точность vision-моделей недостаточна для grid-классификации. Latency win: -200-500ms на каждом fallback-VIN. Decode chain теперь: exist → emex → LLM-decode. |
| v0.4.10 | **Russian-assembly VIN coverage**. Багфикс: `_apply_vin_overrides(None, vin)` теперь строит dict из VIN-структуры вместо ранней возврата → WMI+pos10 year применяются даже когда exist+emex+LLM все пусты. Новая `_RUS_PROJECTS` таблица + `_rus_project(vin)` функция: 40+ записей `(WMI, VDS-prefix)→(make, model)` для Kaluga VAG (XW8: VW Polo/Tiguan/Jetta/Golf, Skoda Rapid/Octavia/Fabia/Yeti), AvtoVAZ (XTA: Vesta/Granta/Niva/Largus/Xray), Renault Avtoframos (X7L: Logan/Sandero/Duster/Kaptur/Arkana), BMW Avtotor (X4X: X1/X3/X4/X5/3/5 Series), Hyundai/Kia СПб (Z94: Solaris/Creta/Rio), GAZ (XTH: GAZelle/Next/Sobol/Volga), UAZ (XTT: Patriot/Hunter/Pickup). Z94 добавлен в WMI-таблицу (был пропущен). Rule 16b в систпроме: типовые engine/transmission для русской массовки. Триггер: real XW8ZZZ61ZKG045729 в проде декодился как "Peugeot 2011" (LLM-мисс при пустых exist+emex). **Baseline 4.3% → after 100% FULL match** на тестовом сете (23 VINs, 2 real + 21 synthetic, 6 заводов). Caveat: 100% это unit-уровень (синтетика); real-world cap ниже если попадётся непокрытый VDS-prefix. |
| v0.4.11 | **OEM accuracy hardening**. Корневой баг найден через baseline-замер (5 cross-market VIN-ов, emex_verified=33% от заполненных primary_oem): **backfill из alternatives** в boundary бездумно копировал aftermarket part_number в `primary_oem` (Mann `W 719/45`, `Filter MO-429`, Reinz `A326/2 STD`). Это задумывалось для химии (Liqui Moly без vehicle-OEM), но извращало OEM-поле для обычных запчастей. Плюс Rule 2 явно разрешал LLM заполнять `primary_oem` из памяти → недетерминизм и галлюцинации. Фикс: (a) **smart backfill** — копировать в primary_oem только если alt.`quality_tier="oem"` ИЛИ part_number матчит OEM-regex марки, (b) **`_OEM_FORMATS_MAKE`** regex-таблица для 25 марок (VAG, Kia/Hyundai, Toyota/Lexus, Nissan, Honda/Acura, Mazda, Subaru, Mitsubishi, Suzuki, BMW/MINI, Mercedes-Benz, Ford, Lada/GAZ/UAZ, Renault/Peugeot/Citroen) + `_looks_like_oem(make, oem)` в boundary blank-ит несоответствующие primary_oem, (c) **temperature 0.2→0.0** в основном tool-loop (str 1400) для детерминизма, (d) **`_RUS_ENGINE_HINTS`** — для XW8 Polo (CFNA до 2015 / CWVA после), Niva (21214), Yeti (CAXA) → engine подставляется в vehicle.engine до tool-loop, чтобы каталог отдавал OEM под конкретный мотор, (e) **Rule 0 + Rule 2** переписаны: жёсткий формат primary_oem с примерами по маркам (VAG 11-символьный, Kia `26300-XXXXX`, BMW `\d{8,12}`, Mercedes `A\d{3}\d{3}\d{2}\d{2}`, Ford `XXXX-NNNN-XX`), catalog-first, никаких OEM из памяти при сомнении. **Smoke до/после на Kia Cerato KNAFW411BD5677381**: `primary_oem` `'Filter MO-429'` (Mann aftermarket в OEM-поле, emex не знает) → `'26300-35503'` (Kia OEM, emex знает 48 записей). XW8 калужский Polo: catalog не индексирует калужские VIN → честно пустой primary_oem (вместо угадывания). 206 тестов зелёные. Files: `app/services/orchestrator.py` (~80 строк), `app/main.py` VERSION. Harness/data: `scripts/measure_oem_min.py`, `tests/data/cross_market_vins.json`, baseline log `/tmp/oem_baseline_v0410_min.log` на mini. Caveat: full re-measure не прошёл (Cerebras gateway 180s timeouts при paced 30s), нужен paced ≥120s или отдельный провайдер для интеграционного теста — но точечные smoke через прямой `curl` показывают баг закрыт. |

## Тестирование

**206 unit-тестов** (v0.4.10: +22 параметризованных rus_decode + 3 rus_project + 1 truly_empty), все зелёные. Запуск: `cd /Users/cebers_mini/Services/avtero && venv/bin/python3 -m pytest tests/ -q --ignore=tests/test_diag_probe.py`.

Ключевые тестовые наборы:
- `test_universal_brand_coverage.py` — 50 WMI prefix → brand параметризованных тестов
- `test_exist_vin_pipeline.py` — exist.resolve_vin, multi-word makes, fuzzy gate
- `test_emex_resolve_vin.py` (v0.4.8) — pin парсера на реальный Kia Cerato payload + edge cases (engineno→code, year, brand-normalize, options, empty list, http error, negative cache)
- `test_rus_projects.py` (v0.4.10) — 22 параметризованных rus_decode + 3 unit-теста на `_rus_project` (XW8 VW/Skoda различение, Z94 Hyundai/Kia, None для непокрытых) + 1 truly_empty fallback
- `test_hallucination_guard.py` — KIT-/FAKE-/PLACEHOLDER patterns
- `test_ocr.py` — NIM vision wiring (mocked httpx)
- `test_vin_year_decoder.py`, `test_wmi_oem_guardrail.py`, `test_oem_brand_mismatch_at_boundary.py` — boundary guards
- `test_version_consistency.py` — VERSION format `0.\d+.\d+`, нет stray-литералов

**Playwright e2e**: запускается отдельным скриптом через python heredoc от venv на mac mini, делает скриншоты в `/tmp/pk_*.png`. Используется для UI-аудита, не в CI.

## Известные ограничения / осознанные skip'ы

- **vinformer reCAPTCHA v2** — выписан в v0.4.9 после эмпирического эксперимента. Попытка скопировать решатель из zemlya-bot (puzzle-slider для Avito + image-OCR для Rosreestr) и адаптировать через NIM vision (Llama 3.2 90B + cosmos-nemotron-34b) дала 0/10 success rate. Бесплатные vision-модели не дают 95%+ precision на grid-classification. Для re-enable: 2captcha API (~$3/1000 решений), 30 мин кода. Recon-артефакты на mini в `/tmp/vinformer_recon/`.
- **Auth / magic-link** — skip. LAN-only single-user, cookie+IP trial достаточно. Включится за 2-3ч если решится publish.
- **Engine code prefetch parallel** — evaluated в v0.4.8, marginal win (+250ms once per VIN, cache 24h), refactor risk выше выгоды → skip.

## Текущий тестовый кейс

VIN: `KNAFW411BD5677381` (Kia Cerato 2012 1.6 G4FC) — основной smoke. Работает все рейсы (exist resolve, exist parts catalog, OCR fallback, SSE progress, anti-hallucination).

VIN: `XTAGFL110JY123456` (Lada Vesta 2018) — fallback-путь тестируется. LLM-decode даёт правильный AvtoVAZ make/model/engine, OEM через LLM knowledge.

## Операционка

Бамп версии: `sed` в `app/main.py::VERSION` + `templates/index.html` (Jinja default fallback), restart launchd.

Чистка кеша: `redis-cli -n 2 KEYS 'avtero:parts:*' | xargs redis-cli -n 2 DEL` + `rm data/cache/avtero_parts_*.json` (двойной кеш: Redis DB 2 + filesystem fallback).

Логи: `logs/stderr.log` (uvicorn), `logs/avtero.out.log`. `.env` рядом с проектом, есть `.env.bak-*` варианты.

## Что НЕ блокирует но nice-to-have

- Performance: 60-150с на запрос — LLM-bound, мало что сделать без агрессивного caching
- Multi-language (только RU)
- emex VIN response shape подтверждён только на Kia VIN — для VAG/BMW/Mercedes/etc реальные ответы не получены, парсер defensive но может потерять nuance (например, BMW `N47D20A` truncated до `N47D` — see _engine_code 4-char heuristic)
