fix(tradein/yandex): снести признак «панорама» — его нет на площадке #2851

Merged
bot-backend merged 3 commits from fix/2674-has-panorama into main 2026-08-12 20:44:12 +00:00
Collaborator

Summary

Вердикт разбора: НЕПРИМЕНИМО → УДАЛИТЬ. has_panorama разбирался, писался и обещался публичным контрактом market.v_houses, но положительных значений не имел ни одного.

Числа (прод, 13.08.2026):

срез значение
external_valuations source=yandex_valuation, страниц за 24.05–12.08 1536
из них raw_payload->'house'->>'has_panorama' = 'true' 0
= 'false' 1536
ключ отсутствует 0
houses: строк / непустых / true 9468 / 12 / 0

Живая проверка боевым трактом 13.08.2026. curl_cffi impersonate=chrome120 + прод env-прокси, RealScraperConfig + get_scraper_delay — ровно как estimator.py:879; proxy_provider=None, пул не задействован, в БД не писал. Три адреса Екатеринбурга, все HTTP 200:

  • Советская 51 — HTML 1 191 929 б (body 500 120)
  • Парина 46/5 — 1 185 458 б (body 412 660)
  • Сурикова 47 — body 438 313

Вхождений «анорам» без учёта регистра в полном HTML: 0, 0, 0. Равно как panorama / Panorama / 3D-тур / Виртуальн / Street — 0. Мета при этом разобралась целиком: 1974/9эт/панель/2,50/46 объектов · 2020/18эт · 1977/5эт/кирпич/184 объекта.

Словарь удобств дома на странице: «Дом 1974 года · 9 этажей · Панельное здание · 2,50 м потолки · Газ · Лифт · Мусоропровод», причём с явными отрицаниями — «Лифт отсутствует», «Мусоропровода нет». Будь панорама признаком дома, она печаталась бы в этом же ряду и в отрицательной форме.

Природа нуля — третий вид: неприменимо. Не «метка переехала»: слова нет ни в тексте, ни в атрибутах, ни в data-*, ни в JSON-стейте — документ проверен целиком, а не только body.text. Ноль был механически гарантирован самим кодом ("Панорама" in body_text по тексту, где такого слова не бывает) и о предметной области говорил ровно одно: измерять нечего.

Опровергнуто в исходной формулировке задачи. «Соседний has_lift — готовая контрольная группа, он доказывает, что разбор работает» — неточно. has_lift = "Лифт" in body_text ловит подстроку внутри отрицания: живая Сурикова 47 (5 этажей, 1977 г.) печатает «Лифт отсутствует», парсер отдаёт True. На проде 300 из 313 пятиэтажек значатся «с лифтом», и 1370 из 1402 отрисованных страниц вообще. Как контроль «текст читается» он годится (это и подтвердилось), как контроль корректности разбора — нет; его 1241/1326 завышены. Гейт отрицания для has_liftотдельной однострочной правкой, в этот PR не смешан.

Тесты на панораму были тавтологичны: test_parse_house_meta_with_panorama кормил парсеру строку «7 объектов Дом 2010 года Панорама Лифт», собранную самим автором. Соседний test_parse_house_meta_full, наоборот, задаёт полный блок дома вообще без панорамы и ждёт False — автор фикстуры уже знал реальный словарь страницы.

«В справочнике 12 из 12 — нет» читалось как второй дефект; на деле путь записи _save_yandex_house_panorama влит 2026-08-06 (f5b39e6f) и просто не успел набрать объём.

Что сделано

Снос сквозь 9 файлов + миграция:

  • packages/scraper-kit/.../yandex/valuation.py — поле ValuationHouseMeta.has_panorama + строка разбора
  • app/services/estimator.py_save_yandex_house_panorama и её вызов (порядок «резолв дома до раннего возврата» сохранён — остаточный эффект переноса реален: дом и house_sources для страниц без истории)
  • app/services/matching/conflict_resolution.py — ключ в HOUSE_FIELD_PRIORITY
  • 6 тестовых файлов (5 тестов удалено, 3 счётчика коммитов поправлены, 2 гейта заведены)
  • data/sql/259_houses_drop_has_panorama.sql

Цена: ломаем публичный контракт

has_panorama входит в market.v_houses (мигр. 154), где сказано прямым текстом: «adding a column later is backward compatible, renaming/removing one is not». Изменение ломающее и принято осознанно — читателей вне tradein-mvp нет (git grep пуст, включая newbuilding_crossload.py, единственный живой консьюмер контракта).

CREATE OR REPLACE VIEW колонку удалить не умеет → DROP VIEWDROP COLUMNCREATE VIEW. Порядок обязателен ещё и потому, что DROP COLUMN без CASCADE упрётся в зависимость view (на проде это единственный зависимый объект). CASCADE не используем.

DROP VIEW уничтожает гранты — на проде на market.v_houses висит GRANT SELECT для gendesign_reader (мигр. 155); восстановлен явно. Без этой строки внешний ETL получил бы permission denied на следующем прогоне. Новый список колонок сверен с прод-information_schema: 60 → 59, порядок идентичен, разница ровно одна колонка.

SET LOCAL lock_timeout = '5s'scripts/check-migration-lock-timeout.py ✓ (блокирующий DDL прикрыт lock_timeout (проверено новых миграций: 9)).

Красный прогон

Снос измерителя нельзя проверить самим измерителем, поэтому гейт — регрессионный: имя не должно вернуться в боевой код, а миграция обязана снести колонку, пересобрать контракт и вернуть грант.

На origin/main (те же тесты, код нетронут) — 3 failed:

E  AssertionError: удалённое снова упоминается:
   tradein-mvp/backend/app/services/estimator.py: has_panorama;
   tradein-mvp/backend/app/services/matching/conflict_resolution.py: has_panorama;
   tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/yandex/valuation.py: has_panorama
E  FileNotFoundError: .../data/sql/259_houses_drop_has_panorama.sql
E  AssertionError: assert 'has_panorama' not in {...HOUSE_FIELD_PRIORITY...}

На ветке — 124 passed (6 затронутых файлов), полный бэкенд-прогон 4317 passed, 21 skipped.

Гейт умеет краснеть и после фикса (проверено фальсификацией на ветке): вернул ключ в HOUSE_FIELD_PRIORITY и убрал GRANT из миграции → те же 3 теста снова red, с осмысленными сообщениями («DROP VIEW уничтожает гранты — без явного GRANT внешний ETL получит permission denied»). Порча откачена.

Оговорка честности

Сырой HTML прошлых сборов не хранится (raw_payload держит только body_len/items_count), поэтому утверждение «метка была и исчезла в мае» доказательно не опровергается. Но и положительных за всё окно 1536 страниц ноль — в измеренной истории её тоже не было.

Test plan

  • check-migration-lock-timeout.py — зелёный
  • красный прогон на origin/main — 3 failed, вывод выше
  • зелёный прогон на ветке — 4317 passed / 21 skipped
  • фальсификация гейта на ветке — краснеет
  • пост-деплой: _schema_migrations содержит 259_houses_drop_has_panorama.sql; information_schema.columns по houses без has_panorama; market.v_houses = 59 колонок; role_table_grantsgendesign_reader снова с SELECT на market.v_houses

Refs #2674

## Summary Вердикт разбора: **НЕПРИМЕНИМО → УДАЛИТЬ**. `has_panorama` разбирался, писался и обещался публичным контрактом `market.v_houses`, но положительных значений не имел ни одного. **Числа (прод, 13.08.2026):** | срез | значение | |---|---| | `external_valuations` source=`yandex_valuation`, страниц за 24.05–12.08 | 1536 | | из них `raw_payload->'house'->>'has_panorama' = 'true'` | **0** | | `= 'false'` | 1536 | | ключ отсутствует | 0 | | `houses`: строк / непустых / true | 9468 / 12 / **0** | **Живая проверка боевым трактом 13.08.2026.** curl_cffi `impersonate=chrome120` + прод env-прокси, `RealScraperConfig` + `get_scraper_delay` — ровно как `estimator.py:879`; `proxy_provider=None`, пул не задействован, **в БД не писал**. Три адреса Екатеринбурга, все HTTP 200: - Советская 51 — HTML 1 191 929 б (body 500 120) - Парина 46/5 — 1 185 458 б (body 412 660) - Сурикова 47 — body 438 313 Вхождений «анорам» без учёта регистра в **полном** HTML: **0, 0, 0**. Равно как `panorama` / `Panorama` / `3D-тур` / `Виртуальн` / `Street` — 0. Мета при этом разобралась целиком: 1974/9эт/панель/2,50/46 объектов · 2020/18эт · 1977/5эт/кирпич/184 объекта. Словарь удобств дома на странице: «Дом 1974 года · 9 этажей · Панельное здание · 2,50 м потолки · Газ · Лифт · Мусоропровод», причём с **явными отрицаниями** — «Лифт отсутствует», «Мусоропровода нет». Будь панорама признаком дома, она печаталась бы в этом же ряду и в отрицательной форме. **Природа нуля — третий вид: неприменимо.** Не «метка переехала»: слова нет ни в тексте, ни в атрибутах, ни в data-\*, ни в JSON-стейте — документ проверен целиком, а не только `body.text`. Ноль был механически гарантирован самим кодом (`"Панорама" in body_text` по тексту, где такого слова не бывает) и о предметной области говорил ровно одно: измерять нечего. **Опровергнуто в исходной формулировке задачи.** «Соседний has_lift — готовая контрольная группа, он доказывает, что разбор работает» — неточно. `has_lift = "Лифт" in body_text` ловит подстроку внутри отрицания: живая Сурикова 47 (5 этажей, 1977 г.) печатает «Лифт отсутствует», парсер отдаёт `True`. На проде **300 из 313** пятиэтажек значатся «с лифтом», и 1370 из 1402 отрисованных страниц вообще. Как контроль «текст читается» он годится (это и подтвердилось), как контроль **корректности** разбора — нет; его 1241/1326 завышены. Гейт отрицания для `has_lift` — **отдельной однострочной правкой, в этот PR не смешан.** Тесты на панораму были тавтологичны: `test_parse_house_meta_with_panorama` кормил парсеру строку «7 объектов Дом 2010 года Панорама Лифт», собранную самим автором. Соседний `test_parse_house_meta_full`, наоборот, задаёт полный блок дома вообще без панорамы и ждёт `False` — автор фикстуры уже знал реальный словарь страницы. «В справочнике 12 из 12 — нет» читалось как второй дефект; на деле путь записи `_save_yandex_house_panorama` влит 2026-08-06 (f5b39e6f) и просто не успел набрать объём. ## Что сделано Снос сквозь 9 файлов + миграция: - `packages/scraper-kit/.../yandex/valuation.py` — поле `ValuationHouseMeta.has_panorama` + строка разбора - `app/services/estimator.py` — `_save_yandex_house_panorama` и её вызов (порядок «резолв дома до раннего возврата» **сохранён** — остаточный эффект переноса реален: дом и `house_sources` для страниц без истории) - `app/services/matching/conflict_resolution.py` — ключ в `HOUSE_FIELD_PRIORITY` - 6 тестовых файлов (5 тестов удалено, 3 счётчика коммитов поправлены, 2 гейта заведены) - `data/sql/259_houses_drop_has_panorama.sql` ## Цена: ломаем публичный контракт `has_panorama` входит в `market.v_houses` (мигр. 154), где сказано прямым текстом: «adding a column later is backward compatible, renaming/removing one is not». Изменение ломающее и принято осознанно — читателей вне `tradein-mvp` нет (`git grep` пуст, включая `newbuilding_crossload.py`, единственный живой консьюмер контракта). `CREATE OR REPLACE VIEW` колонку удалить не умеет → `DROP VIEW` → `DROP COLUMN` → `CREATE VIEW`. Порядок обязателен ещё и потому, что `DROP COLUMN` без `CASCADE` упрётся в зависимость view (на проде это **единственный** зависимый объект). `CASCADE` не используем. **`DROP VIEW` уничтожает гранты** — на проде на `market.v_houses` висит `GRANT SELECT` для `gendesign_reader` (мигр. 155); восстановлен явно. Без этой строки внешний ETL получил бы permission denied на следующем прогоне. Новый список колонок сверен с прод-`information_schema`: 60 → 59, порядок идентичен, разница ровно одна колонка. `SET LOCAL lock_timeout = '5s'` — `scripts/check-migration-lock-timeout.py` ✓ (`блокирующий DDL прикрыт lock_timeout (проверено новых миграций: 9)`). ## Красный прогон Снос измерителя нельзя проверить самим измерителем, поэтому гейт — регрессионный: имя не должно вернуться в боевой код, а миграция обязана снести колонку, пересобрать контракт и вернуть грант. **На `origin/main` (те же тесты, код нетронут) — 3 failed:** ``` E AssertionError: удалённое снова упоминается: tradein-mvp/backend/app/services/estimator.py: has_panorama; tradein-mvp/backend/app/services/matching/conflict_resolution.py: has_panorama; tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/yandex/valuation.py: has_panorama E FileNotFoundError: .../data/sql/259_houses_drop_has_panorama.sql E AssertionError: assert 'has_panorama' not in {...HOUSE_FIELD_PRIORITY...} ``` **На ветке — 124 passed** (6 затронутых файлов), полный бэкенд-прогон **4317 passed, 21 skipped**. **Гейт умеет краснеть и после фикса** (проверено фальсификацией на ветке): вернул ключ в `HOUSE_FIELD_PRIORITY` и убрал `GRANT` из миграции → те же 3 теста снова red, с осмысленными сообщениями («DROP VIEW уничтожает гранты — без явного GRANT внешний ETL получит permission denied»). Порча откачена. ## Оговорка честности Сырой HTML прошлых сборов не хранится (`raw_payload` держит только `body_len`/`items_count`), поэтому утверждение «метка была и исчезла в мае» доказательно **не опровергается**. Но и положительных за всё окно 1536 страниц ноль — в измеренной истории её тоже не было. ## Test plan - [x] `check-migration-lock-timeout.py` — зелёный - [x] красный прогон на `origin/main` — 3 failed, вывод выше - [x] зелёный прогон на ветке — 4317 passed / 21 skipped - [x] фальсификация гейта на ветке — краснеет - [ ] пост-деплой: `_schema_migrations` содержит `259_houses_drop_has_panorama.sql`; `information_schema.columns` по `houses` без `has_panorama`; `market.v_houses` = 59 колонок; `role_table_grants` — `gendesign_reader` снова с SELECT на `market.v_houses` Refs #2674
bot-backend added 1 commit 2026-08-12 20:05:21 +00:00
fix(tradein/yandex): снести признак «панорама» — его нет на площадке
All checks were successful
CI Trade-In / backend-tests (pull_request) Successful in 4m30s
CI / changes (pull_request) Successful in 11s
CI Trade-In / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
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
3e30b97d6b
has_panorama разбирался, писался и обещался публичным контрактом
market.v_houses, но положительных значений не имел ни одного: 0 true из
1536 страниц yandex_valuation за 24.05–12.08.2026 (false 1536, ключ
отсутствует 0), houses — 12 непустых из 9468, все false.

Соседний has_lift выглядел контрольной группой («текст читается, разбор
работает»), но контролирует он только факт отрисовки страницы: "Лифт" in
body_text ловит подстроку внутри отрицания, и на живой пятиэтажке 1977 г.
со строкой «Лифт отсутствует» парсер отдаёт True. На проде 300 из 313
пятиэтажек «с лифтом». Гейт отрицания для has_lift — отдельной правкой,
здесь не смешиваю.

Живая проверка боевым трактом 13.08.2026 (curl_cffi impersonate=chrome120
+ прод-прокси, RealScraperConfig + get_scraper_delay — ровно как
estimator.py; только чтение, в БД не писал): три адреса Екатеринбурга,
все HTTP 200, полный HTML ~1.19 МБ каждый. Вхождений «анорам» без учёта
регистра — 0/0/0, равно как panorama / 3D-тур / Виртуальн / Street.
Мета при этом разобралась целиком (1974/9эт/панель/2,50/46 объектов;
2020/18эт; 1977/5эт/кирпич/184 объекта). Метка не «переехала» в атрибут
или JSON-стейт — её нет в документе вовсе.

Это третий вид нуля: не «уже починено» и не «заблокировано выше по
потоку», а НЕПРИМЕНИМО к предметной области. Ноль был механически
гарантирован самим кодом — поиск подстроки по тексту, где такого слова не
бывает, — и о доме не говорил ничего.

Цена: снос ломает публичный контракт market.v_houses (мигр. 154 прямо
называет удаление колонки ломающим изменением). Принято осознанно —
читателей вне tradein-mvp нет (git grep пуст, в т.ч. newbuilding_crossload.py).
CREATE OR REPLACE VIEW колонку не удаляет, поэтому DROP VIEW → DROP COLUMN
→ CREATE VIEW, и вместе с DROP VIEW теряются гранты: GRANT SELECT для
gendesign_reader восстановлен явно.

Оговорка честности: сырой HTML прошлых сборов не хранится (raw_payload
держит только body_len/items_count), поэтому «метка была и исчезла в мае»
доказательно не опровергается — но и в измеренной истории 1536 страниц
положительных ноль.

Refs #2674
Light1YT added 2 commits 2026-08-12 20:38:41 +00:00
chore(tradein/db): миграция панорамы 259 → 260 — номер занят соседним PR
All checks were successful
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m20s
b96b74b79d
#2850 (снятие показателя «доля с кадастром») смержен первым и занял 259.
Git такую коллизию не видит: имена файлов разные, конфликта нет, а номер один.
Ровно этот случай ловит гейт номеров миграций (#2786, ждёт владельца).

Обновлено вместе с файлом: ссылка в tests/test_dead_code_sweep_2674.py и
запись в _manifest_applied.txt.
bot-backend merged commit 17d558b18c into main 2026-08-12 20:44:12 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#2851
No description provided.