fix(tradein): word-boundary street match + cian unescape robustness (Refs #769 A1+D) #798
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
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#798
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/769-street-match-cian-unescape"
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?
Что
Две code-only части grab-bag #769 (остальные отложены, см. ниже).
Часть A1 (#10 correctness) — client-quality
address ILIKE '%' || :street || '%'давал ложные совпадения: «Мира» матчит «Адмирала Макарова» → раздувало deal/listing-аналоги, питающие оценку. Добавлен word-boundary regexaddress ~* :street_regex(\m..\M) рядом с ILIKE-prefilter (дёшево + точно) вtrade_in.py(street-deals) иestimator.py(_fetch_dkp_corridor). Bind-param, не f-string. Тронут только street-match предикат — anchor/pricing/dedup не трогались.Часть D (#16) — scraper robustness
cian_state_parserhand-rolled unescape был order-dependent (\"заменялся раньше\\→ порча) → backslash в описании ронял парс всей страницы. Заменён на атомарныйjson.loadsвнешней quoted-строки.Тесты
«Мира»≠«Адмирала Макарова» но =«Мира 5»;
street_regexуходит в db.execute; backslash-в-описании парсится (cianId сохранён).pytest -k "street or cian"→ 22 passed, ruff clean.Отложено (нужна live tradein-DB / investigation → follow-up в #769)
deals.address(нужен EXPLAIN на live).fetch_size/use_remote_estimate.refresh_search_matviewрасписание (investigate).Refs #769
✅ APPROVE
Word-boundary street-match (A1) + cian unescape (D), #769 (2 из 5, остальные deferred).
Проверил потенциальный regex-injection (street_name в
~*без escape) — unfounded:_STREET_NAME_RE(estimator.py:2536) ограничивает капчу до[0-9]/[А-Яа-яёЁ]/дефис/пробел — ни один не regex-метасимвол (дефис литерал вне char-class).r"\m"+street_name+r"\M"всегда валидный regex; invalid-regex error-путь невозможен.Part A1 —
address ~* :street_regexс PG word-boundary\m…\Mрядом с ILIKE-prefilter в street-deals и_fetch_dkp_corridor. «Мира» больше не матчит внутри «Адмирала Макарова» (м перед word-char → нет\m-границы). Bind-param (нет SQL-инъекции). Верно.Part D — order-dependent
.replace()-цепочка → escape-aware capture(?:[^"\\]|\\.)*+ двухстадийныйjson.loads(outer unquote → inner parse). Атомарно, backslash-в-описании не портит парс. Верно.Tests — backslash-desc (cianId+backslash сохранены), word-boundary (Мира≠Адмирала, =Мира 5), street_regex в params. 22 passed, ruff clean.
Deferred (A2 trgm-индекс, B FDW fetch_size, C matview-refresh, E centroid) — честно задокументированы как требующие live-DB EXPLAIN/investigation → follow-up #769. A1 чинит correctness; A2 perf отложен (correctness>perf к демо). Разумный split.
Scope/security — trade_in.py + estimator.py + cian_state_parser.py + тесты; bind-params, нет secrets, нет self-extending триггера. P3.
Verdict: ✅ APPROVE (#769 A1+D). Smoke → qa.