МЕРА: настройки оценщика, которые никогда не менялись, стали кодом — 4 кластера флагов #1970 #3556
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#3556
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/estimator-flag-collapse"
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?
Четыре подзадачи #1970 одной веткой, по коммиту на issue. Смысл у всех один: поля
estimate_*вSettings, которые формально переопределялись через ENV, но ни разу не переопределялись, становятся кодом. Булевы выключатели схлопнуты к боевомуTrue, числовые пороги перенесены константами модуля с прежними значениями.Порядок мержа с #3554 (обязательно прочитать до мержа)
Ветка удаляет поля
Settingsи параметрradius_floor_factorу тестового хелпера.git merge-treeсливает её с #3552 и #3554 без текстовых конфликтов. CI в Forgejo гоняет голову ветки, а не результат слияния, поэтому смысловой конфликт при мерже не всплывёт: PR, влитый вторым, молча сделает main красным.fix/estimator-analog-quality) — уже согласован. После ревью автор #3552 убралmonkeypatch.setattr(settings, "estimate_dedup_analogs_enabled", True)(коммит1a9a612a). Проверка: пробное слияниеf48a510e(эта ветка) +3992d754(#3552), тесты 2087, 3234, гейт, 1970 и 2265 дают 164 passed, rc=0. Порядок любой.fix/corridor-advisory-tier-a) — конфликт есть. На пробном слиянииf48a510e+8bcb025cбез правок: rc=1, 4 failed, 61 passed: За AttributeError прячется второй отказ. Если подставить порог руками, оба теста radius-floor падают сTypeError: _run_estimate() got an unexpected keyword argument 'radius_floor_factor'. Патч адаптации выложен комментарием в #3554. Он переводит тесты наestimator.CORRIDOR_CLAMP_SLACKиapp.core.config.CORRIDOR_CLAMP_MIN_N, убираетradius_floor_factor=0.8и переименовывает три комментария во фронте. С патчем: те же 4 файла дают 65 passed, rc=0. Весь сьют на тройном слиянии (эта ветка + #3552 + #3554 + патч): 6318 passed, 42 skipped, rc=0.Правило: кто вливается вторым из пары #3556/#3554, тот сначала мержит
origin/mainк себе, накладывает патч и перепрогоняет сьют. Если #3554 вольют раньше, это сделаю в этой ветке.С чем сверялся
До удаления каждого поля проверил его фактическое значение на проде 17.09.2026:
settings.<поле>внутриtradein-backend,tradein-scraperиtradein-tgbotна poincare. Во всех трёх контейнерах значения совпали с дефолтами из кода.printenvне нашёл ни одной переменнойESTIMATE_*из этих кластеров (есть толькоESTIMATE_EXTERNAL_SOURCES_BACKGROUND). Файла/app/.envв контейнерах нет. Схлопывал к этим значениям.Для проверки мало гейта по агрегированным метрикам, поэтому добавил реплей замороженной фикстуры по каждой сделке: 1600 предсказаний, из них 615 на радиусном пути (median, expected_sold, диапазон, tier, confidence). После каждого из четырёх коммитов результат
cmp-идентичен main. Реплей детерминирован: два прогона на main дали один и тот же файл.Какие механизмы фикстура реально задевает (строки лога в реплее, main и эта ветка совпадают):
Ни разу не сработали квартальный индекс, гейт Tier C и кламп
ratio > 1.0. Для этих механизмов побитовое совпадение гейта ничего не доказывает, их держат юнит-тесты и новый тест значений (см. ниже).#2378 — дедуп аналогов, disclosure широкого коридора, ручная оценка
Было. Флаг
estimate_dedup_analogs_enabledостался, потому что гейт бэктеста пинил его вFalse. Тестыtest_same_building_anchorиtest_estimator_n_analogs_pricedвыключали дедуп, чтобы изолировать другой инвариант. Четыре числовых порога (wide_corridor_thresholdи триmanual_review_*) жили вSettings. ВSettingsтакже лежали осиротевшие комментарии к флагам, которые #2475 уже удалил.Почему можно убрать. С #3251/#3254 фикстура захвачена с
estimate_dedup_analogs_enabled=True, а реплей брал значение из неё. Пин в гейте был мёртвым.Сделано.
_dedup_cross_sourceработает всегда.replay_fixtureубраны пин флага и try/finally, из гейта убран monkeypatch.WIDE_CORRIDOR_THRESHOLD=1.2,MANUAL_REVIEW_HIGH_VALUE_RUB=20_000_000,MANUAL_REVIEW_WIDE_RANGE_RATIO=1.9,MANUAL_REVIEW_ELITE_PPM2=250_000._manual_reviewбольше не принимаетsettings.Grep из приёмки
estimate_(wide_corridor|confidence_floor_no_analogs|manual_review|radius_dedup|dedup_analogs)поconfig.py/estimator.pyдаёт 0 совпадений.Оговорка: старую фикстуру, захваченную с дедупом OFF, реплей больше не воспроизведёт. Она упадёт на «control flow diverged from capture», и её надо перезахватить. В репозитории лежит только свежая фикстура.
#2379 — квартальный индекс (#764)
Было. Переключатель снят ещё в #2475. Шесть числовых полей остались в
Settings:min_n_deals,match_skip_ratio,max_for_small_n,small_n_threshold,factor_min,factor_max.Сделано. Поля перенесены константами
QUARTER_INDEX_MIN_N_DEALS=10,QUARTER_MATCH_SKIP_RATIO=0.6,QUARTER_INDEX_MAX_FOR_SMALL_N=2.0,QUARTER_INDEX_SMALL_N_THRESHOLD=50,QUARTER_INDEX_FACTOR_MIN=0.6,QUARTER_INDEX_FACTOR_MAX=1.8. Комментарии #764/#859 переехали вместе с ними. Бэктест читаетm.QUARTER_INDEX_MIN_N_DEALS.Про риск из issue («проверить pure-radius путь отдельно»). Реплей побитово совпадает по всем 615 радиусным сделкам. Но квартальный индекс в нём не применяется ни разу: у сделок фикстуры нет своего кадастра цели. Мутационная проверка по одной константе за раз до ревью показала:
small_n_threshold,factor_minиfactor_maxловят поведенческие тестыtest_estimator_quarter_index.py;min_n_deals,match_skip_ratioиmax_for_small_nне ловил ни один тест.Поэтому добавлен
tests/test_1970_estimator_constants.py. В нём боевые значения, снятые с прода, и проверка, что поле не вернулось вSettings. Этот тест держит только число в константе, а не то, что код его читает (см. «Правки по ревью»: оба гарда можно было снять целиком). Теперьmin_n_deals,match_skip_ratioиmax_for_small_nловят поведенческие тестыtest_estimator_quarter_index.py, в том числе на месте вызова.#2380 — #1795: corridor-clamp, radius-floor, expected_sold ≤ asking, outlier small-n
Было. Флаги
corridor_clamp_enabledиradius_floor_enabledсняты в #2475. Остались булевestimate_expected_sold_le_askingс двумя ветками и пять числовых полей.Сделано.
if effective_ratio > 1.0и повторный кламп после хедоники.estimator.pyперенесеныCORRIDOR_CLAMP_SLACK=0.40,RADIUS_FLOOR_FACTOR=0.8,OUTLIER_SMALL_N_THRESHOLD=15,OUTLIER_TUKEY_K_SMALL=1.0.CORRIDOR_CLAMP_MIN_N=10лежит вapp.core.configрядом сLISTINGS_FRESH_DAYS. Этот же порог читаетDkpCorridor.advisory_only(#3452), а схема не должна импортировать estimator. Порог по-прежнему один.tukey_k_small < 1.5: это сравнение константы с константой.ratio=0.70: при нём0.70 × 1.30 = 0.91 < 1, и кламп не срабатывает. Мёртвый параметрradius_floor_factorвtest_estimator_radius_floorубран: все вызовы и так передавали дефолт 0.8.rosreestr.ts,types/trade-in.ts,content.ts) по-прежнему называют старые имена полей. Правка из первой версии откачена, чтобы не пересобирать фронт ради комментариев (см. «Правки по ревью», п.7).Бизнес и элит отдельно. Реплей по сделкам побитово тот же во всех сегментах по sold ₽/м²: эконом 884, комфорт 538, бизнес 169, элит 5, премиум 4.
#2381 — якорь «тот же дом» и IMV-blend
Было. Пять булевых флагов сняты в #2475 («No numeric/dict tuning constants touched»). Оставались 14 числовых полей.
Сделано. Поля перенесены константами:
IMV_BLEND_WEIGHT=0.5,IMV_BLEND_THRESHOLD=1.15,SB_MIN_COMPS=4,SB_AREA_SIGMA=0.18,SB_ROOMS_MATCH_BOOST=1.6,SB_FLOOR_SIGMA=0.25,SB_GUARDRAIL_TOL=0.05,SB_MAD_K=3.5,SB_MAD_K_SMALL_N=2.5,SB_SMALL_N_THRESHOLD=10,ANCHOR_TIER_C_CORRIDOR_MULT=1.5,FSD_K=1.65,SB_GATE_MIN_N=3,SB_GATE_MAX_FSD=0.20. Убрана мёртвая проверкаtier_c_mult > 0. Тесты подменяютSB_MIN_COMPSна модуле, а не полеsettings.git grep -E 'estimate_(imv_blend|sb_|fsd_k|anchor_tier_c)' -- tradein-mvp/backend/appдаёт 0 совпадений.Мутационная проверка по одной константе:
imv_blend_weight/threshold,sb_min_comps,sb_area_sigma,sb_mad_k,sb_mad_k_small_n,sb_small_n_thresholdиsb_gate_min_nловят гейт или поведенческие тесты;sb_rooms_match_boostиsb_floor_sigma: после ревью их ловитtest_estimate_anchor_gets_prod_weights, в том числе подмену на месте вызова;anchor_tier_c_corridor_multпри сдвиге вверх (3.0) ловитtest_estimator_price_spine::test_tier_c_corridor_gate_suppresses_anchor, при сдвиге вниз (1.2) не ловит никто, кроме теста значений. В первой версии тела было «не ловит никто», это было неверно;sb_guardrail_tol(0.10),fsd_k(1.5) иsb_gate_max_fsd(0.30) поведенческие тесты не ловят, проверено заново 17.09: 319 passed, rc=0 без теста значений. Тест значений держит только число, подмену на месте использования он не заметит.Это долг покрытия, существовавший и до правки. Перед калибровкой этих порогов на них нужны поведенческие тесты.
По условию issue перед мержем обязателен APPROVE от
deep-code-reviewer. Его нет, поэтому #2381 этот PR не закрывает (Refs, а неCloses). Код и тесты по #2381 готовы. Issue закрывается после APPROVE вручную, или строка закрытия для #2381 возвращается в тело, если APPROVE придёт до мержа.Тесты
tradein-mvp/backendна головеf48a510e(после правок по ревью и mergeorigin/main):DATABASE_URL=... uv run python -m pytest tests/ -q -p no:cacheprovider→ 6280 passed, 42 skipped, rc=0 (rc снят у самого pytest). До ревью было 6274: +6 новых тестов и параметров. На main до правки было 6225 passed, 42 skipped, rc=0.test_backtest_regression_gate.py: 2 passed. Реплей по сделкамcmp-идентичен main после каждого коммита.uv run ruff check app tests scripts: rc=0.ruff format --checkна 23 изменённых .py: 23 files already formatted, rc=0.tradein-mvp/frontendсовпадает с main.Фальсификация (красные прогоны)
Каждый раз копировал исходник в scratchpad, ломал его, прогонял тесты, восстанавливал и сверял
diff -q.#2378.
_dedup_cross_sourceсхлопнут к OFF-ветке (return lots),WIDE_CORRIDOR_THRESHOLD=1.5,MANUAL_REVIEW_HIGH_VALUE_RUB=25_000_000:#2379.
QUARTER_INDEX_MIN_N_DEALS=5,QUARTER_MATCH_SKIP_RATIO=0.85, полеestimate_quarter_index_factor_maxвозвращено вSettings:#2380. Кламп expected_sold схлопнут к OFF-ветке (
if False:в обоих местах):Затем значения:
CORRIDOR_CLAMP_MIN_N=8,CORRIDOR_CLAMP_SLACK=0.30,RADIUS_FLOOR_FACTOR=0.7:#2381.
FSD_K=1.5,SB_MIN_COMPS=3:Деплой
tradein-mvp/backend/**, пересобирается образ backend. Три комментария во фронте откачены: ради них пересобирался и пересоздавалсяtradein-frontend.tradein-backend,tradein-tgbotиtradein-scraper.tradein-frontendне трогается.scrape_runs.status='running'до 5 минут, а потом всё равно пересоздаёт scraper. Перед мержем проверить, что running-прогонов нет.Приёмка на проде (после деплоя, до 18.09.2026)
Счёт строк в логах признаком не годится. Контейнеры на poincare перезапущены 17.09 в 08:36 UTC. С тех пор в
docker logs tradein-backend --since 24h0 строкsb_anchor,imv_blend,corridor clamp,quarter_index,corridor_advisory_zoneи 0POST /api/v1/trade-in/estimate(проверено 17.09 в 09:00 UTC). Сравнение «до и после» дало бы 0 против 0 при любом исходе.tradein-backend,tradein-scraper,tradein-tgbot): Ожидается4 1.8 0.4 1.2 []. На старом коде команда падает сAttributeError: module 'app.services.estimator' has no attribute 'SB_MIN_COMPS'. Проверено наorigin/main17.09, на голове ветки вывод4 1.8 0.4 1.2 []: маркер различает оба состояния. Рядом нужна проверка образа:docker inspect -f '{{.Image}}'у всех трёх контейнеров должен совпасть с digestgendesign-tradein-backend:<sha мержа>.median_price_rub,expected_sold_priceиrange_low/high_rubдолжны совпасть. Между запусками не должен закончиться ни одинscrape_runs, иначе меняется набор аналогов. Если числа разошлись, сначала сравнить спискиanalogsв двух ответах. Оценка пишет строку вtrade_in_estimates, решение о ней за тем, кто деплоит.cmp-идентичен main после каждого коммита.Правки по ревью
Каждую находку перед правкой проверял сам. Все подтвердились, опровергнутых нет.
1. Смысловой конфликт с #3552 и #3554 (блокирующая). Подтверждено. Пробные слияния голов через
git merge-tree --write-tree+commit-treeв отдельных worktree. Для #3552 (голова08216e4c, до его правки): rc=1, 2 failed, 92 passed,AttributeError: Settings(...) has no attribute 'estimate_dedup_analogs_enabled'вtest_estimator_dedup_cross_source_2087.py:963/:981. Для #3554: rc=1, текст выше в «Порядок мержа». С тех пор #3552 исправился сам (1a9a612a), а #3554 добавил ещё один такой вызов (test_floor_applied_at_exactly_clamp_min_n). Сделано: патч для #3554 проверен на тройном слиянии (6318 passed, rc=0), выложен комментарием в #3554. Правило порядка записано в начале тела.2. Закрытие #2381 без APPROVE
deep-code-reviewer. Подтверждено. Через API у PR нет ни одного ревью. Закрывающая строка для #2381 заменена наRefs.3. Тест значений защищает число, а не использование. Подтверждено, и дыра оказалась шире. Повторил M4 (
rooms_boost=1.0на месте вызова) и M5 (min_n_deals=5на обоих местах вызова): 345 passed, rc=0. Дальше выяснилось, что Guard-2 и bimodal-гард квартального индекса можно снять целиком (if False:), иtest_estimator_quarter_index.pyостаётся зелёным (40 passed, rc=0). Оба их теста были пустыми. В Guard-2 заглушка отдавала аналогам индекс цели, получался factor=1.0. В bimodal-тесте у аналогов не было кадастра, и поправку гасил Guard-5 (#2583) при любом состоянии гарда. Это путь #2379 с риском MEDIUM. Сделано (коммитыbc7fe9b0,228c5f58):min_n_deals, тест требует[10, 10];_compute_same_building_anchorтребует боевыеsigma/rooms_boost/floor_sigma/min_comps/mad_k.Фальсификация: каждый раз копия исходника в scratchpad, правка, прогон 17 файлов PR +
test_estimator_quarter_index.py+test_backtest_fixture_roundtrip.py, восстановление,diff -qбез различий.Без
test_1970_estimator_constants.pyповеденческие тесты ловят и сдвиг самих констант.QUARTER_INDEX_MIN_N_DEALS=5,QUARTER_MATCH_SKIP_RATIO=0.85и0.5,QUARTER_INDEX_MAX_FOR_SMALL_N=4.0и1.9,SB_ROOMS_MATCH_BOOST=1.0,SB_FLOOR_SIGMA=0.5: rc=1 в каждом случае.4. Неточность про
anchor_tier_c_corridor_mult. Подтверждено. M9 (3.0): rc=1,test_tier_c_corridor_gate_suppresses_anchor. M10 (1.2): падает только тест значений. Формулировка в #2381 исправлена.5. Приёмка слабо фальсифицируема. Подтверждено. 17.09 в 09:00 UTC в логах с рестарта в 08:36 было 0 строк по всем пяти маркерам и 0 POST-оценок. Раздел приёмки переписан: основной признак — маркер импорта плюс digest образа, дальше прямое сравнение одной оценки.
6.
settings_at_captureбез калибровочных констант. Подтверждено._write_fixtureписал толькоestimate_*. Теперь туда же пишутся числовые константы модуля, 63 штуки,CORRIDOR_CLAMP_MIN_Nв том числе.replay_fixtureснимок не читает, гейт не меняется. Коммит91a075bc, тестtest_write_fixture_records_engine_constants. Фальсификация: фильтр констант заменён наif False→ rc=1,{'CORRIDOR_CLAMP_MIN_N': None} != {'CORRIDOR_CLAMP_MIN_N': 10}.7.
tradein-frontendпересобирается ради трёх комментариев. Подтверждено. Фильтрdeploy-tradein.ymlfrontend: tradein-mvp/frontend/**. Правки откачены (коммит2cfc2eda), фронт PR совпадает с main. Переименования ушли в патч для #3554: там фронт пересобирается в любом случае.Прочее. Ветка уже была запушена, поэтому вместо rebase сделан
git merge origin/main(f48a510e, подтянут9cc23c66, #3567).Closes #2378
Closes #2379
Closes #2380
Refs #2381 (закрыть после APPROVE
deep-code-reviewer)🤖 Generated with Claude Code
bot-backend referenced this pull request2026-09-17 09:47:06 +00:00