fix(forecasting): нормализовать dict-district в build_site_finder_report (#1130 follow-up) #1199
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#1199
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/orchestrator-district-dict-guard"
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?
Summary
Defensive нормализация
districtвbuild_site_finder_report— защита от silent-degrade при сыром dict-входе.Контекст (как нашли)
В Phase A замере #1130 (2026-06-12) мой первый тестовый скрипт скормил
analyze["district"]напрямую какdistrict=:Result:
forecasts.n_horizons=0,scenarios.n=0+ 15TypeError: unhashable type: 'dict'traceback'ов из_safe_call(compute_market_metrics(district=<dict>)→forecast_request_cache.wrapper:121→cache.get(cache_key, _MISS)гдеcache_keyсодержит dict).Прод штатно защищён ровно одним местом —
workers/tasks/forecast.py:123:Но любой другой caller (тесты, расширения чата, ad-hoc эндпоинты) этой конвенции не знает → silent degrade без warning'а в проде.
Что делает PR
_normalize_district(district: Any) -> str | None— pure нормализация на входе оркестратора:strNoneNonedictсdistrict_name: str(непустая)dictбезdistrict_name/ с пустой /dict_name=42None+logger.warningint,list, ...)None+logger.warningНикаких изменений в downstream
_build_site_finder_report_impl— только вход вbuild_site_finder_report.Test plan
TestNormalizeDistrict(str / None / dict + 4 крайних случая).pytest tests/services/forecasting/test_orchestrator.py→ 27 passed (20 existing + 7 new).ruff check+ruff format --check→ clean.forecast.py:123уже даёт str, поведение не меняется.Closes
Чип
task_4a4aa3bb(заведён 2026-06-12 после Phase A замера).Refs #1130
`analyze["district"]` в этой кодовой базе — dict вида {"district_name": "Верх-Исетский", "dist_to_center": 0.0, "median_price_per_m2": ...}. Штатный caller (`workers/tasks/forecast.py:123`) явно извлекает `district_name`: `district = row.district or analyze["district"]["district_name"]`. Но новые callers (тесты, расширения чата, ad-hoc эндпоинты) легко передают сырой dict без знания этой конвенции — тогда внутри §9.x-слоёв compute_market_metrics(district=<dict>) падает с TypeError: unhashable type: 'dict' в forecast_request_cache.wrapper, `_safe_call` это проглатывает → секции future_market.forecasts_by_horizon=[] и scenarios.by_scenario={} тихо остаются пустыми (silent degrade, не 500). Добавлен `_normalize_district(district)` — pure-нормализация на входе оркестратора: - str → как есть; - None → None; - dict с district_name (непустая строка) → извлекаем; - dict без district_name / с пустым / неподдерживаемый тип → None + logger.warning. 7 unit-тестов в test_orchestrator.py::TestNormalizeDistrict (все варианты входов). Не меняет поведение штатного caller'а (str → str), только защищает от случайных dict-callers. Discovered through: #1130 Phase A (мой первый тестовый скрипт со скормленным сырым `analyze["district"]` dict выдал forecasts.n_horizons=0 + 15 TypeError'ов в _safe_call). Закрывает чип task_4a4aa3bb. Refs #1130