Назад к сервису

Методология скоринга

Под каждым блоком есть поле для комментария — напишите, что непонятно, с чем не согласны или что стоит поменять. Комментарии сохраняются в этом браузере автоматически. Когда закончите — нажмите «Скачать комментарии», сохранится файл с вашими правками — поделитесь им с командой удобным способом.

Техническая документация: как считаются баллы, рекомендованная цена, отсев и рекомендации. Единственный источник истины — lib/scoring.ts, этот файл описывает, что там реализовано, и почему именно так. Если код и документ разойдутся — прав код, документ нужно обновить.

Для пользовательского описания (что вводится, что происходит, что на выходе) — см. USER_GUIDE.md. Для инфраструктуры автоматического CJM (что теперь делает воркер сам, без чата) — DEPLOYMENT_PLAN.md. Для процесса, которым я (Клод) добавляю/перепроверяю машины вручную в чате (Avito, разбор конкретной карточки по запросу) — WORKFLOW.md.

Обзор: три независимых слоя

  1. Основной рейтинг (computeScore) — определяет, какие машины вообще попадают в топ и в каком порядке. Считается для каждой машины по отдельности.
  2. Воронка/триаж (triageStatus, isReadyForPriority) — определяет, допускается ли машина до сравнения вообще (объективный отсев + гейт полноты данных). Это фильтр, не часть суммы баллов.
  3. Практический скоринг (practicalScore) — второй, отдельный вопрос, который решается только внутри уже отобранного «Годно.Рекомендует» (размер пула динамический, 3/5/7 (максимум) — см. topPicksSize): какая из этих машин лучше по чисто практическим для покупки параметрам (цена/пробег/ДТП/владельцы/ожидаемые траты на ТО), с учётом гарантированных вложений после покупки.

Ничего не хранится предвычисленным — все баллы считаются на лету из текущих полей при каждой отрисовке, поэтому изменение любого поля мгновенно пересчитывает рейтинг без отдельной кнопки «пересчитать».

1. Основной рейтинг — computeScore

computeScore = baseScore + aiScore + importBonus + auctionPenalty + recallPenalty + dtpCostRiskPenalty + inspectionScore

baseScore включает три компонента соответствия профилю покупателя — кузов/топливо/привод (bodyTypeMismatchPenalty/fuelTypeMatchBonus/driveTypeMatchBonus, см. раздел 1.1 ниже) — раньше этот факт был виден только в коде, документ формулу не показывал (найдено аудитом методологии 2026-08-10).

Ответ на комментарий пользователя (2026-07-27) — «нужно окно выбора, какие критерии важны каждому пользователю»: реализовано в два захода. У каждого профиля подбора есть enabledCriteria (SearchProfileModal.tsx, чекбоксы на все ALL_SCORE_COMPONENTS) — можно выключить ЛЮБОЕ слагаемое рейтинга целиком (год/пробег/владельцев/цену/любой из категориальных критериев/балл Годно./риски/чек-лист осмотра) для конкретного профиля, не трогая остальные. Вкл/выкл — это только половина запроса; 2026-07-28 добавлены полноценные ВЕСА (criteriaWeights, «пробег важнее ДТП в 2 раза») — см. раздел 1.1а ниже.

Риска техобслуживания в этой формуле больше нет (2026-07-31). Раньше здесь было слагаемое maintenanceRisk (баллы -1..-5). Пересмотрено целиком по разбору с пользователем — см. раздел 5а ниже: критерий переехал в практический балл «Годно.Рекомендует», считается иначе (ожидаемые траты в ₽, не баллы) и виден только там, не в «Рейтинге».

1.0а. Веса важности критериев — criteriaWeights

Каждый ВКЛЮЧЁННЫЙ критерий (ScoreComponentKey, тот же список, что и у enabledCriteria) можно дополнительно взвесить числом 0 / 1 / 2 / 3 / 4 / 5 (CRITERIA_WEIGHT_STEPS) — не множитель поверх базового веса, а прямая замена: итоговый вклад критерия — это его сырой балл, умноженный на это число как есть. Не просто «учитывать/не учитывать», а «насколько важен». Формула не меняется структурно, каждое слагаемое домножается на свой вес ДО суммирования:

Вес 0 — не то же самое, что выключить критерий через enabledCriteria. Выключенный критерий не участвует в расчёте вообще (и не считается «плохим» по молчанию — просто вне формулы). Вес 0 — критерий явно включён, но его вклад в сумму намеренно обнулён (componentWeight() отличает «вес не задан» от «вес задан и равен нулю» — иначе w > 0 тихо подменял бы настоящий ноль дефолтным ×1, ровно обратное тому, что запрашивалось). На практике разница видна в разбивке рейтинга (scoreBreakdown) — критерий с весом 0 там показан строкой с вкладом 0, выключенный через enabledCriteria не показан вообще.

baseScore = yearScore×w(year) + mileageScore×w(mileage) + ownersScore×w(owners) + priceScore×w(price) + Σ(критерий×w(критерий))
computeScore = baseScore + aiScore×w(aiScore) + importBonus×w(importBonus) + (auctionPenalty+recallPenalty+dtpCostRiskPenalty)×w(risks) + 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) на практике мешает уверенно ставить оценку, это стоит сделать отдельным заходом с явным пересчётом/миграцией, а не тихой заменой констант.

1.1. 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

Масштабирование под диапазон профиля (effectiveYearScore/effectiveMileageScore, запрос владельца 2026-08-09). Таблицы выше — дефолт, когда у профиля нет собственного диапазона. Узкий поиск («до 50 тыс. км», «2015–2022», коллекционные седаны 1990-х) с этими фиксированными абсолютными порогами иначе сваливал бы все машины поиска в один и тот же банд — критерий переставал что-либо различать внутри самой выборки. Реальный балл считается пропорционально:

  • Пробег — от triageMaxMileage профиля (тот же порог, что уже используется для отсева, второе поле заводить не пришлось): ≤54.5% потолка → 5, ≤72.7% → 4, ≤90.9% → 3, иначе 1 (те же доли, что дают дефолтные 60/80/100 тыс. от 110 тыс. — при пустом triageMaxMileage результат идентичен таблице выше).
  • Год выпуска — от нового двухстороннего диапазона scoringMinYear/scoringMaxYear профиля (не влияет на отсев, только на балл; отдельное поле в настройках подбора, «Диапазон года выпуска»). Год не имеет «естественного нуля», как пробег — считается расстояние ОТ ВЕРХНЕЙ границы диапазона (новее — лучше), в тех же долях (54.5%/72.7%/90.9%) от самого диапазона maxYear − minYear. Один или оба края не заданы (или перевёрнуты, min ≥ max) — старые фиксированные банды из таблицы выше, не гадаем на неполных данных.

Оба заново вычисляются при каждом показе (рейтинг нигде не хранится в БД) — изменение диапазона профиля применяется мгновенно ко всем его машинам, без отдельного пересчёта.

Владельцы (effectiveOwnersScore) — считается не от сырого числа из объявления, а от эффективного числа владельцев (effectiveOwners): при параллельном импорте (importType === "parallel") к сырому числу автоматически прибавляется +1 — обычно есть незадекларированный зарубежный владелец до ввоза в РФ, и это последовательно применяется во всех расчётах (рейтинг, отсев, «идеальная история», рекомендованная цена).

Таблица зависит от возраста машины (OWNERS_AGE_THRESHOLD_YEARS = 10 лет), не одна на всех — синхронизировано с кодом аудитом методологии 2026-08-10 (раньше здесь была устаревшая упрощённая 3-уровневая таблица, код давно ушёл на две age-зависимые 7-уровневые). Запрос владельца 2026-08-09: старой машине «лишний» владелец не должен стоить так же дорого, как молодой — второй банд стартует на одного владельца позже.

Машине ≤10 лет (OWNERS_BANDS_YOUNG):

Владельцев (эффективно) Баллы
1 5
2 4
3 3
4 2
5 1
6 0
7+ −2

Машине старше 10 лет (OWNERS_BANDS_OLD) — тот же шаг −1/владельца, но старт на одного владельца позже:

Владельцев (эффективно) Баллы
1–2 5
3 4
4 3
5 2
6 1
7–8 0
9+ −2

Цена (priceScore) — НЕ бакеты по абсолютным рублям, а % отклонения фактической цены (priceActual) от независимой рекомендованной цены (recommendedPrice, методика — раздел 4):

Отклонение от рынка Баллы
дешевле рынка на 10%+ 8
−10%…0% 4
0%…+10% 3
+10%…+20% 2
дороже рынка на 20%+ 1

Категориальные критерии — готовый балл хранится напрямую (не пересчитывается формулой), суммируются все сразу:

Критерий Значения и баллы
ДТП (dpt) Нет = 5, ДТП без деталей = 3, Локальный ремонт = 2, Существенный ремонт = 0, Критические повреждения = −5
Двигатель (engine) Дизель 3 л = 5, Дизель 2 л = 3, Остальное = 2 — больше не заполняется ИИ, не входит в сумму баллов (см. ниже)
Пневмоподвеска (pneuma) Да = 3, Нет = 0
Комплектация (trim) Хорошая = 3, База = 1
Состояние кузова (body) 0–5 (ручная оценка)
Состояние салона (interior) 0–5 (ручная оценка)
Стиль (style) 0–5 (ручная оценка)
Класс (klass) Кроссовер = 3, Седан = 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).

klass/engine — не участвуют в computeScore/baseScore (найдено аудитом методологии 2026-08-10), но судьба у них разная после разбора, кто где реально используется:

  • klass — заполняется ИИ, хранится, и это оправдано: используется в peer-пуле рыночной оценки (klassPool, раздел 4 — при редкой модели система смотрит на цены машин того же класса) и показывается в карточке отдельной строкой. Балл ему сознательно не вернули — bodyType уже честнее решает тот же вопрос через соответствие профилю покупателя (см. выше), а не фиксированный рейтинг «кроссовер всегда лучше универсала».
  • engine — убран из промпта ИИ и схемы вердикта (2026-08-10). Проверка по коду не нашла ни одного места, где поле реально читается после сохранения — не в computeScore, не в peer-пуле, не в дедупе, не в UI. Модель тратила внимание на вопрос «дизель 3л или 2л?» на каждом анализе без всякой пользы дальше. CRITERIA.engine и колонка в БД остались (старые значения не трогаем), просто новых больше не спрашиваем.

ДТП (dpt) — единый критерий, объединяет бывшие раздельные «ДТП» и «Окрасы» (аудит методологии ДТП 2026-08-09). Категория выбирается ИИ по явным признакам отчёта/текста (не по одним словам продавца, см. WORKFLOW.md): число повреждённых/окрашенных элементов кузова (1–3 → «Локальный ремонт», 4+ → «Существенный ремонт»), явные маркеры серьёзной аварии (тотал, лонжероны, подушки безопасности → «Критические повреждения», перебивает счёт элементов). Суммы страховых выплат из отчёта — тоже сигнал тяжести (добавлено 2026-08-10, живая находка — BMW X5 с выплатой 531 179 ₽ получил категорию «без деталей», хотя отчёт прямо показывал масштаб аварии): до ~100 000 ₽ ничего не меняет, ~100–200 тыс. ₽ — минимум «Локальный ремонт», ~200–400 тыс. ₽ — минимум «Существенный ремонт», от ~400 000 ₽ — минимум «Критические повреждения» (сумма такого масштаба сама по себе достаточный признак, без обязательного упоминания тотала/подушек). Полный текст правила — системный промпт в lib/ai/claude.ts.

Кузов/топливо/привод (bodyType/fuelType/driveType) — соответствие профилю покупателя, не собственная точечная оценка. Все три работают ОДИНАКОВО (выровнено аудитом методологии 2026-08-10, п.2 — раньше топливо/привод были асимметричны, см. архив ниже): если у профиля не задано целевое значение — 0 (не участвует); если задано и машина совпадает — 0 (все целевые машины получают одно и то же, не награждаем за «то, что и так искали»); если задано и машина явно НЕ совпадает — штраф −2. Пустое поле у самой машины (факт ещё не извлечён) никогда не штрафуется — штраф только за подтверждённое несовпадение, не за отсутствие данных.

Архивная запись (актуальна до 2026-08-10): топливо/привод раньше давали +3 за совпадение и 0 за несовпадение (асимметрично с кузовом) — решение, помеченное как «пока предложено оставить без штрафа». Найдено живым примером: профиль с «нужен только полный привод» никак не наказывал заднеприводную машину. Владелец подтвердил при разборе аудита: та же логика, что у кузова.

techRelevance (S15-P1, добавлено 2026-07-29) — опциональный, не как остальные. Для сегмента «Машины-гаджеты» (мультимедиа/ADAS/гибрид-электро важны больше, чем для типичного покупателя премиального SUV) — сознательно НЕ входит по умолчанию ни в MANUAL_FIELDS, ни в COMPLETENESS_FIELDS. Обе группы имеют побочный эффект при пустом enabledCriteria профиля (архитектурная особенность isComponentEnabled — пустой список значит «включено всё», в т.ч. новые критерии): попадание в MANUAL_FIELDS сделало бы критерий тихим блокирующим гейтом для перехода в «Рейтинг» у ЛЮБОГО существующего профиля, попадание в COMPLETENESS_FIELDS тихо подняло бы порог 80%-полноты для всех, кто про этот критерий не спрашивал. Включается вручную — чекбокс критерия в форме подбора, как и любой другой опциональный компонент рейтинга (раздел 1).

1.2. aiScore

Свободное поле — качественное суждение сверх формульных критериев (нюансы, которые не ловятся ни одним отдельным полем). Хранится как есть, без собственной формулы. Для Avito/Auto.ru/Drom проставляется автоматически фоновым воркером (ClaudeAiProvider, тот же системный промпт и та же методология, что описана здесь — см. DEPLOYMENT_PLAN.md), для остальных площадок и при ручной перепроверке — по-прежнему я (Клод) в чате. Пересмотрено 2026-07-23:

  • Диапазон сужен с −4…+4 до 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 считает систематическое смещение числом, а не по паре запомнившихся случаев. Цикл замкнут 2026-08-14 (Strategic-3, раздел 10) — накопленное смещение по модели (n≥5) теперь автоматически применяется как поправка к свежему aiScore на ДО-репортных путях (analyze-link/verify-listing), не только показывается в дайджесте.

1.3. importBonus

+2, если importType === "official". Официальный ввоз против серого/параллельного — отдельная фиксированная надбавка, не смешивается со свободным aiScore, чтобы причина бонуса была прозрачна.

1.4. auctionPenalty

Штраф за участие в аукционе битых авто (по сути «тотальный ущерб в прошлом», жёстче любого штрафа за обычное ДТП, хранится отдельно от dpt) — с 2026-07-23 градация по тяжести повреждений вместо единственного жёсткого уровня:

auctionHistory Штраф
light (лёгкие повреждения) −2
medium (средние повреждения) −5
critical (тяжёлые повреждения) −10
clean / не заполнено 0

Раньше был один уровень (flagged = −10) на любое попадание в аукцион — но повреждения там бывают очень разными, от машины, которую страховая признала нерентабельной в ремонте после лёгкого ДТП, до реального тотала. Автоматического анализа фото для объективной оценки тяжести пока нет (см. раздел «Анализ фото» в WORKFLOW.md) — тяжесть проставляется вручную при разборе отчёта/объявления. Только critical продолжает участвовать в автоотсеве (triageStatus) — та же планка серьёзности, что была у прежнего единственного уровня; light/medium только снижают рейтинг, не блокируют машину автоматически.

1.5. recallPenalty

−1 балл за каждую непогашенную отзывную кампанию (openRecalls), но не больше −3 суммарно — чтобы одна машина с длинным списком старых отзывов не улетела в минус сильнее, чем реальное ДТП.

1.5а. dtpCostRiskPenalty — сумма ДТП относительно порога профиля (добавлено 2026-08-10)

До этой правки порог «дорогой ДТП» профиля (triageMaxDtpCost, раздел 2) влиял только на автоотсев — участвовал в ScoringContext и прокидывался во все места отображения рейтинга, но ни на что не влиял внутри самой формулы (единственный прежний потребитель, effectiveDptScore, был удалён аудитом методологии ДТП 2026-08-09, когда dpt стал прямой AI-категорией без пересчёта по сумме — мёртвое поле, найдено повторным аудитом 2026-08-10). Машина, прошедшая триаж вручную (triageOverride: "approved" при сумме выше порога), или машина с суммой ремонта заметно БЛИЖЕ к порогу, чем к нулю, получала тот же вклад «risks», что и машина с чистой историей.

Не рескейл категориальной шкалы dpt (ту по-прежнему выбирает ИИ по явным маркерам тяжести, раздел 1.1) — отдельный, дополнительный, градуированный сигнал:

если сумма ДТП неизвестна → 0
ratio = сумма_ДТП / порог_профиля (или TRIAGE_MAX_DTP_COST, если у профиля свой порог не задан)
если ratio ≤ 0.5 → 0 (сумма далеко от порога, ещё не риск)
иначе → −min(3, (ratio − 0.5) / 0.5 × 3) (линейно растёт до −3 на пороге и дальше)

Потолок −3 — того же порядка, что максимум auctionPenalty+recallPenalty вместе, ни один риск не должен односторонне забивать остальные слагаемые «risks».

1.6. Риск техобслуживания — перенесён в раздел 5а

До 2026-07-31 здесь был критерий maintenanceRisk (баллы −1…−5 в основном рейтинге). Убран из computeScore целиком и переработан — методика ожидаемых трат в ₽ и её место в практическом баллу «Годно.Рекомендует» описаны в разделе 5а ниже, вместе с обоснованием, почему баллы рейтинга — не то место, где эта оценка должна была жить.

1.7. 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 / items.length — каждый подтверждённый (match) пункт даёт долю от единицы, независимо от того, сколько пунктов ещё не проверено. computeScore() — единственное место, где применяется Math.round(), поэтому дробный бонус чек-листа никогда не просачивается в отображаемый рейтинг как нецелое число.

Знаменатель — не всегда 8. Для моделей с записью в MAINTENANCE_COST_PROFILES (раздел 5а) чек-лист дополняется до 4 персонализированными пунктами конкретно под болячки этой модели (personalizedChecklistItems, вес 2 — если у пункта есть известная стоимость ремонта, иначе 1) — они входят и в штраф за mismatch, и в знаменатель бонуса наравне с базовыми 8. Диапазон поэтому не фиксирован: от −12 (только базовые 8, все mismatch) до −20 (8 базовых + 4 персонализированных c весом 2 каждый, все mismatch) в худшем случае; бонус — по-прежнему максимум +1 при 100% подтверждённых, независимо от знаменателя.

Чек-лист меняет только числовой балл (inspectionScore в computeScore). Текстовые выводы (aiComment/aiStrengths/aiWeaknesses) чек-лист не трогает — после того как пользователь его заполнил и сохранил, я отдельно перечитываю результат в чате и обновляю их вручную (см. WORKFLOW.md, «Чек-лист осмотра»).

2. Воронка / быстрый отсев — 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) — свободный прямой доступ явно не тривиален, иначе перепродавать было бы нечего.
  • Даже если бы капчи не было: VIN не заполняется до покупки платного отчёта — Car.vin распознаётся только из имени файла отчёта (lib/vin.ts), автопарсер объявлений (Auto.ru/Drom) не вытаскивает VIN из текста. То есть звонить в эти сервисы на этапе бесплатного триажа физически не по чему для подавляющего большинства карточек.

Решение: не подключать. Автоматизация была бы либо (а) невозможна без решения капчи, либо (б) платная через реселлера (у одного из проверенных — api-cloud.ru — минимальный платёж 700 ₽/мес при <2000 запросов/мес, первый счёт от 3000 ₽) — пользователь платный сервис сейчас подключать не хочет. Раздел закрыт без реализации; если объём базы/бюджет изменятся, вернуться можно с тех же двух источников.

3. Гейт готовности данных — isReadyForPriority

Машина переходит из «Черновиков» в «Рейтинг», только когда выполнены все три условия:

  1. Заполнены обязательные ручные поля (hasAllManualFields, REQUIRED_MANUAL_FIELDS) — trim, body, interior, style. pneuma (пневмоподвеска) в их число не входит с 2026-08-07 — редкая опция, требовать явный ответ на каждой карточке было рутиной без пользы для рейтинга; поле по-прежнему нельзя определить по тексту объявления и оно участвует в скоринге, если указано, просто не блокирует переход.
  2. Заполнено ≥80% всех 15 критериев оценки (hasEnoughDataForScore, список — COMPLETENESS_FIELDS, точное число — COMPLETENESS_FIELDS.length в коде, не хардкодить здесь на будущее — «18» тут было устаревшим ещё до вчерашних правок, поймано только глубоким аудитом 2026-08-10). Ниже этого порога сравнение с другими машинами вводит в заблуждение — слишком много полей посчитаны как отсутствующие (по умолчанию 0/нейтрально), рейтинг не отражает реальное качество машины.
  3. Прошла отсев: triageStatus !== "reject", а если review — только при triageOverride === "approved".

Это не отдельный статус в БД, а живой пересчёт из текущих значений полей при каждой отрисовке — карточка сама «переезжает» между вкладками по мере дозаполнения, без ручного переключения.

4. Независимая рекомендованная цена — 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-взвешивание трёх источников снизу вверх:

  1. Приор — текущая цена карточки (обычно догадка нейросети, ClaudeAiProvider по общим знаниям о рынке, либо предыдущее значение recommendedPrice) — передаётся в peerMarketEstimate() явным третьим аргументом, не зашита в функции константой.
  2. Пиры того же класса/сегмента (klass — Кроссовер/Седан/Компакт/Универсал) — частичный пулинг между моделями: раньше редкая модель с 1-2 карточками в базе не занимала вообще ничего у похожих моделей того же сегмента, теперь широкий пул того же класса служит промежуточным ориентиром.
  3. Пиры той же модели — самый точный уровень, как и раньше.

Каждый уровень считается той же формулой медиана+поправка, что и раньше:

оценка_уровня = медиана_цены_пула
        − (пробег_машины − медиана_пробега_пула) / 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). С 2026-08-10 (повторный аудит методологии, п.2) первые два (₽/км, ₽/владельца) масштабируются под бюджет вызывающего профиля — segmentScaleFactor(priceMin, priceMax) в lib/scoring/market.ts использует уже существующее поле профиля «бюджет от-до» (не заведено отдельное поле «сегмент»): бюджетный поиск получает коэффициент заметно ниже 1, премиум-сегмент — заметно выше. Без priceMin/priceMax у профиля — коэффициент 1, старое поведение не меняется. PEER_DTP_WEIGHT (0.5) НЕ масштабируется — не входил в исходную находку аудита, оставлен как есть.

Уверенность и диапазон вместо голой точки. PeerMarketEstimate теперь возвращает не только price, но и peerCount/klassPeerCount (сколько наблюдений на каждом уровне), confidence (low/medium/high, производная от эффективного веса пиров модели) и priceRangeLow/priceRangeHigh (наблюдаемый диапазон цен пула, из которого взята оценка). marketComment (formatPeerEstimateComment()) явно называет источники и уровень уверенности — раньше текст был один и тот же независимо от того, откуда взялась цена.

Ответ на комментарий пользователя — идеи повысить релевантность оценки (архив, часть решена выше):

  • «Поднять порог с 3 до 5-7» — снято пересмотром выше: жёсткого порога больше нет вообще, вопрос «сколько доверять N пирам» решается плавным весом, а не одним числом.
  • «Сравнивать не модель vs модель, а с учётом года/пробега/ДТП/владельцев» — частично решено частичным пулингом по klass (см. выше); полноценная многофакторная регрессия по всем признакам сразу по-прежнему отложена — нужно ощутимо больше исторических данных, чтобы не переобучиться на нескольких точках на модель.
  • «Собирать цены по всем агрегаторам (Avito+Drom+Auto.ru), не только с сайта своего объявления» — не сделано: требует платного скрапинг-провайдера или парсинга нескольких сайтов разом (тот же класс работы, что Avito-автоматизация) — решение с реальной денежной стоимостью, не форсирую его без отдельного разговора.

Ограничение методологии: при тонкой выборке (confidence: "low") результат размечается как менее уверенный прямо в marketComment — это не скрывается от пользователя. С 2026-08-10 (аудит методологии, п.7) UI идёт дальше пометки в тексте: hasLowMarketConfidence() (lib/scoring/market.ts) пересчитывает peerMarketEstimate на пуле профиля прямо при отрисовке и, если confidence === "low", скрывает штамп «±N% от рынка» в таблице/карточках/сравнении/«Годно.Рекомендует» целиком — сама цена объявления видна всегда, пропадает только сравнение с рынком, которое на тонкой выборке читалось как более уверенное утверждение, чем оно есть на самом деле. Проверить реальный эффект пересмотра методологии оценки цены на уже накопленной базе (сколько карточек и на сколько изменили recommendedPrice/состав «Годно.Рекомендует») можно через scripts/scoring-snapshot.ts + scripts/scoring-diff.ts — см. раздел 10.

5. Практический скоринг «Годно.Рекомендует» — practicalScore

Отдельный, второй вопрос: не «какие 5 машин вообще стоит смотреть» (это по-прежнему решает основной рейтинг, раздел 1), а «какая ИЗ ЭТИХ пяти лучше по чисто практическим для покупки БУ авто параметрам».

practicalScore = 0.263 × ценаSub + 0.138 × пробегSub + 0.314 × ДТПSub + 0.066 × владельцыSub + 0.219 × надёжностьSub

Веса пересчитаны AHP-инструментом 2026-08-10 (аудит методологии, п.3) — первый реальный прогон инструмента, который существовал с 2026-07-28, но ни разу не применялся. Попарные сравнения (scripts/ahp-practical-weights-2026-08-10.json, npx tsx scripts/ahp-weights.ts <файл>) дали consistencyRatio 0.001 (< 0.1 — согласовано). Владелец проверил результат на живом пересчёте реального пула (13 машин BMW X5 в базе) до и после смены весов — 2 машины реально поменялись местами в ранжировании — и одобрил применение. Раньше (до этой правки) веса были 0.30/0.15/0.25/0.10/0.20 — сами тоже поменяны один раз, 2026-07-28, но по одному устному аргументу без проверки, не AHP: «уровень влияния ДТП на практический скоринг недооценён» (было 0.20/0.15/0.30 в порядке цена/пробег/ДТП — задом наперёд от текущего, ДТП и пробег стояли на изначальных местах, поменяны местами тем же заходом). Честная оговорка про порог: dtpCostForScoring по-прежнему ограничивает сумму ДТП сверху — но с 2026-08-10 (повторный аудит методологии) этот потолок реальный порог КОНКРЕТНОГО профиля (triageMaxDtpCost, 5-й параметр practicalScore), не жёстко зашитая глобальная константа: раньше кэп совпадал с TRIAGE_MAX_DTP_COST (100 000 ₽) независимо от того, что реально настроил пользователь в отсеве этого профиля — та же болезнь, что уже раз чинили 2026-07-24 (тогда константу привязали к дефолту, но не к реальному профилю), найдено на шаг глубже. На практике различия в ДТП-подоценке внутри пула часто меньше различий в пробеге, новый вес даёт бо́льшую ЧУВСТВИТЕЛЬНОСТЬ к оставшимся различиям, не гарантирует драматический сдвиг на каждом пуле.

Компонент «надёжность» добавлен 2026-07-31 (веса пересчитаны с нуля под 5 слагаемых, не пропорционально урезаны старые 4 — price/dtp сознательно остаются двумя самыми весомыми статьями потенциальных денег) — методика в разделе 5а ниже.

Каждая под-оценка нормализуется 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 ₽ — большая доля от меньшей базы), что и отражает интуицию «разница в цене значит меньше на фоне одинаковых для всех вложений». Это postPurchaseBudget — ОДИНАКОВАЯ для всех машин пула сумма (сколько вы в любом случае готовы вложить), не путать с компонентом «надёжность» (раздел 5а) — тот считает РАЗНУЮ по машинам ожидаемую стоимость обслуживания конкретно ЭТОЙ модели, отдельным слагаемым, не подмешивается в цену.

Дефолт 350 000 ₽ масштабируется под бюджет профиля, если пользователь не задал свой (2026-08-10, повторный аудит методологии, п.4). effectivePostPurchaseBudget(profile) (lib/scoring/market.ts) — 6% от priceMax профиля (postPurchaseBudgetSuggestion()), округление до 10 000 ₽. Не segmentScaleFactor/масштаб от опорного сегмента (первая версия правки в тот же день использовала его — владелец отменил: «опорный сегмент не актуален, мы везде должны считать 6% от указанного пользователем диапазона»). Формула единая для трёх мест сразу — эта функция, зелёная подсказка «рек. X ₽» в визарде и в квизе Stage0 — раньше это были три независимые копии, разошедшиеся на 17-30% на реальных бюджетах, найдено сверкой документации с кодом. Ни диапазон не задан вообще — тогда честный дефолт 350 000 ₽. Явное значение postPurchaseBudget в профиле по-прежнему в безусловном приоритете.

ДТП без точной суммы (dtpCostForScoring) — для практического скоринга (не для отсева) категория без цифры оценивается условно, а не приравнивается к нулю: «Нет» → 0, «Лайт» без суммы → 0.6 от порога, известная сумма — используется как есть, с тем же потолком. Порог — не жёсткий глобальный (100 000 ₽), а реальный triageMaxDtpCost конкретного профиля (см. раздел 5, «честная оговорка про порог» выше) — если у профиля свой порог не задан, используется дефолт TRIAGE_MAX_DTP_COST (100 000 ₽, тот же, что у фильтра «дорогое ДТП» по умолчанию).

5а. Компонент «надёжность» практического балла — ожидаемые траты на ТО (expectedMaintenanceCost)

До 2026-07-31 риск техобслуживания был отдельным слагаемым основного рейтинга (maintenanceRisk, баллы −1…−5 в разделе 1.6 выше). Пересмотрено целиком в разговоре с пользователем — найден методологический изъян: данные по каждой модели собирались чтением форумов/DRIVE2/сервисных блогов, где владельцы пишут про поломки на порядки чаще, чем про «проехал 200 тыс. без единой проблемы» — reporting bias (систематическое смещение к негативным отзывам, без знаменателя «сколько всего таких машин»). Конкретный симптом: Audi Q7 получала максимальный штраф −5 (тот же потолок, что у моделей с реально более тяжёлой репутацией), хотя независимые опросы всего парка — ADAC Pannenstatistik (реальные вызовы техпомощи по всей Германии) и TrueDelta (обязательный отчёт всех участников программы, не только у кого что-то сломалось) — независимо друг от друга ставят Q7 5 звёзд / «excellent reliability».

Новая методика — актуарная (вероятность × стоимость = ожидаемые траты):

expectedMaintenanceCost = Σ по применимым пунктам( P(тир) × reliabilityDamping_модели × costRub )
  • frequencyTier — качественная частота КОНКРЕТНОГО дефекта по языку источника (rare 3% / occasional 10% / common 25% / very_common 50%, FREQUENCY_PROBABILITY в lib/maintenanceProfiles.ts) — приближение, не измеренная величина, но та же логика, что уже неявно применялась раньше (пункты с «редкий, единичный случай» получали extraRisk: 0, с «характерный» — реальный штраф), теперь явное поле, а не подразумеваемое.
  • reliabilityDamping — коэффициент доверия НА УРОВНЕ МОДЕЛИ ЦЕЛИКОМ (не на уровне отдельного дефекта), из агрегатной статистики (ADAC/TrueDelta/RepairPal/J.D. Power/Consumer Reports): источники согласны, что модель надёжнее среднего → 0.4 (сильная скидка на форумную оценку конкретных дефектов); источники противоречат друг другу (напр. Porsche Cayenne: ADAC 5★ против RepairPal «последнее место в классе») → 0.7 (скидка минимальная, осторожность из-за разногласия источников, не игнорирование негативного сигнала); независимого источника нет вообще или источники согласны, что риск реальный (напр. Cadillac Escalade, Range Rover Sport) → 1.0 (без скидки). Обе ветки 1.0 намеренно остаются одним и тем же коэффициентом демпфирования трат — но с 2026-08-18 (H1↔D8, см. врезку ниже) это больше не одно и то же для другой цели, флэт-штрафа топ-5.

sourceConfidence — отдельный от reliabilityDamping сигнал (H1↔D8 фикс, аудит методологии/промптов, раунд-3 2026-08-18). Разбор двух независимо предложенных фиксов вскрыл прямое противоречие: методологический аудит (H1) требовал перестать путать «независимого источника нет вообще» с «источники согласны, риск реален» — оба давали reliabilityDamping=1.0, и флэт-штраф топ-5 (engineReliabilityPenalty, ниже) штрафовал по одному и тому же условию damping>=1.0; аудит промптов (D8) предлагал текст, который эту путаницу закрепил бы явной формулировкой. Решение — развести сигнал полем, не одним числом: sourceConfidence: "agree_good" | "agree_bad" | "disagree" | "no_data" в MaintenanceCostProfile/RESEARCH_TOOL, reliabilityDamping не меняется (по-прежнему коэффициент демпфирования трат из формулы выше), engineReliabilityPenalty теперь гейтится строго на sourceConfidence==="agree_bad". Реальный эффект на проде: два хардкоженных профиля (Audi Q5L, Zeekr 001 — обе China-only, вне покрытия ADAC/TrueDelta/RepairPal/JD Power) до фикса ошибочно штрафовали машину и показывали в «Годно.Рекомендует» текст «независимые источники подтверждают риск», хотя независимых источников для этих моделей нет вообще — оба профиля перепомечены sourceConfidence:"no_data", штраф для них больше не применяется. Поле опционально — старые хардкоженные профили и уже опубликованные в БД записи без явного значения трактуются как «нет прямого сигнала» (безопасный дефолт: лучше промолчать, чем ошибочно тревожить), не как автоматическое agree_bad.

  • costRub — типичная стоимость ремонта, ЕСЛИ дефект произошёл (не участвует в вероятности) — из текста источника (диапазон → середина) или по аналогии с похожим ремонтом на другой модели той же платформы, если источник называет дефект, но не сумму.
  • Гейтинг по применимости — тот же, что был у maintenanceRisk (проекция пробега car.mileage + buyerProfile.annualMileageKm × buyerProfile.ownershipYears, appliesIf по типу ввоза) — эта часть логики не изменилась, см. issueRelevance в lib/scoring.ts.

Результат — ₽, не баллы, и не участвует в основном рейтинге (computeScore) вообще. Единственное место, где используется — 5-й компонент практического балла (normalizeLowerIsBetter внутри пула «Годно.Рекомендует», тот же принцип, что у ДТП/пробега/владельцев выше): абсолютная точность оценки в ₽ вторична, важен только относительный порядок машин внутри сравниваемой пятёрки — систематическая ошибка в одну сторону у ВСЕХ оценок почти не меняет порядок.

Число нигде не показывается пользователю напрямую — ни диапазон, ни точка (решение пользователя 2026-07-31: волатильность стоимости ремонта по регионам/сервисам/оригинальности запчастей делает точную ₽-цифру маркетингово нечестной — тот же эффект, что у цены «от X₽», которая на деле оказывается в разы больше). Видно только опосредованно — через место машины по этой оси практического балла (компактно) и через качественный список «чего ждать при обслуживании» с честными пометками по каждому пункту (актуально/появится позже/не относится) — оба ТОЛЬКО для машин, реально попавших в «Годно.Рекомендует» (isInTop5), не для всех подряд в «Рейтинге» (там раньше была отдельная колонка «ТО» — убрана вместе с критерием).

Пример (Audi Q7, официальный ввоз, damping 0.4, пункт «цепь ГРМ» — tier occasional 10%, costRub ≈55 000₽, порог 40 тыс. км): при прогнозном пробеге ≥40 тыс. км ожидаемые траты по этому пункту — 0.10 × 0.4 × 55 000 ≈ 2 200₽. Для сравнения — Porsche Cayenne (damping 0.7, пункт «пневмоподвеска» — tier common 25%, costRub 60 000₽): 0.25 × 0.7 × 60 000 = 10 500₽ по одному этому пункту. Разница отражает не только разную частоту дефекта, но и разную уверенность в источниках (Q7 — оба независимых источника согласны, Cayenne — противоречат).

Полный пересмотр исходных моделей + 4 новых (Audi A4 allroad, Hyundai Palisade, Infiniti QX80, Land Rover Range Rover Sport) сделан одним заходом 2026-07-31, список с тех пор пополнялся — на 2026-08-03 в MAINTENANCE_COST_PROFILES 22 модели, см. lib/maintenanceProfiles.ts для актуального списка, профилей и источников по каждой.

Первичное исследование новых моделей автоматизировано целиком, включая публикацию (найдено устаревшим при повторном аудите методологии 2026-08-10 — здесь раньше стояло «автоматизации по-прежнему нет», это была неточность, не отражавшая уже реализованный MAINTENANCE_AUTOMATION_PLAN.md). При 3+ машинах новой модели без профиля (maybeQueueModelResearch, lib/maintenanceResearchTrigger.ts) фоновая задача (lib/jobs/research-model.ts) сама ищет источники (ADAC/TrueDelta/RepairPal/JD Power/DRIVE2/Drom/Auto.ru), формирует профиль через ИИ и публикует его автоматически, если источники согласны (passesSourceAgreementGate) и профиль прошёл критик-гейт — без единого ручного шага. Ручное вмешательство нужно ТОЛЬКО когда источники расходятся или критик не пропустил — тогда статус pending_review, уведомление в Telegram (sendMaintenanceReviewTelegram), и уже реально может провисеть без ответа (на 2026-08-10 в очереди застряла ровно 1 такая модель, второй день без реакции). Список источников целиком западный (ADAC/TrueDelta/RepairPal/JD Power) — непроверенная гипотеза: для брендов без представленности на этих рынках (например китайские) шанс не пройти гейт согласия источников может быть выше, ни одна такая модель пока не прошла через пайплайн, чтобы проверить.

Нечёткое совпадение имени модели при чтении профиля (maintenanceProfile(), lib/domain/car.ts, аудит методологии 2026-08-10, п.4). Реальный сбой на проде (2026-08-09): «GLS-класс»/«Touareg» не находили уже готовые профили «Mercedes GLS»/«VW Touareg» точным совпадением строки — 9 карточек молча остались без данных, обе модели заново (платно) исследованы под «новым» именем. Раньше единственным способом закрыть это было руками дописать пару в MAINTENANCE_ALIASES — для каждого нового случая заново. Теперь при промахе точного совпадения maintenanceProfile() пробует resolveCanonicalModel() (lib/maintenanceModelNormalize.ts) — тот же safe fuzzy-механизм, что уже применялся только при постановке задачи на исследование (maintenanceResearchTrigger.ts): опечатка/пропущенный бренд при СОВПАДАЮЩЕМ коде модели («X5» находит «BMW X5», «Q5» никогда не найдёт «Q7» — код модели сравнивается дословно, без этого разделения близкие модели слились бы в одну), плюс общее правило для суффикса каталога «-класс» и короткий список устойчивых сокращений бренда (Volkswagen/VW, Mercedes-Benz/Mercedes). MAINTENANCE_ALIASES остаётся — там пары, которые это принципиально не покрывает (например «GLS-класс» = «GLS» — суффикс уже правило общее, но исторические записи оставлены как есть, лишняя перепроверка не требуется).

6. Аргументы для торга — 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 довода, объясняющие меньшую часть числа). Сами весовые коэффициенты в таблице выше не менял — это те же эмпирические ориентиры для сегмента, что и раньше, конкретных альтернативных цифр предложено не было.

Счётчик оставшихся доводов убран (round 4, D2, 2026-07-30). Кратко существовал BargainAdvice.extraCount (S28, 2026-07-29) со строкой «И ещё N причин (не вошли в сумму торга)» под топ-3 доводами. Решение пересмотрено: аргументы для торга теперь всегда ровно топ-3, без счётчика остатка — упрощение интерфейса «Годно.Рекомендует» по итогам живого раунда правок.

Медиана вместо среднего для сравнения с похожими (2026-07-28, «Независимый взгляд на методологию»). Пороги «пробег выше похожих на 15%+» и «владельцев больше среднего +1» раньше считались от арифметического среднего пула похожих машин той же модели — на пуле из 2-6 карточек одна кривая запись (опечатка в пробеге, неучтённый выброс) сдвигает среднее сильнее, чем медиану. Заменено на медиану (та же функция median(), что уже используется в peerMarketEstimate, раздел 4) — устойчивее к единичным аномалиям, сам порог (15%/+1) не менялся.

7. Приоритизация покупки платных отчётов — reportPurchasePriority

Живая (не хранимая) функция, полностью производная от triageStatus:

Статус триажа Приоритет отчёта Логика
reject 🔴 skip Машина и так уходит в отказ по бесплатным данным — отчёт её не спасёт, деньги на него не окупятся
review 🟡 high Машина в «серой зоне» именно из-за нехватки данных, которые способен закрыть отчёт (например, точная сумма ДТП) — максимальная отдача с потраченных 200–300 ₽
ok 🟢 low Машина и так уже проходит по бесплатным данным — отчёт нужен как подстраховка перед реальным осмотром, а не для отсева

Цвета high/low поменяны местами 2026-07-28 по комментарию пользователя — раньше high (реальная неопределённость, которую отчёт может разрешить) был зелёным, что читалось как «всё хорошо», хотя смысл ровно обратный. Теперь цвет — это статус машины по бесплатным данным (🟢 всё уже ясно, 🟡 есть неопределённость, 🔴 уже отказ), а не «насколько выгодно» тратить деньги на отчёт.

8. Прочие производные признаки

  • «Идеальная история» (hasIdealHistory) — декоративный бейдж. Два условия всегда фиксированные: effectiveOwners ≤ 3 (эффективное число владельцев — та же поправка на незадекларированного зарубежного владельца при параллельном ввозе, что и в разделе 1.1, не сырое поле owners) и dpt === 5 (ДТП нет). Третье условие — не фиксированная пара, а топ-3 по весу профиля из пула в 6 критериев (IDEAL_HISTORY_POOL: официальный ввоз, срок владения текущим >2 лет, отсутствие рисков (аукцион/отзывные), нет окраса, состояние кузова/салона на «отлично») — какие именно 3 из 6 реально проверяются, зависит от criteriaWeights профиля (idealHistoryCriteria, раздел 1.0а: критерии с большим весом важнее пользователю, значит именно их и стоит требовать для «идеальности» именно в этом профиле) и от того, какие из 6 вообще включены (enabledCriteria) — выключенные в пул не попадают. Бейдж не показывается, пока хоть одно из требуемых для профиля полей не заполнено. openRecalls === 0 именно (не == null || === 0) — Strategic-2, см. врезку в разделе 10: до 2026-08-14 непроверенные отзывные кампании засчитывались наравне с подтверждённым нулём, симметричная регрессия к уже исправленному auctionHistory в том же условии.
  • Готовность карточки (completenessCount/missingFields) — сколько из COMPLETENESS_FIELDS (точное число — COMPLETENESS_FIELDS.length в коде, не хардкодить здесь — «18» тут уже однажды было устаревшим числом, см. раздел 3, поймано только глубоким аудитом 2026-08-10) заполнено, показывается как N/M в таблице; используется и для гейта раздела 3.
  • Разбивка рейтинга (scoreBreakdown) — раскладывает computeScore на подписанные слагаемые (по каждому критерию отдельно, плюс «Балл Годно.», «Официальный ввоз», «Риски», чек-лист осмотра), отсортированные по убыванию — используется в объяснении «почему машина на этом месте» в «Годно.Рекомендует». Риска техобслуживания здесь больше нет с 2026-07-31 — см. раздел 5а.

8.1. Индикатор уверенности в вердикте — 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), не только факт наличия файла. Тот же принцип, что уже применяется при гейте «Годно.Рекомендует» — предотвращает путаницу, которая уже случалась вручную (карточка с загруженным, но непрочитанным отчётом выглядела проанализированной).

8.2. Честные маркеры недостающей проверки — 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 вручную, потому что это не совет, а факт о текущем состоянии данных.

9. Автоматизация CJM (2026-07-27, дополнено 2026-07-28)

Отдельный слой поверх методологии выше — не про сами формулы, а про то, кто сейчас их применяет. До 2026-07-27 весь путь «объявление → заполненная карточка → вердикт» проходил через меня в чате. Теперь для Avito/Auto.ru/Drom он полностью автоматический: фоновый воркер сам скрейпит объявление (Firecrawl для Auto.ru/Drom, платный Автопульс для Avito — капча блокирует Firecrawl даже через выделенный скрапер, проверено дважды на реальных ссылках), вызывает ту же модель (Claude, тот же системный промпт, что реализует разделы 1–8 выше) и пишет результат в карточку — без единого ручного шага. Кнопка «Обновить данные» (и суточное расписание, auto-refresh-listings) той же цепочкой перепроверяет уже существующие карточки.

Что осталось ручным и почему:

  • Ссылки за пределами Avito/Auto.ru/Drom — для них автоматического скрапинга нет вообще (не проблема капчи, просто не реализовано), карточка заполняется вручную («Заполнить вручную» в интерфейсе) или, если у меня есть доступ к этой ссылке, мной в чате тем же путём, что был основным до 2026-07-27.
  • Живой поиск рыночных аналогов при малом числе пиров в базе (раздел 4, приор) — автоматический вызов работает только с текстом самого объявления/отчёта, живого поиска 3–5 сравнимых объявлений в рамках этого вызова не делает. Цена в таком случае — оценка модели по общим знаниям о рынке (приор), а не сверенная с конкретными живыми аналогами; при малом числе пиров именно этот приор всё ещё определяет большую часть итоговой цены (см. credibility-вес).
  • Чек-лист осмотра и body/interior/style — по-прежнему требуют физического осмотра машины, автоматизировать нечем.

Это раздел специально для комментариев по самой автоматизации — если считаете, что живой поиск цены стоит закрыть платным скрапинг-провайдером, или видите другой приоритет, пишите здесь.

10. Инструментарий: бэктест / AHP / калибровка (2026-07-28)

Отдельный слой поверх методологии — не новые формулы, а инструменты для того, чтобы БУДУЩИЕ правки формул проверялись числом на реальных исторических данных, а не только рассуждением в комментарии к коммиту («Независимый взгляд на методологию» — независимый разбор текущей математики системы и предложенный по его итогам план).

  • Бэктест-стенд (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.

  • AHP (Analytic Hierarchy Process) (lib/ahp.ts + scripts/ahp-weights.ts) — ОБЯЗАТЕЛЬНЫЙ шаг перед любой будущей сменой весов (PRACTICAL_WEIGHTS в lib/scoring/market.ts и componentWeight-дефолты baseScore — раздел 1.1, см. находку ниже) вместо разовой договорённости по одному устному аргументу (как было с dtp/mileage 0.20/0.30→0.30/0.20 в 2026-07-28) — структурированные попарные сравнения критериев («ДТП важнее пробега во сколько раз?») с проверкой согласованности (consistencyRatio — ловит внутренние противоречия сравнений, например A>B>C, но C>A). Найдено глобальным аудитом 2026-08-07 (S-MEDIUM-11): инструмент был построен и проверен тестами, но ни разу фактически не применялся к реальной смене веса. Закрыто 2026-08-10 (аудит методологии, п.3) — первый реальный прогон, применён к PRACTICAL_WEIGHTS (раздел 5), попарные сравнения зафиксированы в scripts/ahp-practical-weights-2026-08-10.json для воспроизводимости. При следующей смене весов (PRACTICAL_WEIGHTS или baseScore) — тот же путь, не устный аргумент; comparison-файл сохраняется рядом со скриптом по тому же шаблону имени (scripts/ahp-<цель>-<дата>.json) для воспроизводимости и код-ревью.

    Находка Q5 (аудит методологии 2026-08-14, «прогнать существующий инструмент на текущих весах — узнать, есть ли противоречие»): повторный прогон scripts/ahp-weights.ts scripts/ahp-practical-weights-2026-08-10.json подтвердил прежний результат (consistencyRatio 0.001, согласовано) — сами PRACTICAL_WEIGHTS внутренне непротиворечивы. Но сравнение с baseScore (раздел 1.1) вскрывает противоречие МЕЖДУ формулами, не внутри одной: baseScore по умолчанию взвешивает price/mileage/owners/risks поровну (componentWeight = 1 у всех четырёх, пока пользователь не задаст веса вручную через «Критерии ранжирования»), а PRACTICAL_WEIGHTS — те же по смыслу четыре измерения (цена/пробег/владельцы/ДТП) — весит их сильно неравномерно: ДТП (31.4%) весит в 4.8 раза больше владельцев (6.6%), цена (26.3%) — почти вдвое больше пробега (13.8%). То есть машина с плохими владельцами и машина с серьёзным ДТП штрафуются ОДИНАКОВО на этапе основного рейтинга/триажа (baseScore), но СИЛЬНО по-разному на этапе «Годно.Рекомендует» (practicalScore) — та же система по-разному отвечает на один и тот же вопрос «что важнее» в зависимости от того, какая из двух формул считает. Изменений в этом пакете сознательно не внесено (Q5 — узнать, не изменить); Strategic-4 переводит следующий шаг из «возможно в будущем» (формулировка выше) в обязательный пункт: baseScore-дефолты — кандидат №1 на AHP-пересчёт следующим заходом, с тем же протоколом (comparison-файл + бэктест на реальной базе перед применением, как раздел 5).

  • Системное правило: «неизвестно» ≠ «подтверждённо хорошо» (Strategic-2, аудит методологии 2026-08-14). Сформулировано явно как принцип, не разовый фикс: там, где отсутствие данных о поле МОЖЕТ выглядеть как благоприятный факт (бейдж, награда, отсутствие штрафа при нормализации на диапазон), null обязан вести себя СТРОЖЕ, чем подтверждённое хорошее значение, а не наравне с ним — тот же принцип, что уже применён к auctionHistory (S-MEDIUM-9, 2026-08-07) и hasUnverifiedPremiumTrimClaim/hasPresentationVsConditionGap (S22/S23). Целенаправленный проход по lib/scoring.ts/lib/scoring/*.ts/lib/domain/car.ts нашёл:

    • Подтверждённый и исправленный: idealCriterionMet("risks", …) (раздел 8) — openRecalls == null засчитывался наравне с подтверждённым 0 в бейдже «Идеальная история», в буквальном соседстве со строкой, уже один раз исправленной для auctionHistory тем же аудитом 2026-08-07 — регрессия того же принципа внутри одной и той же функции. Реальные данные: из 129 карточек с auctionHistory: "clean" 21 имели openRecalls: null (не проверялось вообще) — все 21 до фикса неправомерно проходили критерий, после — нет.
    • Найдены, но НЕ изменены в этом заходе (архитектурно согласованы с уже существующей конвенцией, требуют отдельного решения, не точечного патча): engineReliabilityPenalty (раздел 5а, lib/scoring/market.ts) и carDtpCost ?? 0 в estimateFromPool (раздел 4) — оба используют тот же "штраф только на подтверждённо плохом, необследованное = потолок 0" паттерн, что и auctionPenalty/recallPenalty/bodyTypeMismatchPenalty и другие плоские штрафы в разделе 1 (везде по коду есть комментарий "не наказывать за отсутствие данных, а не за реальное несоответствие") — то есть здесь "неизвестно" не превосходит подтверждённо-хорошее, а делит с ним общий потолок наравне с самим принципом плоских штрафов. Не патчится точечно, потому что смена этой конвенции — решение уровня всей формулы (та же категория, что и baseScore-AHP выше), не одного места. engineReliabilityPenalty отдельно получил другой, не архитектурный фикс 2026-08-18 (H1↔D8, см. врезку в разделе 5а) — гейт условия был не просто "не наказывать за неизвестное", а буквально путал два РАЗНЫХ значения одной и той же переменной (damping=1.0 при "риск подтверждён" и при "данных нет вообще") под одним условием; carDtpCost ?? 0 такой путаницы не содержит, остаётся as-is.
  • Калибровка aiScore (lib/calibration.ts, лог — data/calibration-log.jsonl, отчёт — scripts/calibration-report.ts) — см. раздел 1.2. Автопоправка (Strategic-3, аудит методологии 2026-08-14, «замкнуть цикл калибровки») — formatCalibrationDigest уже автоматически показывает систематический перекос по модели владельцу в Telegram каждые 25 записей, но что делать с этим числом до 2026-08-14 оставалось целиком ручным решением. suggestedAiScoreCorrection()/applyAiScoreCorrection() замыкают цикл: для модели с n≥MIN_TRUSTWORTHY_SAMPLE (тот же порог 5, что и у самого дайджеста — не заведена отдельная, более мягкая граница) analyze-link.ts/verify-listing.ts (оба — ДО-репортные пути, где aiScore ещё не видел более достоверный источник) автоматически сдвигают свежий aiScore на округлённое накопленное смещение, зажатое в диапазон -2..2. analyze-report.ts (уже ПОСЛЕ отчёта) поправку сознательно НЕ применяет — иначе то самое расхождение, которое копит лог, задваивалось бы само на себя. Реальные данные на момент внедрения (114 записей): BMW X5 (n=24, смещение −0.67 → поправка −1), Audi Q7 (n=22, −0.64 → −1), Mercedes GLS-класс (n=6, −0.67 → −1) — Porsche Cayenne/Audi Q5/Mercedes GLE (|смещение|<0.5) поправки не получают, сигнал слишком слабый для целого пункта шкалы.

  • Калибровка порога belowMarketRisk против реального рынка (P6 pilot, аудит методологии 2026-08-14) — lib/belowMarketRiskPilot.ts (чистая логика) + scripts/calibrate-below-market-risk-pilot.ts (ручной запуск, квота Автопульса ограничена). Порог -8% в belowMarketRisk (раздел 2) флагует отклонение от recommendedPrice — НАШЕЙ ЖЕ оценки по машинам в базе, ни разу не сверенной с тем, что происходит на реальном рынке прямо сейчас. Скрипт берёт уже флагованные карточки, тянет свежие предложения того же года ±2 через searchOffers() (Автопульс, lib/parsing/autopulse.ts) и сравнивает: подтверждает ли реальный рынок ту же просадку цены (confirmed), опровергает (contradicted, наш -8% не подтверждён рынком — сигнал пересмотреть порог/пул для конкретной модели), или живых предложений для сравнения не нашлось (insufficient_real_data). Пилот — по выбору владельца узкий (только этот порог, не полная рыночная калибровка recommendedPrice), запускается вручную: npx tsx scripts/calibrate-below-market-risk-pilot.ts [макс_моделей]. Первый живой прогон (P6) сразу же нашёл и починил реальный баг в searchOffers() — page был помечен опциональным параметром, но Автопульс безусловно требует его (400 "parameter page is required"); функция ни разу не вызывалась в проде до этого пилота, баг был в коде с самого начала.

  • «Собрано, но не читается дальше» (Strategic-5, аудит методологии 2026-08-14) — scripts/find-unread-fields.ts. Автоматизирует ручной grep-аудит, которым нашли P1 (раздел 2 плана, «Ремонт методологии»): Stage0Answers спрашивает 6 личных сигналов, реально читаются дальше только 4. Не автофиксер и не точный dataflow-анализ (не считает деструктуризацию const { field } = obj, не отличает "читается в логике" от "читается только чтобы положить в другой объект") — использует TypeScript compiler API (не голый grep — иначе частые имена вроде model/year тонули бы в шуме несвязанных совпадений на других типах) для точного подсчёта .field-обращений именно к целевым интерфейсам (Stage0Answers/AiVerdict/SearchProfile — выбраны как "интерфейсы-приёмники входа", не Car/lib/db.ts-хранилища, где обилие легитимно "только отображаемых" полей дало бы в основном шум). Печатает поля от наименее используемых снаружи — начинать ручной разбор оттуда. Запуск: npx tsx scripts/find-unread-fields.ts.

    Первый живой прогон (Strategic-5) нашёл новый, ранее не задокументированный кандидат: SearchProfile.city (город/регион, где ищут машину) пишется при создании/обновлении профиля (insertSearchProfile/updateSearchProfile, lib/db.ts), но не читается НИ РАЗУ во всей логике скоринга/рыночной оценки — тот же класс пробела, что уже описан для Car.city в исходном плане (P5, «Регион нигде не учитывается», сознательно не взят в работу в этом заходе). Изменений не внесено — кандидат для следующего раунда аудита, не для этого пакета.

Итог раунда «Ремонт методологии» (2026-08-14)

13 пакетов (P1-P4, P6-pilot, Q1-Q5, Strategic-2/3/4/5), см. CHANGELOG.md v5.195.0-v5.206.1. Полная сверка с исходным лендингом аудита:

  • P1-P6 — все закрыты, кроме P5 (регион/город, сознательно не взят в работу — не баг, а неизмеренная ось персонализации, лендинг сам рекомендует сначала сверить с реальными интервью пользователей, не гадать) и половины P6 (взят узкий pilot — только порог belowMarketRisk, не полная рыночная калибровка recommendedPrice, по явному выбору владельца).
  • Q1-Q6 — все закрыты, кроме Q6 (штраф ДТП не растёт после потолка — сознательно не взят: сам лендинг называет его низким приоритетом, "сначала посмотреть по логам, как часто это случается"). Q4 — взята только вторая рекомендация (снижение порога автоисследования 3→2); первая (подставлять среднее по известным моделям вместо 0 для необследованных) не взята — та же категория решения уровня всей формулы, что и baseScore-AHP (см. Strategic-2 выше).
  • Strategic-1 («единый профиль покупателя, текущий через весь путь») — закрыт КАК СУММА P1-P4, не отдельным пакетом: Stage0-приоритеты → criteriaWeights (P1), buyerProfile → основная формула (P2), buyerProfile → промпт ИИ (P3), готовность к самостоятельному ремонту → практический балл (P4) — вместе они и есть маршрут «квиз → профиль поиска → промпт ИИ → формула рейтинга», который просил лендинг.
  • Strategic-2/3/4/5 — закрыты как отдельные пакеты (11/12/9/13).
  • Strategic-6 («единый индикатор уверенности не только для цены, но и для итогового рейтинга — на скольких реально проверенных фактах основан балл») — сознательно не взят в этот раунд (не попал в исходно одобренный список 13 пакетов; при сверке лендинга постфактум 2026-08-15 решено не расширять уже завершённый раунд, оставить отдельным пунктом на будущее). Q1 (раздел 8.1, verdictConfidence) и Package 5 (визуальное ослабление цены при низкой уверенности) закрывают ту же идею частично — для вердикта и для цены отдельно, но не единым индикатором на итоговый балл целиком.

Ограничения методологии (честно)

  • Малая выборка для пиров (раздел 4) — обрыва на конкретном N больше нет (см. credibility-взвешивание, раздел 4), но при 2–6 машинах на модель в базе даже сглаженная оценка менее статистически надёжна, чем при полноценной рыночной регрессии; confidence/диапазон в marketComment делают это явным, а с 2026-08-10 при confidence: "low" UI прячет и сам штамп «±N% от рынка» (раздел 5).
  • Коэффициенты поправок (₽/км, ₽/владельца, вес ДТП, k в credibility-весах раздела 4) — по-прежнему эмпирические ориентиры, согласованные в разговоре с пользователем и откалиброванные на здравом смысле, не результат статистической оптимизации на большом датасете. Исключение — веса практического скоринга (раздел 5): пересчитаны AHP-инструментом 2026-08-10, это первый коэффициент в системе, прошедший структурированную проверку согласованности, а не только устное согласование. Для проверки эффекта смены коэффициентов на реальных данных — бэктест-стенд (раздел 10).
  • body/interior/style — единственные полностью субъективные поля, заполняются человеком на глаз по фото/осмотру, без формулы или проверки. Уже можно отключить полностью — enabledCriteria профиля поиска (см. раздел 1) выключает эти 3 критерия из рейтинга, если для конкретного профиля они не нужны.
  • expectedMaintenanceCost (раздел 5а) — знание на уровне модели/поколения, не диагностика конкретного экземпляра; это ожидание, а не гарантия («может понадобиться», а не «понадобится»). frequencyTier/reliabilityDamping — калиброванные приближения по языку и согласованности источников, не измеренная генеральной выборкой частота; методика 2026-07-31 честно решает конкретно проблему reporting bias (форумные жалобы без знаменателя), но не превращает оценку в статистически точную — сама по себе она по-прежнему не заменяет диагностику при осмотре.
  • aiScore — качественное суждение без формулы, дополняет, а не заменяет объективные критерии; хранится как обычное поле и может быть неточным, если исходные данные (объявление/отчёт) сами неполны или недостоверны. Систематический перекос теперь можно проверить числом — см. раздел 10.
  • Триаж отсеивает только по 4 названным критериям — не претендует на исчерпывающую проверку безопасности сделки (юридическая чистота, залоги, розыск и т.п. проверяются платным отчётом отдельно, не формулой; кандидат на пятый критерий проверен и отклонён, раздел 2).

Ответ на комментарий пользователя — «придумай алгоритм снятия этих ограничений, кажется все они решаемые»: по пунктам честно, не все одинаково решаемы кодом:

  • Малая выборка — частично решено алгоритмом (не только ростом базы): credibility-взвешивание и частичный пулинг по klass (раздел 4) убрали резкий обрыв и дали редким моделям опору на более широкий сегмент, не дожидаясь роста базы. Полноценная многофакторная регрессия по всем признакам сразу — по-прежнему вопрос объёма данных, тут исходная оценка пользователя была верна.
  • Коэффициенты не из статистики — большинство чисел (₽/км, k) по-прежнему не переоптимизированы статистически, но есть инструмент, чтобы проверять последствия их смены на реальных данных (бэктест, раздел 10) и структурировать сам процесс смены весов (AHP, раздел 10) — 2026-08-10 AHP применён впервые по-настоящему, к весам практического скоринга (раздел 5), не только описан как доступный.
  • body/interior/style субъективны и риск техобслуживания — рыночный срез, не диагностика конкретного экземпляра — согласен с пользователем, это не баги, а осознанная граница системы (первое — вкус пользователя, у формулы не должно быть мнения о дизайне; второе — статистика по модели/поколению это ровно то, что можно честно знать без диагностики конкретной машины). Оставлены как есть, без ложных попыток эти два пункта «решить» — но сама методика риска техобслуживания пересмотрена 2026-07-31 (раздел 5а) там, где нашёлся реальный изъян (reporting bias), а не там, где было принципиальное ограничение.
  • Триаж только по 4 критериям — кандидат на пятый (ГИБДД/реестр залогов, раздел 2) проверен и отклонён 2026-07-28: оба источника фактически недоступны без капчи или платного реселлера, а VIN для запроса всё равно не собирается до покупки отчёта. Перепроверено аудитом методологии 2026-08-10 — та же причина, то же решение, повторного открытия не потребовалось. Остаётся честным ограничением, не решённым алгоритмом.