feat(site-finder): D4 pipeline 24mo — future competition (#36) #90
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#90
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/site-finder-pipeline-24mo-v2"
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
Closes #36 (priority/high, Demand D4 — будущая конкуренция за покупателя 24mo). Closes #19 (Конкурентный 360).
Backend —
_aggregate_pipeline()+ новый запрос кdomrf_kn_objects:ready_dt BETWEEN CURRENT_DATE AND CURRENT_DATE + 24 monthsanalyze.pipeline_24mo:objects_count,flats_totalby_class: {economy: N, comfort: M, business: K, ...}(эконом / комфорт / бизнес / премиум / элит / стандарт)by_quarter: [{quarter: "2026-Q1", objects, flats}, ...]— хронологическиseverity: low (<500 квартир) / medium (<3000) / high (≥3000) per acceptance #36top_objects[10](по flat_count desc)noteпро оптимистичность planned_commissioningFrontend —
Pipeline24moBlock.tsx:Acceptance (per issue #36)
Test plan
ruff+tsc --noEmit+lint— clean (✅ локально)Risks
⚠️ Auto-review: minor (non-blocking) — 0 блокеров, 3 must-fix-before-push + 5 для cleanup.
Должно быть починено до merge:
distance_mfalsy guard (parcels.py:343-344).if obj.get("distance_m")пропускаетround()дляdistance_m == 0.0(falsy float). Centroid на здании → rawDecimalпадает в JSON. Fix:if obj.get("distance_m") is not None.Нет GIST/spatial индекса на
domrf_kn_objects(latitude, longitude).ST_DWithinфильтр в CTE = full seq scan для каждой строки. Текущий размер (~3032 объекта) — OK, но это паттерн который уже ломал производительность. Создать issue дляdatabase-expert: добавить(latitude, longitude)или stored geography column до роста объёмов.obj_classвсегда NULL в production (известный bugBug_Kn_API_Obj_Class_Always_Null_OPEN, ~1670 NULL'ов).by_classchips в UI всегда покажут только "Не определён" — dead UI segment. Код defensively не падает ((r["obj_class"] or "unknown")), но дофиг unused UI. Отметить в vaultfixes/Bug_Kn_API_Obj_Class_Always_Null_OPEN, что D4 теперь тоже затронут.Для cleanup после merge:
CLASS_LABEL.nulldead key (Pipeline24moBlock.tsx). JS object key всегда string,nullиз JSON приходит as absent key, не"null". Уберать записьnull: "Не указан".3 sync HTTP calls в endpoint (weather + air quality + seasonal). Каждый с
timeout=5→ в худшем случае 15s блокировки. Pre-existing pattern, но этот PR делает их все три. Issue: запускать вThreadPoolExecutorили background task с кэшем.Magic numbers:
500,3000(severity),5000(radius),'24 months'(horizon) inline. Вынести в module-level:PIPELINE_RADIUS_M,PIPELINE_HORIZON_MONTHS,PIPELINE_SEVERITY_MEDIUM_THRESHOLD,PIPELINE_SEVERITY_HIGH_THRESHOLD.ST_GeomFromText(:wkt, 4326)calculated twice в одном query. CSE должна оптимизировать, но cleaner с named CTE для centroid.obj.ready_dt.substring(0, 7)fragile для display. Работает для ISO date, но если backend переключится на datetime — silently truncate. Использоватьnew Date(...).toLocaleDateString(...)с options.Что хорошо (✅):
(obj_id) ORDER BY snapshot_date DESC— правильный dedup pattern (fix известный поBug_Comparables_Duplicates_Fixed).int(... or 0),(... or "unknown").lower().strip() or "unknown".severity: "none"корректно протянут через все слои: Python early-return + TS literal union + Record include + FE empty-state guard.any, optionalpipeline_24mo?→ backward compat.aria-expanded,titletooltips — accessibility basics.hasAnyguard в MarketTab синхронизирован.Применено в `e452a1d` — все 3 must-fix + 3 из 5 cleanup:
Must-fix:
Cleanup:
4. ✅ `CLASS_LABEL.null` dead key убран с комментарием почему (JSON null → absent key).
6. ✅ Magic numbers → константы: `PIPELINE_RADIUS_M`, `PIPELINE_HORIZON_MONTHS`, `PIPELINE_SEVERITY_MEDIUM_THRESHOLD`, `PIPELINE_SEVERITY_HIGH_THRESHOLD`, `PIPELINE_TOP_OBJECTS_LIMIT`. SQL query f-string'ует их (int литералы — injection-safe).
8. ✅ `fmtMonth(iso)` — `new Date(iso).toLocaleDateString("ru-RU", {year, month})` с fallback к substring при NaN.
Defer:
5. Async 3 HTTP calls — pre-existing pattern, нужен ThreadPoolExecutor refactor отдельным PR (weather/air_quality/seasonal все три).
7. `ST_GeomFromText` дважды — CSE OK на этом масштабе.
`ruff` + `tsc` + `lint` — clean.
🛑 Auto-merge halted: merge conflict — PR не может быть смержен чисто (конфликт с уже-merged изменениями в
backend/app/api/v1/parcels.py, скорее всего после merge #88).Что нужно сделать:
Bot re-review автоматически после fixup-push.
✅ Auto-review passed (LGTM) — все 8 прошлых рекомендаций обработаны корректно.
Resolved (✅):
distance_mfalsy guard:if obj.get("distance_m") is not None:+ комментарий про 0.0/raw Decimal.obj_classNULL production bug — docstring ссылается наfixes/Bug_Kn_API_Obj_Class_Always_Null_OPEN+ D6/#38.CLASS_LABEL.nulldead key удалён + объясняющий комментарий.intконстант:PIPELINE_RADIUS_M,PIPELINE_HORIZON_MONTHS,PIPELINE_SEVERITY_MEDIUM_THRESHOLD,PIPELINE_SEVERITY_HIGH_THRESHOLD,PIPELINE_TOP_OBJECTS_LIMIT.ST_GeomFromText2x — deferred (CSE для 3K rows OK).substring(0,7)→fmtMonth(iso)helper с Date + ru-RU locale + NaN fallback.Safety checks:
text(f"""...""")— оба значенияintконстанты, нет user-input path → no SQL injection.Merge conflict resolved → auto-merge через ~3 мин (idle threshold для approve + commits ≤ 6).
🛑 Auto-merge halted: merge conflict (повторно) — PR снова конфликтует с main после твоего rebase, видимо в этом промежутке ещё один PR смержился в main (либо первая попытка rebase не завершилась clean).
Что нужно сделать:
Bot re-review автоматически.
Merge conflict resolved в `4e431bf` (merge main into branch — main теперь содержит X1 #92, X2 #88, P1 #89, P2 #91).
Что слилось:
`ruff check + format` + `tsc --noEmit` + `next lint` — clean.
PR diff после merge — D4-only (4 файла):
`mergeable: MERGEABLE`. Ждём bot re-review.
⚠️ Auto-review: minor — clean rebase, core logic OK, но есть 3 non-blocking findings которые лучше починить перед merge (политика v5: minor = ждём fixup).
Findings:
`frontend/src/types/site-finder.ts`: `PipelineObject.distance_m` типизирован как `number` (non-nullable), но `_aggregate_pipeline` в `parcels.py` имеет explicit `if obj.get("distance_m") is not None` guard. Если guard сработает, JSON будет содержать `null` → TS runtime type mismatch. Хотя SQL `latitude IS NOT NULL` CTE filter защищает в практике, type должен быть `number | null` чтобы matchить defensive Python path.
`backend/app/api/v1/parcels.py` (pipeline SQL block): `text(f"""...""")` с `INTERVAL '{PIPELINE_HORIZON_MONTHS} months'` — единственный `text(f"...")` pattern в файле; все остальные SQL используют `text("""...""")`. Safe (int const, no user data), но для consistency лучше `INTERVAL :horizon_months` через параметр + `cast(:horizon_months as interval)`.
`backend/app/api/v1/parcels.py` (top_objects loop): `dict(r)` на raw `RowMapping` сериализует все колонки DB row (включая `latitude`, `longitude`, `snapshot_date` из `SELECT *` в CTE). Leak'ит DB schema columns в API response. Не security issue (no PII), но не consistent с principle of explicit field selection. Лучше explicit dict construction со списком нужных полей.
Positives:
Fix and push — bot re-review автоматически.
Все 3 minor findings применены в `8f804d1`:
✅ `PipelineObject.distance_m: number | null` — TS теперь матчит defensive Python guard. Inline comment объясняет почему (centroid-on-building 0.0 falsy, raw Decimal crash в JSON).
✅ Pipeline SQL: `text(f"...")` → `text("...")` + parameters:
Consistency с остальными SQL в файле + защита от future injection.
✅ `top_objects` — explicit field selection: `obj_id, comm_name, dev_name, obj_class, flat_count, ready_dt, distance_m`. `dict(r) for r in rows` убран. Больше нет schema-leak'а из CTE `SELECT *` (latitude/longitude/snapshot_date/region_cd/dev_id etc).
`ruff` + `tsc` + `lint` — clean. Ждём re-review.
✅ Auto-review passed (LGTM) — все 3 minor findings починены чисто.
Verification fixup delta (
4e431bf→8f804d1):No new issues. Мержим (политика v5: approve = instant merge).