fix(tradein): подпись коридора говорит про выборку, а не про алгоритм
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m9s
CI Trade-In / backend-tests (pull_request) Successful in 5m21s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m9s
CI Trade-In / backend-tests (pull_request) Successful in 5m21s
Ревью #3462 поймало ложь в микрокопии: «оценку по ним не корректировали» утверждает про АЛГОРИТМ то, чего код не гарантирует. Порог estimate_corridor_clamp_min_n гейтит только две страховки — кламп headline и radius-floor. Третий ценовой путь, гейт Tier C (#1795 шаг 3, estimator.py), сравнивает якорь с потолком коридора БЕЗ порога вообще: коридор из пяти сделок там способен уронить headline на треть (воспроизведено ревьюером: якорь Tier C 300 000 ₽/м², с коридором 200 000 против 300 500 без него). Плюс deals-headline-fallback берёт медиану коридора начиная с трёх сделок. Формулировка переписана на утверждение о ДАННЫХ — оно истинно во всех достижимых состояниях: «справочно: сделок мало (N) — коридор ориентировочный». Ветка «объявлений рядом нет» (n_analogs = 0) больше не молчит: раньше там возвращался null, и клиент не узнавал, что вся его цена стоит на трёх сделках. Теперь — «оценка построена на этих сделках — их всего N». Докстринги advisory_only в схеме и комментарий у лога тоже перестали обещать «коридор в цену не пошёл»: поле значит ровно «страховки выключены». Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b71f3f9957
commit
668ac40631
5 changed files with 47 additions and 34 deletions
|
|
@ -190,15 +190,22 @@ class DkpCorridor(BaseModel):
|
|||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def advisory_only(self) -> bool:
|
||||
"""#3452: True = коридор показан, но в цену не пошёл — сделок меньше порога.
|
||||
"""#3452: True = сделок меньше порога, ценовые страховки коридора выключены.
|
||||
|
||||
Порог один и тот же (`estimate_corridor_clamp_min_n`) у обоих ценовых
|
||||
путей коридора: soft-кламп headline и radius-floor (estimator.py). Ниже
|
||||
него коридор всё ещё виден клиенту и участвует в fallback-путях, но
|
||||
ценовой страховки по нему нет — зона n=3..9 на экране была неотличима от
|
||||
работающей. Производное от count, поэтому верно во ВСЕХ конструкторах
|
||||
DkpCorridor автоматически (POST /estimate и GET-rehydrate) и не
|
||||
дублирует порог вторым числом.
|
||||
Порог один и тот же (`estimate_corridor_clamp_min_n`) у обоих СТРАХОВОЧНЫХ
|
||||
путей коридора: soft-кламп headline сверху и radius-floor снизу
|
||||
(estimator.py). Ниже него коридор всё ещё виден клиенту, но не держит
|
||||
цену — зона n=3..9 на экране была неотличима от работающей.
|
||||
|
||||
ВНИМАНИЕ, поле НЕ значит «коридор в цену не вошёл»: гейт Tier C
|
||||
(#1795 шаг 3) сравнивает якорь с потолком коридора БЕЗ порога вообще, и
|
||||
deals-headline-fallback берёт медиану коридора начиная с трёх сделок.
|
||||
Потребителю (витрине) поэтому корректно говорить про РАЗМЕР ВЫБОРКИ, а
|
||||
не про то, что цену коридор не трогал.
|
||||
|
||||
Производное от count, поэтому верно во ВСЕХ конструкторах DkpCorridor
|
||||
автоматически (POST /estimate и GET-rehydrate) и не дублирует порог
|
||||
вторым числом.
|
||||
"""
|
||||
from app.core.config import settings # локально: schemas остаётся import-light
|
||||
|
||||
|
|
|
|||
|
|
@ -5194,11 +5194,12 @@ async def estimate_quality(
|
|||
# 6. Сохраняем в trade_in_estimates
|
||||
estimate_id = uuid4()
|
||||
|
||||
# #3452: коридор ДКП показан, но в цену не пошёл — сделок меньше порога
|
||||
# доверия (тот же estimate_corridor_clamp_min_n гейтит и soft-кламп headline,
|
||||
# и radius-floor). Зона n=3..9 на экране неотличима от работающего коридора,
|
||||
# поэтому попадание в неё пишется явной строкой: число оценок за сутки —
|
||||
# `docker logs <backend> --since 24h | grep -c corridor_advisory_zone`.
|
||||
# #3452: коридор ДКП показан, но его ценовые страховки выключены — сделок
|
||||
# меньше порога доверия (тот же estimate_corridor_clamp_min_n гейтит и
|
||||
# soft-кламп headline, и radius-floor; гейт Tier C порога не имеет вовсе).
|
||||
# Зона n=3..9 на экране неотличима от работающего коридора, поэтому
|
||||
# попадание в неё пишется явной строкой: число оценок за сутки —
|
||||
# `docker logs tradein-backend --since 24h 2>&1 | grep -c corridor_advisory_zone`.
|
||||
# Ровно одна строка на оценку: GET-rehydrate сюда не заходит и счёт не двоит.
|
||||
if dkp_corridor is not None and dkp_corridor.advisory_only:
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -203,11 +203,10 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
|||
// по сегодня», при том что свежайшая пачка Росреестра на проде — I кв. 2026.
|
||||
// null (оценка старше деплоя бэкенда, поля нет) → хвост просто исчезает.
|
||||
const dkpAsOf = dealsAsOfLabel(dkp?.latest_deal_date, "quarter");
|
||||
// #3452: коридор ниже порога доверия — цену по нему не поправляли (ни клампом
|
||||
// сверху, ни floor'ом снизу). Рядом с числами обязан стоять этот факт, иначе
|
||||
// они читаются как участвующие в расчёте.
|
||||
// n_analogs === 0 → headline посчитан по сделкам ДКП, а не по объявлениям
|
||||
// (та же причина, что у analogsCaption ниже) — подпись тогда не рисуется.
|
||||
// #3452: коридор собран из малого числа сделок (ниже порога доверия) — рядом
|
||||
// с числами обязан стоять размер выборки, иначе коридор читается как опора
|
||||
// расчёта. n_analogs === 0 → headline посчитан по этим же сделкам, а не по
|
||||
// объявлениям (та же причина, что у analogsCaption ниже) → подпись другая.
|
||||
const dkpAdvisory = corridorAdvisoryNote(dkp, estimate.n_analogs === 0);
|
||||
|
||||
// Progressive enrichment state
|
||||
|
|
|
|||
|
|
@ -609,10 +609,10 @@ interface DealTier {
|
|||
/** #2846: «по I кв. 2026» — as-of ТОГО набора сделок, который дал числа выше. */
|
||||
asOf: string | null;
|
||||
/**
|
||||
* #3452: подпись «справочно», если числа плитки пришли из ДКП-коридора, по
|
||||
* которому цену не поправляли (сделок меньше порога доверия). null у прочих
|
||||
* веток: street-deals и actual_deals в кламп не ходят вообще, и обещать там
|
||||
* «страховка выключена» было бы новостью не про эту выборку.
|
||||
* #3452: подпись про размер выборки, если числа плитки пришли из ДКП-коридора
|
||||
* с малым числом сделок (ниже порога доверия). null у прочих веток:
|
||||
* street-deals и actual_deals в ценовые пути коридора не ходят вообще, и
|
||||
* подпись про его выборку была бы новостью не про эти числа.
|
||||
*/
|
||||
advisoryNote: string | null;
|
||||
}
|
||||
|
|
@ -695,7 +695,7 @@ function resolveDealTier(
|
|||
// числа карточки (медиана/границы) — из коридора, у которого своя выборка.
|
||||
asOf: dealsAsOfLabel(c.latest_deal_date, "quarter"),
|
||||
// n_analogs === 0 → headline и есть медиана этого коридора (dealsOnlyPrice
|
||||
// ниже по файлу разбирает тот же случай) — подписи тогда нет.
|
||||
// ниже по файлу разбирает тот же случай) — подпись тогда прямее.
|
||||
advisoryNote: corridorAdvisoryNote(c, e.n_analogs === 0),
|
||||
};
|
||||
}
|
||||
|
|
@ -1243,7 +1243,7 @@ export function mapResultPanel(
|
|||
.join(" · ")
|
||||
: "—",
|
||||
bars: dealTier ? dealTier.bars : [],
|
||||
// #3452: «справочно» — числа из коридора, по которому цену не поправляли.
|
||||
// #3452: подпись про размер выборки коридора (страховки по нему выключены).
|
||||
note: dealTier?.advisoryNote ?? undefined,
|
||||
nav: 1,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -39,14 +39,20 @@ export function dealsAsOfLabel(
|
|||
}
|
||||
|
||||
/**
|
||||
* #3452: «справочно» под коридором ДКП — сделок меньше порога доверия.
|
||||
* #3452: подпись под коридором ДКП, собранным из малого числа сделок.
|
||||
*
|
||||
* Порог (`estimate_corridor_clamp_min_n`) считает бэкенд и отдаёт готовый
|
||||
* advisory_only: ниже него коридор ВИДЕН, но цену по нему не поправляют — ни
|
||||
* кламп headline сверху, ни radius-floor снизу. Молчание тут выдавало бы
|
||||
* advisory_only: ниже него у коридора выключены ОБЕ его ценовые страховки —
|
||||
* кламп headline сверху и radius-floor снизу. Молчание тут выдавало бы
|
||||
* выключенную страховку за работающую: на экране такой коридор ничем не
|
||||
* отличается от того, что цену действительно держит.
|
||||
*
|
||||
* Текст намеренно говорит про ДАННЫЕ (сделок мало), а НЕ про алгоритм
|
||||
* («цену по ним не корректировали»). Второе было бы неправдой: гейт Tier C
|
||||
* (#1795 шаг 3, estimator.py) сравнивает якорь с потолком коридора БЕЗ порога
|
||||
* min_n — коридор из пяти сделок там способен уронить headline на треть.
|
||||
* Утверждение про размер выборки истинно во всех достижимых состояниях.
|
||||
*
|
||||
* null (флага нет — оценка старше деплоя, либо сделок хватает) → подписи нет.
|
||||
*/
|
||||
export function corridorAdvisoryNote(
|
||||
|
|
@ -54,13 +60,13 @@ export function corridorAdvisoryNote(
|
|||
headlineFromDeals: boolean,
|
||||
): string | null {
|
||||
if (dkp?.advisory_only !== true) return null;
|
||||
// Молчим, когда headline ПОСТРОЕН из этого же коридора (deals-fallback,
|
||||
// n_analogs === 0 — похожих объявлений не нашлось): там показанная цена и
|
||||
// есть медиана этих сделок, и «оценку по ним не корректировали» было бы
|
||||
// ложью в другую сторону. Метка и лог на бэкенде при этом остаются: факт
|
||||
// «страховка клампом выключена» от источника headline не зависит.
|
||||
if (headlineFromDeals) return null;
|
||||
return "справочно: мало сделок — оценку по ним не корректировали";
|
||||
// headline ПОСТРОЕН из этого же коридора (deals-fallback, n_analogs === 0 —
|
||||
// похожих объявлений рядом не нашлось): коридор тут не сбоку от цены, он и
|
||||
// есть цена, и клиент обязан знать, на скольких сделках она стоит.
|
||||
if (headlineFromDeals) {
|
||||
return `оценка построена на этих сделках — их всего ${dkp.count}`;
|
||||
}
|
||||
return `справочно: сделок мало (${dkp.count}) — коридор ориентировочный`;
|
||||
}
|
||||
|
||||
export async function openRosreestrWithAddress(address: string): Promise<void> {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue