fix(tradein/estimator): восстановить и канонизировать корпус дома в дедупе (регрессия #2429) #2432

Merged
lekss361 merged 1 commit from fix/tradein-dedup-corpus-format-normalize into main 2026-07-04 17:08:40 +00:00
Owner

Summary

Фикс регрессии, найденной собственной live-перезамерой сразу после мержа/деплоя PR #2429 (#2431).

Проблема: #2429 починил avito metro-glue баг, но regex-фикс ((?!\w) lookahead) отбраковывал буквенный суффикс корпуса, НЕ восстанавливая отброшенную цифру — итог house="65" для avito/cian glued-нотации («65к4») vs house="65/4" для domklik slash-нотации. guard_house видел два разных непустых значения и блокировал легитимное кросс-source слияние. На сегодняшних данных это заблокировало 226 групп — больше, чем #2429 исправил (128).

Fix: новая _normalize_house_no() канонизирует ЛЮБУЮ нотацию корпуса («65к4» глухо, «65 к4» с пробелом, «65, корпус 4» словом, «65/4» слэш) к единому «N/M» перед сравнением в guard_house. Литер (буква без цифры за ней, «24г») НЕ трогается — семантически другое (буква корпуса дома, а не номер корпуса).

Regex расширен новой альтернативой «корпус-словоформа» (пробуется ДО bare-letter альтернативы), чтобы захватить сырую корпус-подстроку ЦЕЛИКОМ («65к4», не только «65») — реформатирование в канонический вид делает отдельная функция, не сам regex (читаемее/тестируемее тремя лексическими словоформами + слэш).

Затронуто 2 return-пути _parse_street_house — основной regex-путь и bare-street-ветка (#2291), которую дизайн явно не называл, но воркер корректно нашёл и тоже нормализовал.

Live-верификация (реальные прод-данные, ДО пуша — не после)

Та же методика, что и для #2429/#2291: ssh gendesigndocker exec tradein-backend, монки-патч regex in-memory (без записи в файлы/БД), сравнение survivor-групп на ОДНОМ И ТОМ ЖЕ сегодняшнем датасете.

survivor rows
OLD (текущий прод, post-#2429) 5441
NEW (этот фикс) 5261
Разрешено 180 строк, 185/186 изменившихся кластеров полностью схлопнулись
  • Zero guard-violations по ВСЕМ 4841 кластерам (автоматическая проверка: ни один merge не объединил лоты с генуинно разными номерами корпуса).
  • 20/20 случайно выбранных "improved" кластеров вручную инспектированы воркером — все корректны.
  • Independently re-verified (я, main session, после воркера и после ревьюера): проверил regex/normalize вручную в Python REPL на реальных адресах из живой БД (ssh + psql) — «Викулова, 37к1» (cian) vs «Викулова, 37/1» (domklik) → оба «37/1» ✓; «Щербакова, 31к2Чкаловская21-30 мин.Ботаническая» (avito, corpus+metro-glue вместе) → «31/2» ✓, #2429 не регрессирован; «65, кв. 12» → «65» (не проглочен «к» из «кв.») ✓; «24г» liter не тронут ✓.
  • Честно раскрытый побочный эффект (6 кластеров, НЕ баг): один источник пишет «голый» номер дома («77»), другой — конкретный корпус («77к1») — подтверждено живыми данными (ул. Щербакова, 77к1Ботаническая avito vs улица Щербакова, 77 domklik/cian/yandex, реальная мульти-корпусная застройка). Это восстановление до-#2429 поведения (сам #2429 случайно склеивал их через усечение), не новая регрессия — архитектурный вопрос «голый номер = wildcard для любого корпуса?» вне scope, см. #2431.

Adversarial review

deep-code-reviewer: APPROVE (severity=minor). Ревьюер честно пометил liveDataVerified=false для агрегатных чисел (прод-БД для него была заблокирована песочницей) — но независимо перепроверил САМУ ЛОГИКУ regex/нормализации вручную на ~22 примерах, включая non-regression на #2423. Агрегатные live-числа я (main session) перепроверил лично через прямой SSH-доступ.

Test plan

  • test_estimator_dedup_cross_source_2087.py — 77/77, прогнал лично
  • Полный backend suite (по отчёту воркера) — 2290 passed / 1 pre-existing unrelated failure / 6 skipped
  • Stale pinning test (test_parse_street_house_glued_corpus_letter_partial_capture, пиновал баг «10к1»→«10» как норму) переписан в test_parse_street_house_glued_corpus_canonicalizes_to_slash (→«10/1»)
  • +14 новых тестов (11 по дизайну + параметризация на 4 нотации), включая guard non-regression (разные номера корпуса всё ещё НЕ сливаются) и apartment-marker edge case
  • Live pre/post перезамер на реальных прод-данных — сделан воркером ДО push, независимо перепроверен мной

Refs #2431, #2429, #2423, #2291

## Summary Фикс регрессии, найденной собственной live-перезамерой сразу после мержа/деплоя PR #2429 (#2431). **Проблема:** #2429 починил avito metro-glue баг, но regex-фикс (`(?!\w)` lookahead) отбраковывал буквенный суффикс корпуса, НЕ восстанавливая отброшенную цифру — итог `house="65"` для avito/cian glued-нотации («65к4») vs `house="65/4"` для domklik slash-нотации. `guard_house` видел два разных непустых значения и блокировал легитимное кросс-source слияние. На сегодняшних данных это заблокировало 226 групп — больше, чем #2429 исправил (128). **Fix:** новая `_normalize_house_no()` канонизирует ЛЮБУЮ нотацию корпуса («65к4» глухо, «65 к4» с пробелом, «65, корпус 4» словом, «65/4» слэш) к единому «N/M» перед сравнением в `guard_house`. Литер (буква без цифры за ней, «24г») НЕ трогается — семантически другое (буква корпуса дома, а не номер корпуса). Regex расширен новой альтернативой «корпус-словоформа» (пробуется ДО bare-letter альтернативы), чтобы захватить сырую корпус-подстроку ЦЕЛИКОМ («65к4», не только «65») — реформатирование в канонический вид делает отдельная функция, не сам regex (читаемее/тестируемее тремя лексическими словоформами + слэш). **Затронуто 2 return-пути** `_parse_street_house` — основной regex-путь и bare-street-ветка (#2291), которую дизайн явно не называл, но воркер корректно нашёл и тоже нормализовал. ## Live-верификация (реальные прод-данные, ДО пуша — не после) Та же методика, что и для #2429/#2291: `ssh gendesign` → `docker exec tradein-backend`, монки-патч regex in-memory (без записи в файлы/БД), сравнение survivor-групп на ОДНОМ И ТОМ ЖЕ сегодняшнем датасете. | | survivor rows | |---|---| | OLD (текущий прод, post-#2429) | 5441 | | NEW (этот фикс) | 5261 | | **Разрешено** | **180 строк, 185/186 изменившихся кластеров полностью схлопнулись** | - Zero guard-violations по ВСЕМ 4841 кластерам (автоматическая проверка: ни один merge не объединил лоты с генуинно разными номерами корпуса). - 20/20 случайно выбранных "improved" кластеров вручную инспектированы воркером — все корректны. - Independently re-verified (я, main session, после воркера и после ревьюера): проверил regex/normalize вручную в Python REPL на реальных адресах из живой БД (`ssh` + `psql`) — «Викулова, 37к1» (cian) vs «Викулова, 37/1» (domklik) → оба «37/1» ✓; «Щербакова, 31к2Чкаловская21-30 мин.Ботаническая» (avito, corpus+metro-glue вместе) → «31/2» ✓, #2429 не регрессирован; «65, кв. 12» → «65» (не проглочен «к» из «кв.») ✓; «24г» liter не тронут ✓. - **Честно раскрытый побочный эффект** (6 кластеров, НЕ баг): один источник пишет «голый» номер дома («77»), другой — конкретный корпус («77к1») — подтверждено живыми данными (`ул. Щербакова, 77к1Ботаническая` avito vs `улица Щербакова, 77` domklik/cian/yandex, реальная мульти-корпусная застройка). Это восстановление до-#2429 поведения (сам #2429 случайно склеивал их через усечение), не новая регрессия — архитектурный вопрос «голый номер = wildcard для любого корпуса?» вне scope, см. #2431. ## Adversarial review deep-code-reviewer: APPROVE (severity=minor). Ревьюер честно пометил `liveDataVerified=false` для агрегатных чисел (прод-БД для него была заблокирована песочницей) — но независимо перепроверил САМУ ЛОГИКУ regex/нормализации вручную на ~22 примерах, включая non-regression на #2423. Агрегатные live-числа я (main session) перепроверил лично через прямой SSH-доступ. ## Test plan - [x] `test_estimator_dedup_cross_source_2087.py` — 77/77, прогнал лично - [x] Полный backend suite (по отчёту воркера) — 2290 passed / 1 pre-existing unrelated failure / 6 skipped - [x] Stale pinning test (`test_parse_street_house_glued_corpus_letter_partial_capture`, пиновал баг «10к1»→«10» как норму) переписан в `test_parse_street_house_glued_corpus_canonicalizes_to_slash` (→«10/1») - [x] +14 новых тестов (11 по дизайну + параметризация на 4 нотации), включая guard non-regression (разные номера корпуса всё ещё НЕ сливаются) и apartment-marker edge case - [x] Live pre/post перезамер на реальных прод-данных — сделан воркером ДО push, независимо перепроверен мной Refs #2431, #2429, #2423, #2291
lekss361 added 1 commit 2026-07-04 17:07:33 +00:00
fix(tradein/estimator): восстановить и канонизировать корпус дома в дедупе (#2436)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 8s
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
CI Trade-In / backend-tests (pull_request) Successful in 50s
deffd31156
Регрессия post-#2429: _DEDUP_HOUSE_NO_RE захватывал целиком только слэш-нотацию
корпуса ("65/4"), а глухую ("65к4") и с пробелом ("65 к4") усекал до голой
цифры ("65") — #2429 лишь остановил ложный захват буквы через `(?!\w)`, но не
восстановил отброшенную цифру корпуса. guard_house в union() затем сравнивал
"65" (avito/cian) != "65/4" (domklik) для ОДНОГО и того же физического дома и
блокировал кросс-source слияние — 226 дублирующих групп в проде.

Фикс:
- _DEDUP_HOUSE_NO_RE: добавлена альтернатива для корпус-словоформы
  (к/корп/корпус, глухо или с пробелом, ТРЕБУЕТ цифру корпуса сразу за собой) —
  пробуется ДО bare-letter альтернативы, чтобы "65к4" захватывался целиком, а
  не усекался.
- Новая _normalize_house_no(): канонизирует ЛЮБУЮ нотацию корпуса ("65к4",
  "65 к4", "65, корпус 4", "65/4") к единому "65/4". Литер без цифры за буквой
  ("24г") — другой физический суффикс, НЕ трогается. Идемпотентна.
- Вызывается в единой точке — оба return _parse_street_house (включая
  bare-street ветку #2291) — так что guard_house всегда видит канонические
  значения независимо от source-формата.

Тесты: rewrite test_parse_street_house_glued_corpus_letter_partial_capture
(пиновал баг "10к1" -> "10" как норму) в
test_parse_street_house_glued_corpus_canonicalizes_to_slash ("10к1"/"10 к1" ->
"10/1"); обновлены 3 существующих кейса в test_parse_street_house_extended_formats
и stale-комментарии в 2 e2e тестах; +14 новых тестов (5 unit normalize_house_no,
parametrized cross-notation agreement, e2e slash-vs-glued merge в обе стороны
порядка union(), adversarial liter-vs-corpus non-merge, guard всё ещё блокирует
разные номера корпуса, apartment-marker "кв." не путается с "корп.", dangling
"65к" без цифры = литер).

Live-верификация на проде (read-only, house_id_fk+floor+area+price кластеры,
count(DISTINCT source)>1): 4841 кандидат-кластера / 11756 строк. OLD-логика
(текущий прод, post-#2429) 5441 survivor-строк, NEW (этот фикс) 5261 —
186 кластеров изменились, 185 полностью схлопнулись в 1 представителя (192-й
частично: 4->3). Zero true guard-violations — автоматическая проверка по всем
4841 кластерам подтвердила, что ни один мёрдж не объединил лоты с ГЕНУИННО
разными (после нормализации) номерами корпуса без авторитетного совпадения
кадастрового номера. 6 кластеров показали ПРОТИВОПОЛОЖНЫЙ эффект (было 1
survivor, стало 2) — во всех 6 один источник (обычно domklik) даёт "голый"
номер дома без корпуса ("77"), а другой — конкретный корпус ("77к1"/"77к4");
раньше баг СЛУЧАЙНО делал их равными через усечение, сейчас guard корректно
их не сливает (та же консервативная семантика "genuinely-different -> не
сливаем", что уже применяется к "65/4" vs "65/5"); отдельный,
architecturally-orthogonal вопрос (стоит ли считать голый номер wildcard'ом
для любого корпуса) — вне scope этого фикса, не блокирует.
lekss361 merged commit acc1e05357 into main 2026-07-04 17:08:40 +00:00
lekss361 deleted branch fix/tradein-dedup-corpus-format-normalize 2026-07-04 17:08:41 +00:00
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#2432
No description provided.