Оценка: при якоре того же дома видно, что коридор сделок цену не ограничивал #3554
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3554
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/corridor-advisory-tier-a"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #3466
Условие мержа (блокирующее, из ревью)
Правка трогает
tradein-mvp/backend, поэтому deploy-tradein пересоздастtradein-scraper. Работающие обходы при этом оборвутся. Слив в самом деплое ждёт не дольше 5 минут, потом пересоздаёт контейнер всё равно (deploy-tradein.yml, «WARNING: активные scrape_runs остались после 5 мин ожидания — recreate продолжится»). Длинный обход он не спасает.Поэтому мержить только при
running = 0, и проверять это в том же блоке, что и мерж, а не заранее:Число меняется за минуты: 17.09 около 07:40 UTC было 3, в 08:45 UTC 0, в 08:48 UTC снова 1. Старый замер для решения не годится. Сам блок проверен вживую (без мержа): 08:48 UTC он напечатал
running=1и «НЕ МЕРЖИТЬ: идут обходы (1)».Что меняется для клиента
Когда цена посчитана по аналогам из того же дома (якорь Tier A), рядом с коридором сделок Росреестра теперь стоит подпись: «справочно: цена посчитана по аналогам в этом же доме — коридор её не ограничивает». Так бывает и при двадцати сделках на улице. До правки подписи не было: коридор выглядел рабочим, хотя цену не трогал, причём именно в том случае, которому клиент доверяет больше всего.
Подпись остаётся и при повторном открытии оценки по ссылке
?id=.Что было и почему
DkpCorridor.advisory_only(#3452) считается только из числа сделок. Но кламп headline выключается ещё и якорем того же дома:_apply_corridor_clampне трогает Tier A, а radius-floor работает только приanchor_tier is None. Ревьюер #3462 воспроизвёл это: n=20,advisory_only=False, headline 202 100 ₽/м² против потолка 140 000, подписи нет.За 30 дней на проде 16 живых оценок Tier A, у 3 из них цена выше коридора. Это
SELECTпоconfidence_explanation17.09.Что сделано
Признак. Отдельного поля нет, признаком служит уже существующее
AggregatedEstimate.analog_tier == "same_building"(issue предлагал «пометку на другом уровне ответа»).corridorAdvisoryNote(lib/rosreestr.ts) получила тир, и оба её вызова, v1HeroSummaryи v2mappers.resolveDealTier, его передают. Порядок проверок такой: сначала тир дома, потом размер выборки. Для Tier A подпись про дом точнее: коридор не входит в цену при любом count, а гейт Tier C к этому тиру не относится.GET-rehydrate. Раньше
analog_tierтам всегда былnull, потому что колонки под него нет. Миграции для этой группы не выдавали, поэтому якорный тир (same_building/micro_radius) теперь восстанавливается из подписи, которую пишет якорный блок. Этоestimator.analog_tier_from_explanation, та же схема, что у радиуса в #2632: писатель и читатель используют общую константуANCHOR_TIER_LABELS. Ищется вся фраза «Оценка построена по N аналогам из …», а не хвост «из того же дома»: радиусный тир S пишет рядом «(аналоги из того же дома)». На проде таких строк 8, в 4 из них есть и якорная фраза. Все 8 от 29–30.05, ни одна уже не открывается по?id=, и ни у одной из 16 живых Tier A этой пометки нет. Радиусные тиры на GET по-прежнемуnull. Ограничение помеченоponytail:: если понадобятся радиусные тиры на GET, нужна колонка.Побочный эффект на GET (не регрессия). Для 93 живых оценок Tier C GET теперь отдаёт
analog_tier = "micro_radius", раньше былоnull. Это меняет только одно место: лейбл вHeroTransparency.resolveCompsTier. Раньше он брался эвристикой по тексту («по окружению ≤500 м»), теперь берётся из поля («по ближайшему окружению ≤500 м»), смысл тот же. Опасный случай, когда эвристика читала бы у Tier C текст ghost-anchor «аналоги из того же дома» и писала «того же дома», на проде не встречается: 0 из 93. На подпись коридораmicro_radiusне влияет, для негоcorridorAdvisoryNoteведёт себя как дляnull. Других потребителейanalog_tierнет ни на фронте, ни в бэкенде (grep).Хвосты ревью #3462.
corridorAdvisoryNote:v2/__tests__/corridorAdvisoryNote.test.ts, 10 тестов по образцуdealsAsOf.test.ts. Сама функция проверена на комбинациях advisory_only true/false/undefined, headlineFromDeals и тира; ещё отдельно проверено, что подпись доезжает до плитки черезmapResultPanel. Вызов в v1HeroSummaryпроверяется рендером в__tests__/HeroSummaryCorridorNote.test.tsx, см. «Правки по ревью #3554».test_estimator_radius_floor.pyвообще не доходили до floor. Три лота меньшеHEADLINE_LISTINGS_MIN_N=5, поэтому срабатывал #oblast-E и headline брался из медианы коридора (180k), которая тоже «≥ floor». На origin/main файл зелёный даже с полностью выключенным floor (прогон ниже). Теперь в тестах шесть лотов, ожидания точные (== 120000,== 200000), есть кейсыcount = min_n − 1иcount = min_n.corridor_advisory_zoneсчитает n = 1..9, а не 3..9. Уличный коридор отдаётся уже с одной сделки: для Екатеринбурга widen не срабатывает вовсе, а вне ЕКБ city-выборка берётся только если она больше уличной. «Пол» в widen это не закрыл бы, поэтому поправлены формулировки: комментарий у лога, докстрингиadvisory_only/DkpCorridor, докстрингtest_3452. Добавлен тест:_fetch_dkp_corridorдля ЕКБ с двумя сделками возвращаетcount=2. Приёмку #3452 читать как n = 1..9.Правки по ревью #3554
Каждую находку сначала проверил сам. Опровергнутых нет, все подтвердились.
Исправлено кодом (отдельные коммиты):
HeroSummaryне был покрыт (803ea221). Проверка: убрал аргументestimate.analog_tierиз вызова вHeroSummary.tsx(мутация F2), vitest33 файла, 305 passed, rc=0. Подтверждено. Сделано: рендерHeroSummaryпо образцуthinMarketBadge.test.tsx, n=20,advisory_only=false. Приsame_buildingподпись про дом есть в тексте hero, приmicro_radiusеё нет. Та же мутация F2 теперь даёт красный, текст ниже (фальсификация 7).count == min_nне стереглась (49bcfdac). Проверка:>=→>вestimator.py,tests/test_estimator_radius_floor.pyдал4 passed, rc=0. Подтверждено. Сделано: тестtest_floor_applied_at_exactly_clamp_min_n, в нём n =estimate_corridor_clamp_min_n, медиана 80k поднимается ровно до 120k. Дополнительно в нём проверено, чтоDkpCorridorна той же границе не справочный: витрина молчит, значит floor обязан работать. Мутант>теперь красный (фальсификация 8).Исправлено в описании:
3. Пункт 3 приёмки был неточен. Проверка:
v2/mappers.resolveDealTier. ПриstreetDeals.count > 0плитка ДКП строится из street-deals сadvisoryNote: null, и коридора в v2 нет совсем.dkp_corridorрисуют толькоHeroSummaryи эта веткаmappers.ts.HeroSummaryподключён вsrc/app/page.tsx, то есть на корневой/trade-in/?id=, а не на/trade-in/v2. Подтверждено, приёмка переписана: открывать обе страницы.4. «Таких строк 4» было неверно. Проверка на проде 17.09 08:45 UTC: 8 строк с «(аналоги из того же дома)», у 4 из них есть и якорная фраза, 0 открываются по
?id=, даты 29–30.05. Живых Tier A 16, все за последние 30 дней, у 0 из них есть эта пометка. Подтверждено, текст исправлен. На разбор это не влияет.5. Не упомянут побочный эффект для Tier C на GET. Проверка на проде: 93 живых Tier C. Заодно проверил эвристику, которая раньше давала лейбл: ни одна из 93 не читалась ею как «того же дома». Подтверждено, добавлен абзац выше.
Без изменений:
6. Разбор тира по прозе хрупок. Согласен. Это уже помечено
ponytail:уANCHOR_TIER_LABELS, а ослабление regex до хвоста ловится тестом (фальсификация 2). Колонка понадобится вместе с радиусными тирами на GET.7. Условие мержа по
scrape_runsвынесено наверх описания в виде одного блока «проверить и смержить». В коде менять нечего.Тесты
pytest tests/test_3466_corridor_tier_a.py tests/test_estimate_idor.py tests/test_estimate_revival.py tests/test_estimator_radius_floor.py tests/test_estimator_search_radius_2632.py tests/test_3452_corridor_advisory_zone.py: 71 passed.DATABASE_URL=… pytest tests/ -q -p no:cacheprovider: 6233 passed, 42 skipped, rc=0 (было 6232, +1 тест по ревью).ruff check app tests: rc=0;ruff format --checkпо изменённым файлам: rc=0.tsc --noEmitrc=0,eslintнового теста rc=0,vitest run: 34 файла, 307 тестов, rc=0 (+10 в первом коммите, +2 по ревью).Фальсификация (исходник копировался в scratchpad, после каждого прогона восстановлен,
diff -qчистый)_apply_corridor_clamp(if False and anchor_tier == "A"):из {label}:analog_tier=analog_tier_from_explanation(...):count >= estimate_corridor_clamp_min_nу radius-floor:Тот же мутант с тестовым файлом из origin/main:
3 passed, rc=0.5. Floor выключен целиком (
if False and dkp_raw …): файл из origin/main3 passed, rc=0, файл веткиmedian_ppm2=80000 должна быть поднята ровно до floor=120000.0, rc=1.6. Фронт: ветка same_building в
corridorAdvisoryNoteвыключена:HeroSummary.tsxубран аргументestimate.analog_tier(F2). До нового теста весь vitest был зелёным,305 passed, rc=0. После него:>=заменён на>. До нового теста файл давал4 passed, rc=0, после:Деплой
Своей миграции нет. Merge origin/main принёс в ветку чужую
308_domclick_msk_disable_until_incremental_save.sql(#3567), но она уже в main и накатится независимо от этого PR. Правки вtradein-mvp/backendиtradein-mvp/frontend, поэтому deploy-tradein пересоздастtradein-backend,tradein-browser,tradein-tgbot,tradein-frontend(отдельной командой) иtradein-scraper(backend changed →SCRAPER_RECREATE). Гейт поscrape_runsописан в начале. Это условие мержа.Приёмка на проде (после деплоя)
docker exec tradein-backend grep -c analog_tier_from_explanation /app/app/api/v1/trade_in.py≥ 1. На 17.09 в контейнере 0.SELECT id FROM trade_in_estimates WHERE confidence_explanation ~ 'Оценка построена по [0-9]+ аналогам из того же дома' AND median_price > 0 AND (expires_at > now() OR retain_until > now()) ORDER BY created_at DESC LIMIT 3.GET /api/v1/trade-in/estimate/{id}должен отдать"analog_tier": "same_building", до деплоя тамnull. Обратное направление: у живой радиусной оценки (якорной фразы нет) остаётсяnull. Радиусных строк с «(аналоги из того же дома)», которые открываются по?id=, на проде нет (0 из 8), поэтому этот случай стережёт только тестtest_radius_same_house_note_is_not_anchor./trade-in/?id=…(v1HeroSummary): подпись про дом стоит под коридором ДКП, и в плашке «ДКП · Росреестр», и в блоке цены;/trade-in/v2?id=…: подпись есть в плитке «ДКП · Росреестр» только когда street-deals пусты. Если street-deals есть, плитка строится из них, коридора в v2 нет вовсе, и отсутствие подписи там правильно, а не провал.Обратное направление: у радиусной оценки с n ≥ 10 подписи нет ни на одной странице.
analog_tier = "micro_radius". На корневой/trade-in/?id=…в «Как рассчитано» → «Сопоставление» написано «по ближайшему окружению ≤500 м», до деплоя было «по окружению ≤500 м».Замечено попутно, не чинилось
У ghost-anchor guard (
estimator.py, «Оценка опирается только на аналоги из того же дома») текст один и тот же для Tier A и Tier C. Для Tier C (окружение ≤500 м) это неправда. Разбор тира этот текст не читает. На 17.09 у живых Tier C этого текста нет (0 из 93).🤖 Generated with Claude Code
Смысловой конфликт с #3556 (
fix/estimator-flag-collapse). Git его не покажет:git merge-treeсливает ветки чисто, а CI гоняет голову ветки, не результат слияния.#3556 удаляет поля
Settings.estimate_corridor_clamp_slackиestimate_corridor_clamp_min_n(теперь этоestimator.CORRIDOR_CLAMP_SLACKиapp.core.config.CORRIDOR_CLAMP_MIN_N, значения прежние, 0.40 и 10). Ещё он удаляет параметрradius_floor_factorу_run_estimateвtests/test_estimator_radius_floor.py.Пробное слияние
f48a510e(#3556) +8bcb025c(эта ветка), 17.09.2026:test_3466_corridor_tier_a.py test_estimator_radius_floor.py test_3452_corridor_advisory_zone.py test_1970_estimator_constants.py→ rc=1, 4 failed, 61 passed:С патчем ниже те же файлы дают 65 passed, rc=0. Весь сьют на тройном слиянии (#3556 + #3552
3992d754+ эта ветка + патч): 6318 passed, 42 skipped, rc=0.Порядок. Кто вливается вторым, тот мержит
origin/mainк себе, накладывает патч (git apply) и перепрогоняет сьют. Патч применяется к результату слияния, а не к голове этой ветки:CORRIDOR_CLAMP_MIN_Nпоявляется только с #3556. Если #3554 вольют первым, адаптацию сделаю в #3556. Три правки фронта в патче касаются только комментариев. Они здесь, потому что у #3554 фронт пересобирается в любом случае.патч (git apply на результате слияния)
bot-backend referenced this pull request2026-09-17 09:47:06 +00:00