МЕРА/оценщик: диапазон якоря закреплён тестом на FSD_K, мёртвый гейт gate_thin удалён, недостижимость guardrail под инвариантом (#3590) #3593

Open
bot-backend wants to merge 1 commit from fix/3590-fsd-k-test-dead-branches into main
Collaborator

Выделено из пост-мерж ревью #2381 (PR #3556). Поведение на проде не меняется: гейт регрессии зелёный, baseline не перегенерировался.

1. FSD_K закреплён поведенческим тестом

tests/test_same_building_anchor.py::test_estimate_anchor_range_halfwidth_is_fsd_k_times_fsd: _compute_same_building_anchor подменён якорем с fsd = 0.10 и нулевым спредом комплов (comp_min = comp_max = anchor), чтобы диапазон задавал только FSD. Затем через estimate_quality проверяется полуширина range_low_rub и range_high_rub относительно median_price_rub: она должна быть ровно 1.65 × fsd.

Ожидание задано литералом 1.65, а не импортом FSD_K. Импорт поехал бы вместе с мутацией, и тест остался бы зелёным. Поэтому тест краснеет и на правке формулы в месте вызова (half = 1.5 * fsd при нетронутой константе, мутация M3 ниже), где закрепление значения в test_1970_estimator_constants молчит. Baseline гейта не трогал.

2. SB_GUARDRAIL_TOL: оставлен и помечен как недостижимый

Комментарий в estimator.py у PREMIUM GUARDRAIL: «недостижимо по построению: est_ppm2 ≥ comp_min». Цена якоря — это взвешенное среднее с неотрицательными весами, медиана или p70 того же пула, а comp_min_ppm2 — минимум этого пула. Сама защита не тронута.

3. gate_thin удалён вместе с SB_GATE_MIN_N и SB_GATE_MAX_FSD

Условие n < 3 И FSD > 0.20 недостижимо дважды:

  • _compute_same_building_anchor возвращает якорь только при n ≥ min_comps, а движок передаёт SB_MIN_COMPS = 4;
  • даже если снять первое ограничение (тесты с SB_MIN_COMPS = 1), FSD > 0.20 в той же функции даёт confidence = "low", и уже срабатывает gate_low.

Контракт ответа не затронут: gate_thin был локальной переменной и выводился только в строку лога, в схеме и во фронтенде его нет. Поиск по app/, scripts/, tests/, packages/, фронтенду и backend/ нашёл только сам estimator и тесты. Поэтому ветку удалил, а не оставил с комментарием. Инвариант SB_MIN_COMPS ≥ SB_GATE_MIN_N теперь не нужен: констант больше нет. Вместо него тест проверяет то, на чём держится удаление.

Тавтологичный test_fix1_thin_n_high_fsd_triggers_gate (строил якорь с min_comps=1 и копировал формулу гейта) заменён на test_fix1_anchor_invariants_make_thin_gate_and_guardrail_dead. Он прогоняет шесть пулов с боевыми параметрами (тиры A, C и прочий; uplift к p70; MAD-клип на малой выборке; равные цены; вес на самом дешёвом компле; площадь не у всех комплов) и проверяет три инварианта:

  1. n ≥ SB_MIN_COMPS;
  2. FSD > 0.20 ⇒ confidence == "low";
  3. anchor_ppm2 ≥ comp_min_ppm2 (это пункт 2 задачи).

Чтобы тест не проходил вхолостую, отдельно проверяется, что хотя бы один пул прошёл через uplift, хотя бы один через клип и хотя бы у одного FSD > 0.20. Эта проверка при написании поймала мой же пул: в «Хохрякова 48» uplift на самом деле не срабатывает, пришлось добавить отдельный пул. Кроме того, три компла дают None.

В test_1970_estimator_constants.py строки SB_GATE_MIN_N и SB_GATE_MAX_FSD убраны из сверки значений. Поля estimate_sb_gate_min_n и estimate_sb_gate_max_fsd добавлены в проверку «не вернулись в Settings».

4. content.ts

В комментарии estimate_imv_blend_weight заменено на IMV_BLEND_WEIGHT (одна строка, prettier не запускался).

5. Прогоны

  • tests/test_backtest_regression_gate.py: 2 passed, rc=0, baseline не перегенерировался.
  • Весь сьют tradein-mvp/backend (DATABASE_URL=postgresql+psycopg://test:test@localhost:5432/test uv run pytest -q -rs -p no:cacheprovider): 6464 passed, 72 skipped, rc=0. Все пропуски из-за окружения: 71 — нет Postgres на localhost:5432 (56 «no reachable Postgres test DB», 9 «нет доступной Postgres test-БД», 5 «живой Postgres недоступен», 1 «no reachable PostGIS test DB»), 1 — нет нативных библиотек WeasyPrint. Тестов оценщика, якоря или бэктеста среди пропущенных нет.
  • ruff check и ruff format --check по изменённым файлам зелёные, pre-commit прошёл.

6. Фальсификация (после коммита, без stash; файл восстанавливался через git checkout HEAD --, после каждой мутации diff пустой)

Прогон: test_same_building_anchor.py, test_estimator_audit_fixes.py, test_1970_estimator_constants.py (129 тестов).

Контроль дыры: при FSD_K = 1.5 гейт test_backtest_regression_gate.py даёт 2 passed, то есть не видит мутацию.

M1 FSD_K = 1.5: 2 failed, 127 passed

E       AssertionError: 62135000
E       assert 0.015000000000000013 < 1e-06
E        +  where 0.015000000000000013 = abs((((73100000 - 62135000) / 73100000) - 0.165))
FAILED tests/test_same_building_anchor.py::test_estimate_anchor_range_halfwidth_is_fsd_k_times_fsd
FAILED tests/test_1970_estimator_constants.py::test_constant_keeps_prod_value[FSD_K]

M2 FSD_K = 2.0: 2 failed, 127 passed

E       AssertionError: 58480000
E       assert 0.035 < 1e-06
E        +  where 0.035 = abs((((73100000 - 58480000) / 73100000) - 0.165))
FAILED tests/test_same_building_anchor.py::test_estimate_anchor_range_halfwidth_is_fsd_k_times_fsd
FAILED tests/test_1970_estimator_constants.py::test_constant_keeps_prod_value[FSD_K]

M3 half = 1.5 * anchor["fsd"] в месте вызова, константа не тронута: 1 failed, 128 passed. Закрепление значения молчит, краснеет только новый тест.

E       assert 0.015000000000000013 < 1e-06
FAILED tests/test_same_building_anchor.py::test_estimate_anchor_range_halfwidth_is_fsd_k_times_fsd

M4 банд confidence elif fsd <= 0.30 (FSD > 0.20 больше не low, удаление gate_thin стало бы неверным): 2 failed, 127 passed

E           AssertionError: {'anchor_ppm2': 220000.0, 'anchor_sold_ppm2': 209000.0, 'fsd': 0.28163945352271774, 'confidence': 'medium', ...}
E           assert (0.28163945352271774 <= 0.2 or 'medium' == 'low'
FAILED tests/test_estimator_audit_fixes.py::test_fix1_low_conf_anchor_suppressed_by_gate
FAILED tests/test_estimator_audit_fixes.py::test_fix1_anchor_invariants_make_thin_gate_and_guardrail_dead

Не сделано (вне задачи)

Находка 4 из issue (дайджест выгрузки по сделкам в baseline, чтобы гейт ловил точечные изменения вроде Tier C mult=1.2) в эту задачу не входила и здесь не делалась.

Closes #3590

🤖 Generated with Claude Code

Выделено из пост-мерж ревью #2381 (PR #3556). Поведение на проде не меняется: гейт регрессии зелёный, baseline не перегенерировался. ## 1. FSD_K закреплён поведенческим тестом `tests/test_same_building_anchor.py::test_estimate_anchor_range_halfwidth_is_fsd_k_times_fsd`: `_compute_same_building_anchor` подменён якорем с `fsd = 0.10` и нулевым спредом комплов (`comp_min = comp_max = anchor`), чтобы диапазон задавал только FSD. Затем через `estimate_quality` проверяется полуширина `range_low_rub` и `range_high_rub` относительно `median_price_rub`: она должна быть ровно `1.65 × fsd`. Ожидание задано **литералом 1.65**, а не импортом `FSD_K`. Импорт поехал бы вместе с мутацией, и тест остался бы зелёным. Поэтому тест краснеет и на правке формулы в месте вызова (`half = 1.5 * fsd` при нетронутой константе, мутация M3 ниже), где закрепление значения в `test_1970_estimator_constants` молчит. Baseline гейта не трогал. ## 2. SB_GUARDRAIL_TOL: оставлен и помечен как недостижимый Комментарий в `estimator.py` у PREMIUM GUARDRAIL: «недостижимо по построению: est_ppm2 ≥ comp_min». Цена якоря — это взвешенное среднее с неотрицательными весами, медиана или p70 того же пула, а `comp_min_ppm2` — минимум этого пула. Сама защита не тронута. ## 3. gate_thin удалён вместе с SB_GATE_MIN_N и SB_GATE_MAX_FSD Условие `n < 3 И FSD > 0.20` недостижимо **дважды**: - `_compute_same_building_anchor` возвращает якорь только при `n ≥ min_comps`, а движок передаёт `SB_MIN_COMPS = 4`; - даже если снять первое ограничение (тесты с `SB_MIN_COMPS = 1`), `FSD > 0.20` в той же функции даёт `confidence = "low"`, и уже срабатывает `gate_low`. Контракт ответа не затронут: `gate_thin` был локальной переменной и выводился только в строку лога, в схеме и во фронтенде его нет. Поиск по `app/`, `scripts/`, `tests/`, `packages/`, фронтенду и `backend/` нашёл только сам estimator и тесты. Поэтому ветку удалил, а не оставил с комментарием. Инвариант `SB_MIN_COMPS ≥ SB_GATE_MIN_N` теперь не нужен: констант больше нет. Вместо него тест проверяет то, на чём держится удаление. Тавтологичный `test_fix1_thin_n_high_fsd_triggers_gate` (строил якорь с `min_comps=1` и копировал формулу гейта) заменён на `test_fix1_anchor_invariants_make_thin_gate_and_guardrail_dead`. Он прогоняет шесть пулов с боевыми параметрами (тиры A, C и прочий; uplift к p70; MAD-клип на малой выборке; равные цены; вес на самом дешёвом компле; площадь не у всех комплов) и проверяет три инварианта: 1. `n ≥ SB_MIN_COMPS`; 2. `FSD > 0.20 ⇒ confidence == "low"`; 3. `anchor_ppm2 ≥ comp_min_ppm2` (это пункт 2 задачи). Чтобы тест не проходил вхолостую, отдельно проверяется, что хотя бы один пул прошёл через uplift, хотя бы один через клип и хотя бы у одного `FSD > 0.20`. Эта проверка при написании поймала мой же пул: в «Хохрякова 48» uplift на самом деле не срабатывает, пришлось добавить отдельный пул. Кроме того, три компла дают `None`. В `test_1970_estimator_constants.py` строки `SB_GATE_MIN_N` и `SB_GATE_MAX_FSD` убраны из сверки значений. Поля `estimate_sb_gate_min_n` и `estimate_sb_gate_max_fsd` добавлены в проверку «не вернулись в Settings». ## 4. content.ts В комментарии `estimate_imv_blend_weight` заменено на `IMV_BLEND_WEIGHT` (одна строка, prettier не запускался). ## 5. Прогоны - `tests/test_backtest_regression_gate.py`: **2 passed**, rc=0, baseline не перегенерировался. - Весь сьют `tradein-mvp/backend` (`DATABASE_URL=postgresql+psycopg://test:test@localhost:5432/test uv run pytest -q -rs -p no:cacheprovider`): **6464 passed, 72 skipped, rc=0**. Все пропуски из-за окружения: 71 — нет Postgres на localhost:5432 (56 «no reachable Postgres test DB», 9 «нет доступной Postgres test-БД», 5 «живой Postgres недоступен», 1 «no reachable PostGIS test DB»), 1 — нет нативных библиотек WeasyPrint. Тестов оценщика, якоря или бэктеста среди пропущенных нет. - `ruff check` и `ruff format --check` по изменённым файлам зелёные, pre-commit прошёл. ## 6. Фальсификация (после коммита, без stash; файл восстанавливался через `git checkout HEAD --`, после каждой мутации diff пустой) Прогон: `test_same_building_anchor.py`, `test_estimator_audit_fixes.py`, `test_1970_estimator_constants.py` (129 тестов). **Контроль дыры:** при `FSD_K = 1.5` гейт `test_backtest_regression_gate.py` даёт **2 passed**, то есть не видит мутацию. **M1 `FSD_K = 1.5`:** 2 failed, 127 passed ``` E AssertionError: 62135000 E assert 0.015000000000000013 < 1e-06 E + where 0.015000000000000013 = abs((((73100000 - 62135000) / 73100000) - 0.165)) FAILED tests/test_same_building_anchor.py::test_estimate_anchor_range_halfwidth_is_fsd_k_times_fsd FAILED tests/test_1970_estimator_constants.py::test_constant_keeps_prod_value[FSD_K] ``` **M2 `FSD_K = 2.0`:** 2 failed, 127 passed ``` E AssertionError: 58480000 E assert 0.035 < 1e-06 E + where 0.035 = abs((((73100000 - 58480000) / 73100000) - 0.165)) FAILED tests/test_same_building_anchor.py::test_estimate_anchor_range_halfwidth_is_fsd_k_times_fsd FAILED tests/test_1970_estimator_constants.py::test_constant_keeps_prod_value[FSD_K] ``` **M3 `half = 1.5 * anchor["fsd"]` в месте вызова, константа не тронута:** 1 failed, 128 passed. Закрепление значения молчит, краснеет только новый тест. ``` E assert 0.015000000000000013 < 1e-06 FAILED tests/test_same_building_anchor.py::test_estimate_anchor_range_halfwidth_is_fsd_k_times_fsd ``` **M4 банд confidence `elif fsd <= 0.30` (FSD > 0.20 больше не low, удаление gate_thin стало бы неверным):** 2 failed, 127 passed ``` E AssertionError: {'anchor_ppm2': 220000.0, 'anchor_sold_ppm2': 209000.0, 'fsd': 0.28163945352271774, 'confidence': 'medium', ...} E assert (0.28163945352271774 <= 0.2 or 'medium' == 'low' FAILED tests/test_estimator_audit_fixes.py::test_fix1_low_conf_anchor_suppressed_by_gate FAILED tests/test_estimator_audit_fixes.py::test_fix1_anchor_invariants_make_thin_gate_and_guardrail_dead ``` ## Не сделано (вне задачи) Находка 4 из issue (дайджест выгрузки по сделкам в baseline, чтобы гейт ловил точечные изменения вроде Tier C mult=1.2) в эту задачу не входила и здесь не делалась. Closes #3590 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-09-17 13:41:55 +00:00
МЕРА/оценщик: диапазон якоря закреплён тестом на FSD_K, мёртвый гейт gate_thin удалён (#3590)
Some checks failed
CI Trade-In / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 18s
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 1m57s
CI Trade-In / backend-tests (pull_request) Failing after 6m18s
66dfb030e7
FSD_K задаёт видимый клиенту диапазон range_low_rub/range_high_rub, но гейт
регрессии его не видит: FSD_K=1.5 меняла 857 сделок из 1600 при зелёном гейте.
Новый тест подменяет _compute_same_building_anchor якорем с известным fsd и
нулевым спредом комплов и проверяет полуширину 1.65 × fsd на результате
estimate_quality. Ожидание задано литералом, а не импортом FSD_K: импорт
поехал бы вместе с мутацией.

gate_thin («n < 3 И FSD > 0.20») недостижим дважды: якорь возвращается только
при n ≥ SB_MIN_COMPS = 4, а FSD > 0.20 и так даёт confidence="low", то есть
срабатывает gate_low. В ответ API флаг не уходил (только в лог), поэтому ветка
удалена вместе с SB_GATE_MIN_N и SB_GATE_MAX_FSD. Поля Settings остаются в
проверке «не вернулись».

SB_GUARDRAIL_TOL оставлен и помечен как недостижимый по построению: цена якоря
есть взвешенное среднее, медиана или p70 того же пула, так что
est_ppm2 ≥ comp_min.

Тавтологичный test_fix1_thin_n_high_fsd_triggers_gate (min_comps=1 и копия
формулы) заменён тестом инвариантов на шести пулах: n ≥ SB_MIN_COMPS,
FSD > 0.20 ⇒ low, anchor_ppm2 ≥ comp_min_ppm2. Покрытие ветвей uplift, клипа и
FSD > 0.20 проверяется явно.

В комментарии content.ts старое имя estimate_imv_blend_weight → IMV_BLEND_WEIGHT.

Поведение на проде не меняется: гейт регрессии зелёный без перегенерации
baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Some checks failed
CI Trade-In / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 18s
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 1m57s
CI Trade-In / backend-tests (pull_request) Failing after 6m18s
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/3590-fsd-k-test-dead-branches:fix/3590-fsd-k-test-dead-branches
git checkout fix/3590-fsd-k-test-dead-branches

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git checkout main
git merge --no-ff fix/3590-fsd-k-test-dead-branches
git checkout fix/3590-fsd-k-test-dead-branches
git rebase main
git checkout main
git merge --ff-only fix/3590-fsd-k-test-dead-branches
git checkout fix/3590-fsd-k-test-dead-branches
git rebase main
git checkout main
git merge --no-ff fix/3590-fsd-k-test-dead-branches
git checkout main
git merge --squash fix/3590-fsd-k-test-dead-branches
git checkout main
git merge --ff-only fix/3590-fsd-k-test-dead-branches
git checkout main
git merge fix/3590-fsd-k-test-dead-branches
git push origin main
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3593
No description provided.