Под каждым блоком есть поле для комментария — напишите, что непонятно, с чем не согласны или что стоит поменять. Комментарии сохраняются в этом браузере автоматически. Когда закончите — нажмите «Скачать комментарии», сохранится файл с вашими правками — поделитесь им с командой удобным способом.
Техническая документация: как считаются баллы, рекомендованная цена, отсев и рекомендации. Единственный источник истины — lib/scoring.ts, этот файл описывает, что там реализовано, и почему именно так. Если код и документ разойдутся — прав код, документ нужно обновить.
Для пользовательского описания (что вводится, что происходит, что на выходе) — см. USER_GUIDE.md. Для инфраструктуры автоматического CJM (что теперь делает воркер сам, без чата) — DEPLOYMENT_PLAN.md. Для процесса, которым я (Клод) добавляю/перепроверяю машины вручную в чате (Avito, разбор конкретной карточки по запросу) — WORKFLOW.md.
computeScore) — определяет, какие машины вообще попадают в топ и в каком порядке. Считается для каждой машины по отдельности.triageStatus, isReadyForPriority) — определяет, допускается ли машина до сравнения вообще (объективный отсев + гейт полноты данных). Это фильтр, не часть суммы баллов.practicalScore) — второй, отдельный вопрос, который решается только внутри уже отобранного «Мой топ» (размер пула динамический, 3/5/7 (максимум) — см. topPicksSize): какая из этих машин лучше по чисто практическим для покупки параметрам (цена/пробег/ДТП/владельцы), с учётом гарантированных вложений после покупки.Ничего не хранится предвычисленным — все баллы считаются на лету из текущих полей при каждой отрисовке, поэтому изменение любого поля мгновенно пересчитывает рейтинг без отдельной кнопки «пересчитать».
computeScorecomputeScore = baseScore + aiScore + importBonus + auctionPenalty + recallPenalty + maintenanceRisk + inspectionScore
Ответ на комментарий пользователя (2026-07-27) — «нужно окно выбора, какие критерии важны каждому пользователю»: реализовано в два захода. У каждого профиля поиска есть enabledCriteria (SearchProfileModal.tsx, чекбоксы на все ALL_SCORE_COMPONENTS) — можно выключить ЛЮБОЕ слагаемое рейтинга целиком (год/пробег/владельцев/цену/любой из категориальных критериев/балл ИИ/риски/риск ТО/чек-лист осмотра) для конкретного профиля, не трогая остальные. Вкл/выкл — это только половина запроса; 2026-07-28 добавлены полноценные ВЕСА (criteriaWeights, «пробег важнее ДТП в 2 раза») — см. раздел 1.1а ниже.
criteriaWeightsКаждый ВКЛЮЧЁННЫЙ критерий (ScoreComponentKey, тот же список, что и у enabledCriteria) можно дополнительно взвесить множителем ×0.5 / ×1 / ×1.5 / ×2 (CRITERIA_WEIGHT_STEPS) — не просто «учитывать/не учитывать», а «насколько важен». Формула не меняется структурно, каждое слагаемое домножается на свой вес ДО суммирования:
baseScore = yearScore×w(year) + mileageScore×w(mileage) + ownersScore×w(owners) + priceScore×w(price) + Σ(критерий×w(критерий))
computeScore = baseScore + aiScore×w(aiScore) + importBonus×w(importBonus) + (auctionPenalty+recallPenalty)×w(risks) + maintenanceRisk×w(maintenanceRisk) + inspectionScore×w(inspection)
Дискретные шаги, а не свободный ползунок — сознательно: узкий выбор проще сравнивать между профилями и не создаёт иллюзию точности, которой веса на глаз не обладают (componentWeight() в lib/scoring.ts). Отсутствующий в criteriaWeights ключ = ×1 (DEFAULT_CRITERIA_WEIGHT) — существующие профили без явных весов не меняют свой рейтинг молча, ровно тот же принцип, что уже был у enabledCriteria (пусто = «включено всё» / «вес ×1 у всего»). Хранится в search_profiles.criteria_weights (JSONB, {ключ: вес}, отсутствующие ключи не пишутся — компактнее, чем хранить ×1 явно для всех 19 компонентов).
Прозрачность. Разбивка рейтинга в карточке машины (CarModal.tsx, «Показать приоритезацию») показывает уже ВЗВЕШЕННОЕ значение каждого критерия (не сырое), с пометкой (вес ×N), если он отличается от ×1 — иначе цифры в попапе разошлись бы с блоком «По критериям» выше при любом весе ≠ ×1, та же проблема прозрачности, что уже была решена для «Корректировок» (см. 2026-07-28 ниже, п.4 жалобы пользователя).
Про идею со шкалой Фибоначчи (8 vs 13 нагляднее, чем 4 vs 5): не подходит напрямую большинству критериев — это не последовательная шкала 1..N, а категориальные варианты с разным количеством опций (2-4 варианта на критерий, не 5). Единственное реальное совпадение — body/interior/style (0-5, оценка человеком на глаз). Сознательно НЕ меняю их прямо сейчас: 196 машин в базе уже оценены по текущей шкале, смена весов молча переставила бы местами уже существующие рейтинги без реального улучшения качества — если решите, что перцептивная разница между соседними баллами (3 и 4) на практике мешает уверенно ставить оценку, это стоит сделать отдельным заходом с явным пересчётом/миграцией, а не тихой заменой констант.
baseScore — сумма по критериямbaseScore = yearScore + mileageScore + ownersScore + priceScore + Σ(категориальные критерии)
Год выпуска (yearScore) — бакеты по году:
| Год | Баллы |
|---|---|
| 2021+ | 5 |
| 2020 | 3 |
| 2018–2019 | 2 |
| до 2018 | 1 |
Пробег (mileageScore):
| Пробег | Баллы |
|---|---|
| до 60 тыс. км | 5 |
| до 80 тыс. км | 4 |
| до 100 тыс. км | 3 |
| 100+ тыс. км | 1 |
Владельцы (ownersScore) — считается не от сырого числа из объявления, а от эффективного числа владельцев (effectiveOwners): при параллельном импорте (importType === "parallel") к сырому числу автоматически прибавляется +1 — обычно есть незадекларированный зарубежный владелец до ввоза в РФ, и это последовательно применяется во всех расчётах (рейтинг, отсев, «идеальная история», рекомендованная цена).
| Владельцев (эффективно) | Баллы |
|---|---|
| 1 | 3 |
| 2 | 2 |
| 3+ | 1 |
Цена (priceScore) — НЕ бакеты по абсолютным рублям, а % отклонения фактической цены (priceActual) от независимой рекомендованной цены (recommendedPrice, методика — раздел 4):
| Отклонение от рынка | Баллы |
|---|---|
| дешевле рынка на 10%+ | 8 |
| −10%…0% | 4 |
| 0%…+10% | 3 |
| +10%…+20% | 2 |
| дороже рынка на 20%+ | 1 |
Категориальные критерии — готовый балл хранится напрямую (не пересчитывается формулой), суммируются все сразу:
| Критерий | Значения и баллы |
|---|---|
ДТП (dpt) |
Нет = 5, Лайт = 3 |
Окрасы (paint) |
Нет = 3, Да = 0 |
Двигатель (engine) |
Дизель 3 л = 5, Дизель 2 л = 3, Остальное = 2 |
Пневмоподвеска (pneuma) |
Да = 3, Нет = 0 |
Комплектация (trim) |
Хорошая = 3, База = 1 |
Состояние кузова (body) |
0–5 (ручная оценка) |
Состояние салона (interior) |
0–5 (ручная оценка) |
Стиль (style) |
0–5 (ручная оценка) |
Класс (klass) |
Кроссовер = 3, Компакт = 2, Универсал = 1 |
Срок владения текущим (ownerTenure) |
<1 года = −2, 1–2 года = −1, >2 лет = +1 |
Технологичность (techRelevance) |
Актуально (CarPlay/Android Auto, ADAS, гибрид/электро) = 3, Частично = 1, Устарело = 0 |
body/interior/style — единственные три по-настоящему субъективные, ручные оценки во всей системе (шкала 0–5 без формулы); вместе с pneuma/trim это 5 полей, которые нельзя определить по тексту объявления, поэтому их всегда заполняет человек (см. MANUAL_FIELDS).
techRelevance (S15-P1, добавлено 2026-07-29) — опциональный, не как остальные. Для сегмента «Машины-гаджеты» (мультимедиа/ADAS/гибрид-электро важны больше, чем для типичного покупателя премиального SUV) — сознательно НЕ входит по умолчанию ни в MANUAL_FIELDS, ни в COMPLETENESS_FIELDS. Обе группы имеют побочный эффект при пустом enabledCriteria профиля (архитектурная особенность isComponentEnabled — пустой список значит «включено всё», в т.ч. новые критерии): попадание в MANUAL_FIELDS сделало бы критерий тихим блокирующим гейтом для перехода на «Приоритезацию» у ЛЮБОГО существующего профиля, попадание в COMPLETENESS_FIELDS тихо подняло бы порог 80%-полноты для всех, кто про этот критерий не спрашивал. Включается вручную — чекбокс критерия в форме поиска, как и любой другой опциональный компонент рейтинга (раздел 1).
aiScoreСвободное поле — качественное суждение сверх формульных критериев (нюансы, которые не ловятся ни одним отдельным полем). Хранится как есть, без собственной формулы. Для Avito/Auto.ru/Drom проставляется автоматически фоновым воркером (ClaudeAiProvider, тот же системный промпт и та же методология, что описана здесь — см. DEPLOYMENT_PLAN.md), для остальных площадок и при ручной перепроверке — по-прежнему я (Клод) в чате. Пересмотрено 2026-07-23:
AI_SCORE_RANGE = −2…+2 (lib/scoring.ts, проверяется в validateCarPatch). Старый разброс (8 пунктов) был сопоставим с несколькими структурными критериями сразу и мог их «перевешивать» — узкий диапазон гарантирует, что субъективная поправка остаётся именно поправкой, а не отдельным полноценным критерием.aiScoreReason — обязательное текстовое обоснование числа, отдельное поле рядом с aiScore. Показывается в карточке машины сразу под баллом ИИ.aiScore явно проверяю, не пересказывает ли причина то, что уже даёт dpt/dtpCost/owners/priceScore/auctionHistory — это была системная проблема при первом проходе по базе (2026-07-23): почти все машины с aiScore вне нового диапазона получили экстремальное число именно за факты, уже посчитанные в баллах по критериям (например, дорогое ДТП давало и штраф в dtpCost/триаже, и отдельно −3…−4 в aiScore за «то же самое ДТП» другими словами). aiScore должен ловить только то, что не попадает ни в один структурный критерий: репутация конкретного продавца/дилера, поведенческие красные флаги (только наличные, отказ показать машину), независимые сторонние проверки («Авито Селект», рекомендация Автотеки), признаки сгенерированного текста и т.п.aiScore систематически строже/мягче, чем показывает более достоверный источник, и корректирую подход, если вижу перекос. Формализовано 2026-07-28 (см. раздел 10) — теперь каждый разбор платного отчёта (analyze-report) автоматически логирует пару «aiScore до отчёта / aiScore после отчёта» в data/calibration-log.jsonl, и scripts/calibration-report.ts считает систематическое смещение числом, а не по паре запомнившихся случаев.importBonus+2, если importType === "official". Официальный ввоз против серого/параллельного — отдельная фиксированная надбавка, не смешивается со свободным aiScore, чтобы причина бонуса была прозрачна.
auctionPenaltyШтраф за участие в аукционе битых авто (по сути «тотальный ущерб в прошлом», жёстче любого штрафа за обычное ДТП, хранится отдельно от dpt) — с 2026-07-23 градация по тяжести повреждений вместо единственного жёсткого уровня:
auctionHistory |
Штраф |
|---|---|
light (лёгкие повреждения) |
−2 |
medium (средние повреждения) |
−5 |
critical (тяжёлые повреждения) |
−10 |
clean / не заполнено |
0 |
Раньше был один уровень (flagged = −10) на любое попадание в аукцион — но повреждения там бывают очень разными, от машины, которую страховая признала нерентабельной в ремонте после лёгкого ДТП, до реального тотала. Автоматического анализа фото для объективной оценки тяжести пока нет (см. раздел «Анализ фото» в WORKFLOW.md) — тяжесть проставляется вручную при разборе отчёта/объявления. Только critical продолжает участвовать в автоотсеве (triageStatus) — та же планка серьёзности, что была у прежнего единственного уровня; light/medium только снижают рейтинг, не блокируют машину автоматически.
recallPenalty−1 балл за каждую непогашенную отзывную кампанию (openRecalls), но не больше −3 суммарно — чтобы одна машина с длинным списком старых отзывов не улетела в минус сильнее, чем реальное ДТП.
maintenanceRisk — риск техобслуживания (он же «стоимость технического содержания» — синонимы, используется единый термин «риск техобслуживания»)Критерий, который зависит от модели (болячки определяются платформой/поколением мотора), но применимость и вес каждого конкретного пункта проверяются для этого экземпляра и профиля владения этого покупателя — а не применяются одинаково ко всем машинам модели, как было раньше (см. «Пересмотр методологии» ниже).
maintenanceRisk = max(baseRisk_модели + Σ(extraRisk по актуальным пунктам), −5)
Данные по каждой модели (MODEL_MAINTENANCE в lib/scoring.ts) собраны целевым исследованием: форумы (включая Drive2.ru), сервисные блоги, официальные отзывные кампании производителей — отдельно по каждой модели из MODEL_OPTIONS там же. Формат профиля модели:
baseRisk — базовый штраф −1…−5, начисляется всегда, независимо от профиля покупателя (общая репутация платформы/поколения — не привязана к конкретной болячке или пробегу; даже если ни один частный пункт лично вас не касается, у модели в целом может быть больше мелких проблем, чем у более надёжной);issues — конкретные болячки, каждая с полями:text — описание человеческим языком;atMileage — типичный пробег появления, км, либо null, если проблема не привязана к пробегу (обычно отзывная кампания или дефект детали, который не «выращивается» со временем — актуальна при любом пробеге);extraRisk — вклад в штраф, если пункт актуален (0 — чисто информационный пункт, «на что посмотреть», сам баллов не снимает);appliesIf — необязательное условие применимости к варианту машины (например, к типу ввоза).Два независимых фильтра применимости (issueRelevance в lib/scoring.ts), пункт даёт штраф, только если проходит оба:
atMileage начисляется, только если прогнозный пробег машины к вероятному моменту продажи — car.mileage + buyerProfile.annualMileageKm × buyerProfile.ownershipYears — достигает порога. Иначе проблема, скорее всего, проявится уже у следующего владельца, не у текущего покупателя. buyerProfile — годовой пробег + срок владения, поле профиля поиска (см. USER_GUIDE.md, раздел «Профили поиска»), задаётся в интерфейсе кнопкой «Редактировать» рядом с текущим профилем, хранится в БД (таблица search_profiles), по умолчанию 15 000 км/год и 5 лет. До 2026-07-23 было единой личной настройкой на весь браузер (localStorage) — теперь у каждого профиля поиска свой, т.к. разные поиски могут вестись под разное использование машины. Пункты без atMileage (null) этому фильтру не подчиняются, применяются всегда, если прошли фильтр 2.appliesIf). Не каждая болячка модели касается каждого экземпляра. Пример: система AdBlue/SCR на дизельных Audi/VW не устанавливалась на официально ввезённые в РФ (дилерские) машины — по расчётной норме РФ для дизелей она не требовалась, риск реален только для параллельно ввезённых (европейских) экземпляров (appliesIf: importType !== "official"). При неизвестном importType (null) сравнение «не равно официальному» истинно, то есть по умолчанию риск считается актуальным, пока не подтверждён официальный ввоз — так безопаснее, чем тихо занижать риск по неизвестной машине.Интерфейс показывает не просто список болячек модели, а честную пометку по каждому пункту: «актуально для вас» / «появится позже вашего срока владения» / «не относится к этому экземпляру» (блок «Чего ждать при обслуживании» в карточке машины).
Пример (Porsche Cayenne, baseRisk: -3, дефолтный профиль 15 000 км/год × 5 лет): у машины с пробегом 40 тыс. км прогнозный пробег к продаже — 40 000 + 15 000 × 5 = 115 000 км. Порог 80 тыс. км (пневмоподвеска) пройден → −1. Порог 120 тыс. км (актуатор турбины) НЕ пройден (115 000 < 120 000) → 0, помечен «появится позже вашего срока владения». Итог: -3 -1 = -4, а не -5, как было бы при старой логике (штраф за оба порога машине с любым текущим пробегом ≥120 тыс. км, без учёта того, доедет ли до этого порога именно этот покупатель).
Это единственный критерий, который не заполняется вручную и не запрашивается у ИИ per-объявление — он выводится из model + mileage + importType машины и настроенного пользователем buyerProfile, поэтому не может «забыться» при анализе конкретного объявления, но при этом больше не применяется вслепую одинаково ко всем экземплярам одной модели.
Пересмотр методологии (2026-07-22). Раньше штраф за каждый порог пробега начислялся любой машине модели, достигшей этого порога сейчас, без учёта того, что: (а) у разных покупателей разный годовой пробег и горизонт владения — машина с пробегом 80 тыс. км, которую продадут через 3 года при 10-12 тыс. км/год пробега, не доедет до порога 140 тыс. км и проблема её не коснётся; (б) не каждый пункт физически применим к конкретному экземпляру — AdBlue/SCR пример выше. Заодно при пересмотре нашлась внутренняя нестыковка в профиле Audi A6: штраф был привязан к порогу 35 тыс. км, хотя собственный текст пункта говорил «риск слабо привязан к пробегу» — исправлено: пункт больше не гейтится пробегом (atMileage: null), применяется всегда (с учётом фильтра по типу ввоза).
inspectionScore — чек-лист осмотраЕдинственный критерий, требующий физического присутствия — появляется в карточке машины только когда она реально попала в «Мой топ» (см. computeTop5/topPicksSize — размер списка растёт вместе с числом машин на «Приоритезации», 3/5/7 максимум (пороги пересмотрены 2026-07-29 — раньше рос до 10, теперь потолок ниже: даже большой поиск должен оставаться коротким списком), не фиксированные 5; до этого момента осматривать нечего — машина ещё не прошла даже базовый отбор). 8 пунктов (INSPECTION_CHECKLIST_ITEMS), каждый — match / mismatch / not_checked + заметка при несовпадении, свой вес штрафа за mismatch:
| Вес | Пункты |
|---|---|
| −3 | VIN на кузове/агрегатах |
| −2 | Комплект документов, кузов/следы скрытого ремонта, правдоподобность пробега |
| −1 | Лакокрасочное покрытие, история обслуживания, состояние салона, технические системы |
Асимметрично специально: осмотр в первую очередь ловит проблемы (несовпадение VIN — риск скрутки/перебивки, самое серьёзное; несовпадение документов и следы скрытого ремонта — следующий эшелон), не раздаёт баллы за «и так ожидаемое».
Бонус за подтверждённый осмотр — пропорциональный, не всё-или-ничего (изменено 2026-07-28). Раньше +1 начислялся только если проверены ВСЕ 8 пунктов и по всем match — осмотр на 80% (например, не добрались до VIN на одном из агрегатов) не давал вообще ничего, хотя реально снижает неопределённость. Теперь: + matchCount / 8 — каждый подтверждённый (match) пункт даёт +0.125, независимо от того, сколько пунктов ещё не проверено. computeScore() — единственное место, где применяется Math.round(), поэтому дробный бонус чек-листа никогда не просачивается в отображаемый рейтинг как нецелое число. Диапазон: от −12 (все 8 мисматчей) до +1 (все 8 match, включая случай отсутствия мисматчей).
Чек-лист меняет только числовой балл (inspectionScore в computeScore). Текстовые выводы (aiComment/aiStrengths/aiWeaknesses) чек-лист не трогает — после того как пользователь его заполнил и сохранил, я отдельно перечитываю результат в чате и обновляю их вручную (см. WORKFLOW.md, «Чек-лист осмотра»).
triageStatusСтрого 4 объективных критерия (без добавления новых на усмотрение анализа конкретного объявления). Каждый критерий можно включить/выключить и порог у него переопределить на уровне профиля поиска (TriageSettings, меню профиля → «Критерии быстрого отсева этого профиля») — добавлено 2026-07-23 после того, как обнаружилось, что глобальные пороги, рассчитанные на сегмент «премиальный SUV 2016-2021», отсеивали абсолютно все машины в профиле «коллекционные седаны 1990-х» (у них естественно больше пробег/владельцев для возраста). Пусто/не задано на уровне профиля — используется дефолт ниже, старое поведение не меняется молча:
| # | Критерий | Порог по умолчанию |
|---|---|---|
| 1 | Дорогой ремонт по ДТП | effectiveDtpCost > 100 000 ₽ (TRIAGE_MAX_DTP_COST) |
| 2 | Сомнительная история | участие в аукционе битых, либо в aiWeaknesses найдены слова «дубликат ПТС» / «скрутка пробега» / «нестыковки» — порога нет, только вкл/выкл |
| 3 | Владельцев больше нормы | effectiveOwners > 4 (TRIAGE_MAX_OWNERS) |
| 4 | Пробег больше нормы | mileage > 110 000 км (TRIAGE_MAX_MILEAGE) |
Если сработал хотя бы один из включённых — reject, машина не проезжает дальше (не переопределяется вручную, критерии объективные).
Если ни один не сработал, но по включённым проверкам данных не хватает, чтобы их выполнить (пусты mileage/owners/dpt — каждое поле проверяется только если соответствующий критерий включён в профиле), либо ДТП стоит в категории «Лайт» без известной суммы ремонта и без явного текстового подтверждения европротокола (isConfirmedEuroprotocol — ищет слово «европротокол» в тексте анализа, актуально только если включена проверка «Дорогой ремонт по ДТП») — review: требуется ручное подтверждение (кнопки Да/Нет в интерфейсе, сохраняется в triageOverride, можно изменить позже).
Иначе — ok.
Важный нюанс методологии («дорогой ДТП»): порог 100 000 ₽ проверяется только по подтверждённой сумме ремонта (effectiveDtpCost — из явного поля dtpCost либо вытащенной из текста суммы рядом со словами «Audatex», «смета», «калькуляция», см. textMinedDtpCost). Категория «Лайт» сама по себе не считается основанием для автоматического отказа — без цифры это догадка, а не факт, поэтому уходит в review, а не в reject. Это осознанное решение после разбора реального случая (машина с ДТП-европротоколом изначально ошибочно помечалась как «серьёзное ДТП» только по категории, без проверки суммы).
Ответ на комментарий пользователя — «пока доступны без платного отчёта только 2 параметра (пробег/владельцы), для релевантности нужен третий, какой?»: в реальности критериев 4, просто suspiciousHistory почти никогда не срабатывает без отчёта (сигналы вроде «скрутка пробега» обычно есть только в тексте платного отчёта, не в объявлении) — так что наблюдение по сути верное.
Проверено и закрыто (2026-07-28), кандидат отклонён. Рассматривались гибдд.рф/check/auto (ДТП/ограничения/розыск) и реестр залогов (reestr-zalogov.ru, ФНП). Факт-чек:
reestr-zalogov.ru — подтверждено технически: на форме поиска стоит invisible Google reCAPTCHA Enterprise, обычный HTTP-запрос без решения капчи данных не отдаёт.гибдд.рф/check/auto — прямая проверка капчи не завершилась однозначно, но косвенное доказательство весомее: под оба источника существует целый рынок платных «неофициальных API»-реселлеров (api-cloud.ru прямо называет их «неофициальными API», плюс apipoint.ru, api-parser.ru, вся индустрия avtocod/avtonomer) — свободный прямой доступ явно не тривиален, иначе перепродавать было бы нечего.Car.vin распознаётся только из имени файла отчёта (lib/vin.ts), автопарсер объявлений (Auto.ru/Drom) не вытаскивает VIN из текста. То есть звонить в эти сервисы на этапе бесплатного триажа физически не по чему для подавляющего большинства карточек.Решение: не подключать. Автоматизация была бы либо (а) невозможна без решения капчи, либо (б) платная через реселлера (у одного из проверенных — api-cloud.ru — минимальный платёж 700 ₽/мес при <2000 запросов/мес, первый счёт от 3000 ₽) — пользователь платный сервис сейчас подключать не хочет. Раздел закрыт без реализации; если объём базы/бюджет изменятся, вернуться можно с тех же двух источников.
isReadyForPriorityМашина переходит с «Выгрузки» на «Приоритезацию», только когда выполнены все три условия:
hasAllManualFields) — pneuma, trim, body, interior, style (то, что не определить по тексту объявления).hasEnoughDataForScore, список — COMPLETENESS_FIELDS). Ниже этого порога сравнение с другими машинами вводит в заблуждение — слишком много полей посчитаны как отсутствующие (по умолчанию 0/нейтрально), рейтинг не отражает реальное качество машины.triageStatus !== "reject", а если review — только при triageOverride === "approved".Это не отдельный статус в БД, а живой пересчёт из текущих значений полей при каждой отрисовке — карточка сама «переезжает» между вкладками по мере дозаполнения, без ручного переключения.
peerMarketEstimateРаньше recommendedPrice в основном заимствовалась из «рекомендуемой цены», которую сами агрегаторы (Автотека, «Авто.ру История») публикуют в отчёте — но это чужая закрытая модель, которую нельзя проверить. Текущая методология — гибрид, посчитанный из наблюдаемого рынка.
Найденный и исправленный баг (2026-07-28, при разборе комментариев пользователя к этому разделу): до этой даты peerMarketEstimate() нигде не вызывалась ни из одного реального пути записи (ни из API, ни из автоматических задач) с момента её написания — recommendedPrice для КАЖДОЙ карточки всегда шёл из догадки ИИ, даже когда в базе уже было 5-10 подходящих пиров для точного расчёта. Исправлено: lib/jobs/verdict-patch.ts → applyPeerPriceEstimate() теперь вызывается из всех трёх автоматических задач (analyze-link/analyze-report/verify-listing) после вердикта ИИ.
Пересмотр алгоритма (тот же день, «Независимый взгляд на методологию»). Раньше было бинарно: ≥3 пиров той же модели в базе → цена ПОЛНОСТЬЮ перезаписывается медианой+поправкой («Шаг 1»); <3 → null, приложение целиком переключается на догадку ИИ («Шаг 2»). Резкий обрыв ровно на границе N=3 не был обоснован статистически — при N=2 использовалось 0% своих данных, при N=3 — 100%. Заменено на плавное credibility-взвешивание трёх источников снизу вверх:
ClaudeAiProvider по общим знаниям о рынке, либо предыдущее значение recommendedPrice) — передаётся в peerMarketEstimate() явным третьим аргументом, не зашита в функции константой.klass — Кроссовер/Компакт/Универсал) — частичный пулинг между моделями: раньше редкая модель с 1-2 карточками в базе не занимала вообще ничего у похожих моделей того же сегмента, теперь широкий пул того же класса служит промежуточным ориентиром.Каждый уровень считается той же формулой медиана+поправка, что и раньше:
оценка_уровня = медиана_цены_пула
− (пробег_машины − медиана_пробега_пула) / 1000 × 2000 ₽
− (владельцы_эфф_машины − медиана_владельцев_пула) × 20 000 ₽
− max(0, ДТП_стоимость_машины − медиана_ДТП_пула) × 0.5
и смешивается с уже накопленным итогом по credibility-весу w = N / (N + k) (классическая формула из актуарной практики — чем больше наблюдений на уровне, тем сильнее он «перетягивает» оценку на себя, без единственного порога):
k = 3 для пиров модели (та же величина, что был старый порог — на N=3 пиров модели вес медианы теперь ровно 50%, не 100%, но интуиция «на 3 уже наполовину доверяем» сохранена, просто без обрыва по обе стороны от неё);k = 8 для пиров класса (пул шире и более гетерогенен, нужно больше наблюдений для того же доверия).Если на каком-то уровне пиров вообще нет — он просто пропускается, ничего не смешивая. Если пиров нет вообще нигде (ни модели, ни класса) — функция возвращает приор как есть (не трогает recommendedPrice/marketComment, applyPeerPriceEstimate() в этом случае патч не применяет — так же, как раньше null при <3 пиров означал «не трогать»). null результат — только когда нет вообще ничего: ни пиров, ни приора.
Коэффициенты (2000 ₽/1000 км пробега, 20 000 ₽/владельца, 0.5 от разницы стоимости ДТП) — по-прежнему эмпирические ориентиры для сегмента 3.5–5 млн ₽, не строгая регрессия (та же весовая логика, что в аргументах для торга, раздел 6).
Уверенность и диапазон вместо голой точки. PeerMarketEstimate теперь возвращает не только price, но и peerCount/klassPeerCount (сколько наблюдений на каждом уровне), confidence (low/medium/high, производная от эффективного веса пиров модели) и priceRangeLow/priceRangeHigh (наблюдаемый диапазон цен пула, из которого взята оценка). marketComment (formatPeerEstimateComment()) явно называет источники и уровень уверенности — раньше текст был один и тот же независимо от того, откуда взялась цена.
Ответ на комментарий пользователя — идеи повысить релевантность оценки (архив, часть решена выше):
klass (см. выше); полноценная многофакторная регрессия по всем признакам сразу по-прежнему отложена — нужно ощутимо больше исторических данных, чтобы не переобучиться на нескольких точках на модель.Ограничение методологии: при тонкой выборке (confidence: "low") результат размечается как менее уверенный прямо в marketComment — это не скрывается от пользователя. Проверить реальный эффект этого пересмотра на уже накопленной базе (сколько карточек и на сколько изменили recommendedPrice/состав «Мой топ») можно через scripts/scoring-snapshot.ts + scripts/scoring-diff.ts — см. раздел 10.
practicalScoreОтдельный, второй вопрос: не «какие 5 машин вообще стоит смотреть» (это по-прежнему решает основной рейтинг, раздел 1), а «какая ИЗ ЭТИХ пяти лучше по чисто практическим для покупки БУ авто параметрам».
practicalScore = 0.35 × ценаSub + 0.20 × пробегSub + 0.30 × ДТПSub + 0.15 × владельцыSub
Веса ДТП/пробег поменяны местами 2026-07-28 (было 0.30/0.20) по комментарию пользователя: «уровень влияния ДТП на практический скоринг недооценён — это один из главных аспектов на стоимость эксплуатации/риски». Согласен — на уже отобранном пуле «Мой топ» (все машины прошли триаж, различия в пробеге между ними обычно некритичны) риск скрытых будущих расходов от ДТП более весом, чем ещё одна поправка на 10-20 тыс км. Честная оговорка: dtpCostForScoring ограничивает сумму сверху порогом 100 000 ₽ (тот же, что у отсева) — машины с действительно дорогим ДТП чаще всего УЖЕ отсеяны триажем раньше, чем попадают в «Мой топ», так что на практике разница в ДТП-подоценке внутри самого пула часто меньше, чем разница в пробеге — новый вес даёт бо́льшую ЧУВСТВИТЕЛЬНОСТЬ к оставшимся различиям, не гарантирует драматический сдвиг рейтинга на каждом пуле.
Каждая под-оценка нормализуется min-max внутри самого пула «Мой топ» (не по всей базе) — иначе на группе похожих по качеству машин разница не видна: худшему в пуле значению присваивается 0, лучшему — 100, нет данных у конкретной машины — нейтральные 50 (не награждаем и не штрафуем неизвестность). Машина побеждает, если у неё выше practicalScore.total — это и есть 🏆 практический победитель на вкладке «Мой топ».
Винзоризация пула перед min-max (2026-07-28, «Независимый взгляд на методологию»). Min-max на пуле ровно из 5 машин максимально чувствителен к одному выбросу — он один задаёт ОБА конца шкалы, и три обычные машины с реально разной, но некрайней разницей получают почти неразличимые баллы на его фоне. Перед вычислением min/max пул отсекается на ±3×MAD (median absolute deviation — устойчивая, в отличие от std.dev., мера разброса) от медианы пула; сама оцениваемая машина, если она и есть выброс, тоже отсекается той же границей, а не выходит за пределы [0, 100]. На пуле без выбросов (обычный случай) эффекта нет — граница ±3×MAD достаточно широка, чтобы не задевать нормальный разброс.
Поправка на гарантированные вложения после покупки. Ценовая под-оценка считается не от сырого % отклонения от рынка, а с учётом того, что в любую из пяти машин так или иначе придётся вложить минимум ~350 000 ₽ (нормальное ТО, зимняя резина, химчистка, полировка и т.п. — неизбежно вне зависимости от выбора):
effectivePriceDeviationPct = ((цена + 350 000) − (рек.цена + 350 000)) / (рек.цена + 350 000) × 100
Эффект: та же абсолютная разница в цене между вариантами даёт меньший % отклонения, если её «растворить» в большей общей сумме — компрессия сильнее для машин с изначально более низкой рекомендованной ценой (350 000 ₽ — большая доля от меньшей базы), что и отражает интуицию «разница в цене значит меньше на фоне одинаковых для всех вложений». Это единственное место в системе, где учитывается пост-покупочная стоимость владения — на основной рейтинг (раздел 1) она не влияет.
ДТП без точной суммы (dtpCostForScoring) — для практического скоринга (не для отсева) категория без цифры оценивается условно, а не приравнивается к нулю: «Нет» → 0, «Лайт» без суммы → 60 000 ₽ (0.6 от порога 100 000 ₽), известная сумма — используется как есть, с потолком в 100 000 ₽ (тот же порог, что у фильтра «дорогое ДТП»).
bargainAdviceВозвращает рекомендованную сумму торга и до 3 самых весомых причин (отсортированы по вкладу в сумму), либо null, если торговать не с чем. Источники суммы:
| Основание | Вклад в сумму |
|---|---|
| Цена выше рекомендованной больше чем на 3% | разница priceActual − recommendedPrice |
| Известная сумма ремонта по ДТП | 0.5 × сумма |
| ДТП «Лайт» без известной суммы | 50 000 ₽ (фикс.) |
| Есть окрашенные элементы | 30 000 ₽ (фикс.) |
| Участие в аукционе битых | по тяжести: лёгкие 40 000 ₽ / средние 90 000 ₽ / тяжёлые 150 000 ₽ (фикс.) |
| Непогашенные отзывные кампании | 15 000 ₽ × количество |
| Пробег выше похожих машин той же модели в базе на 15%+ | 20 000 ₽ (фикс., нужно ≥2 похожих для сравнения, порог — от медианы, см. ниже) |
| Владельцев больше, чем у похожих в среднем +1 | 20 000 ₽ (фикс., нужно ≥2 похожих для сравнения, порог — от медианы, см. ниже) |
| В тексте объявления/анализа есть признаки, что продавец уже снижал цену | без суммы, только довод |
Сумма округляется до 10 000 ₽. Начиная с 2026-07-28 сумма считается только по показанным 3 доводам, не по всем найденным основаниям — раньше при 4+ одновременных факторах итоговая сумма включала вклад и от причин, которых не было среди показанных 3 строк (пользователь видел, например, «торг на 250 000 ₽», а под ним только 3 довода, объясняющие меньшую часть числа). Сами весовые коэффициенты в таблице выше не менял — это те же эмпирические ориентиры для сегмента, что и раньше, конкретных альтернативных цифр предложено не было.
Честный счётчик оставшихся доводов (S28, добавлено 2026-07-29). BargainAdvice.extraCount — сколько найденных оснований для торга НЕ попало в показанные топ-3 (items.length − top.length, никогда не отрицательное). Интерфейс («Мой топ») показывает его отдельной строкой «И ещё N причин (не вошли в сумму торга)», только когда extraCount > 0 — та же логика честности, что и у самого решения 2026-07-28 выше: раз сумма считается не по всем основаниям, пользователь должен явно видеть, что есть ещё причины сверх показанных, а не догадываться по молчаливому обрыву списка на 3 строках.
Медиана вместо среднего для сравнения с похожими (2026-07-28, «Независимый взгляд на методологию»). Пороги «пробег выше похожих на 15%+» и «владельцев больше среднего +1» раньше считались от арифметического среднего пула похожих машин той же модели — на пуле из 2-6 карточек одна кривая запись (опечатка в пробеге, неучтённый выброс) сдвигает среднее сильнее, чем медиану. Заменено на медиану (та же функция median(), что уже используется в peerMarketEstimate, раздел 4) — устойчивее к единичным аномалиям, сам порог (15%/+1) не менялся.
reportPurchasePriorityЖивая (не хранимая) функция, полностью производная от triageStatus:
| Статус триажа | Приоритет отчёта | Логика |
|---|---|---|
reject |
🔴 skip |
Машина и так уходит в отказ по бесплатным данным — отчёт её не спасёт, деньги на него не окупятся |
review |
🟡 high |
Машина в «серой зоне» именно из-за нехватки данных, которые способен закрыть отчёт (например, точная сумма ДТП) — максимальная отдача с потраченных 200–300 ₽ |
ok |
🟢 low |
Машина и так уже проходит по бесплатным данным — отчёт нужен как подстраховка перед реальным осмотром, а не для отсева |
Цвета high/low поменяны местами 2026-07-28 по комментарию пользователя — раньше high (реальная неопределённость, которую отчёт может разрешить) был зелёным, что читалось как «всё хорошо», хотя смысл ровно обратный. Теперь цвет — это статус машины по бесплатным данным (🟢 всё уже ясно, 🟡 есть неопределённость, 🔴 уже отказ), а не «насколько выгодно» тратить деньги на отчёт.
hasIdealHistory) — декоративный бейдж, требует одновременно: owners ≤ 3 (сырое значение — при importType === "official", единственно допустимом здесь значении, эффективные и сырые владельцы всегда совпадают, расхождения с триажем нет), dpt === 5 (ДТП нет), importType === "official", ownerTenure === 1 (текущий владелец >2 лет). Требует, чтобы все 4 поля были заполнены — не показывается по неполным данным.completenessCount/missingFields) — сколько из 18 COMPLETENESS_FIELDS заполнено, показывается как N/18 в таблице; используется и для гейта раздела 3.scoreBreakdown) — раскладывает computeScore на подписанные слагаемые (по каждому критерию отдельно, плюс «Балл ИИ», «Официальный ввоз», «Риски», «Риск техобслуживания»), отсортированные по убыванию — используется в объяснении «почему машина на этом месте» на вкладке «Мой топ».verdictConfidence (S15, добавлено 2026-07-29)Не про числовой рейтинг, а про то, насколько ему вообще стоит доверять — дискретный GRADE-стиль индикатор (3 деления, PRODUCT_DESIGN_RESEARCH.md блок D2), сколько независимых источников данных реально стоит за вердиктом этой карточки:
dataComplete = заполнено ≥80% COMPLETENESS_FIELDS (та же планка, что у гейта раздела 3)
если !dataComplete → "low"
иначе: изученный отчёт (hasStudiedReport) + подтверждённый осмотр (inspectionCompletedAt) — оба → "high"; ровно один → "medium"; ни одного → "low"
Полнота полей — не сама по себе высокий уровень, а необходимое условие: карточка с наполовину пустыми критериями остаётся «low», даже если у неё есть и отчёт, и осмотр — иначе индикатор мог бы вводить в заблуждение (высокая уверенность в вердикте, который на самом деле недосчитан по большинству критериев). Рядом с индикатором в карточке — постоянный текстовый дисклеймер, прямо называющий, что ЕСТЬ у вердикта (отчёт/осмотр), а не только чего в нём нет.
Загруженный, но не изученный отчёт не считается источником — hasStudiedReport (раздел 1.7) требует оба поля (reportFileUrl И reportAnalyzedAt), не только факт наличия файла. Тот же принцип, что уже применяется при гейте «Мой топ» — предотвращает путаницу, которая уже случалась вручную (карточка с загруженным, но непрочитанным отчётом выглядела проанализированной).
hasUnverifiedPremiumTrimClaim/hasPresentationVsConditionGap (S22/S23, добавлено 2026-07-29)Оба — не новые критерии рейтинга (числовой балл не меняют), а текстовые коллауты в карточке для двух сегментов покупателей из PRODUCT_DESIGN_RESEARCH.md (блок A1а):
hasUnverifiedPremiumTrimClaim (сегмент «Статус-премиум») — истинно, когда trim === 3 («Хорошая» комплектация) И отчёт ещё не изучен. Комплектацию в объявлениях завышают чаще всего, а у сервиса нет источника данных для независимой проверки по VIN (это не настоящий детектор подмены) — честная пометка «это слова продавца, не проверено независимо», а не молчаливое доверие полю.hasPresentationVsConditionGap (сегмент «понторезки», самый этически чувствительный по формулировке исследования) — истинно, когда ВСЕ заполненные из body/interior/style ≥4 (визуально презентабельно) И при этом есть ДТП (dpt < 5) либо окрас (paint === 0). Явный коллаут «внешний эффект и реальное состояние здесь расходятся» вместо молчаливой красивой карточки.Оба маркера пропадают сами, как только условие перестаёт быть верным (отчёт изучен / нет визуальных полей выше 4 / история чистая) — не dismissible вручную, потому что это не совет, а факт о текущем состоянии данных.
Отдельный слой поверх методологии выше — не про сами формулы, а про то, кто сейчас их применяет. До 2026-07-27 весь путь «объявление → заполненная карточка → вердикт» проходил через меня в чате. Теперь для Avito/Auto.ru/Drom он полностью автоматический: фоновый воркер сам скрейпит объявление (Firecrawl для Auto.ru/Drom, платный Автопульс для Avito — капча блокирует Firecrawl даже через выделенный скрапер, проверено дважды на реальных ссылках), вызывает ту же модель (Claude, тот же системный промпт, что реализует разделы 1–8 выше) и пишет результат в карточку — без единого ручного шага. Кнопка «Обновить данные» (и суточное расписание, auto-refresh-listings) той же цепочкой перепроверяет уже существующие карточки.
Что осталось ручным и почему:
body/interior/style — по-прежнему требуют физического осмотра машины, автоматизировать нечем.Это раздел специально для комментариев по самой автоматизации — если считаете, что живой поиск цены стоит закрыть платным скрапинг-провайдером, или видите другой приоритет, пишите здесь.
Отдельный слой поверх методологии — не новые формулы, а инструменты для того, чтобы БУДУЩИЕ правки формул проверялись числом на реальных исторических данных, а не только рассуждением в комментарии к коммиту («Независимый взгляд на методологию» — независимый разбор текущей математики системы и предложенный по его итогам план).
scripts/scoring-snapshot.ts + scripts/scoring-diff.ts, логика диффа — чистая функция lib/scoring-diff.ts, покрыта тестами). scoring-snapshot.ts снимает снимок вычисляемых результатов (computeScore/triageStatus/peerMarketEstimate/practicalScore/bargainAdvice, состав «Мой топ» по каждому профилю) по всей текущей базе, посчитанный текущим кодом — ничего не пишет, только SELECT. Снятый до и после правки lib/scoring.ts снимок сравнивается scoring-diff.ts: сколько карточек реально сменили статус триажа, на сколько в среднем/максимум сдвинулась recommendedPrice, у каких профилей поменялся состав «Мой топ». Правки весов/порогов теперь можно (и нужно) проверять так же, как и любой другой код — до коммита. Использование: npx tsx scripts/scoring-snapshot.ts before → правка кода → npx tsx scripts/scoring-snapshot.ts after → npx tsx scripts/scoring-diff.ts backups/scoring-snapshot-before.json backups/scoring-snapshot-after.json.lib/ahp.ts + scripts/ahp-weights.ts) — для СЛЕДУЮЩЕЙ смены весов (practicalScore, возможно в будущем и baseScore) вместо разовой договорённости по одному устному аргументу (как было с dtp/mileage 0.20/0.30→0.30/0.20 в этом же разговоре) — структурированные попарные сравнения критериев («ДТП важнее пробега во сколько раз?») с проверкой согласованности (consistencyRatio — ловит внутренние противоречия сравнений, например A>B>C, но C>A).aiScore (lib/calibration.ts, лог — data/calibration-log.jsonl, отчёт — scripts/calibration-report.ts) — см. раздел 1.2.confidence/диапазон в marketComment делают это явным, не скрывают.k в credibility-весах раздела 4) — эмпирические ориентиры, согласованные в разговоре с пользователем и откалиброванные на здравом смысле, не результат статистической оптимизации на большом датасете. Для будущих смен весов — см. AHP-инструмент (раздел 10), для проверки эффекта смены коэффициентов на реальных данных — бэктест-стенд (тот же раздел).body/interior/style — единственные полностью субъективные поля, заполняются человеком на глаз по фото/осмотру, без формулы или проверки. Уже можно отключить полностью — enabledCriteria профиля поиска (см. раздел 1) выключает эти 3 критерия из рейтинга, если для конкретного профиля они не нужны.maintenanceRisk — знание на уровне модели/поколения, не диагностика конкретного экземпляра; это ожидание, а не гарантия («может понадобиться», а не «понадобится»).aiScore — качественное суждение без формулы, дополняет, а не заменяет объективные критерии; хранится как обычное поле и может быть неточным, если исходные данные (объявление/отчёт) сами неполны или недостоверны. Систематический перекос теперь можно проверить числом — см. раздел 10.Ответ на комментарий пользователя — «придумай алгоритм снятия этих ограничений, кажется все они решаемые»: по пунктам честно, не все одинаково решаемы кодом:
klass (раздел 4) убрали резкий обрыв и дали редким моделям опору на более широкий сегмент, не дожидаясь роста базы. Полноценная многофакторная регрессия по всем признакам сразу — по-прежнему вопрос объёма данных, тут исходная оценка пользователя была верна.body/interior/style субъективны и maintenanceRisk — рыночный срез, не диагностика конкретного экземпляра — согласен с пользователем, это не баги, а осознанная граница системы (первое — вкус пользователя, у формулы не должно быть мнения о дизайне; второе — статистика по модели/поколению это ровно то, что можно честно знать без диагностики конкретной машины). Оставлены как есть, без ложных попыток эти два пункта «решить».