feat(site-finder): D4 pipeline 24mo — future competition (#36) #90

Merged
lekss361 merged 4 commits from feat/site-finder-pipeline-24mo-v2 into main 2026-05-12 05:34:02 +00:00
lekss361 commented 2026-05-11 22:01:15 +00:00 (Migrated from github.com)

Summary

Closes #36 (priority/high, Demand D4 — будущая конкуренция за покупателя 24mo). Closes #19 (Конкурентный 360).

Backend_aggregate_pipeline() + новый запрос к domrf_kn_objects:

  • Радиус 5км, фильтр ready_dt BETWEEN CURRENT_DATE AND CURRENT_DATE + 24 months
  • В response analyze.pipeline_24mo:
    • objects_count, flats_total
    • by_class: {economy: N, comfort: M, business: K, ...} (эконом / комфорт / бизнес / премиум / элит / стандарт)
    • by_quarter: [{quarter: "2026-Q1", objects, flats}, ...] — хронологически
    • severity: low (<500 квартир) / medium (<3000) / high (≥3000) per acceptance #36
    • top_objects[10] (по flat_count desc)
    • note про оптимистичность planned_commissioning

FrontendPipeline24moBlock.tsx:

  • Severity badge (color-coded) с label + flats total
  • 3 summary numbers: объектов / квартир суммарно / горизонт+радиус
  • By-class chips (Эконом: 1200, Комфорт: 850, ...)
  • Гистограмма bar по кварталам (нормирована на max, tooltip per-quarter)
  • Toggle «Топ-N ЖК pipeline» → таблица comm_name / класс / квартир / месяц сдачи (sticky thead)
  • Добавлен в MarketTab выше «Market trend»
  • TS типы: Pipeline24mo, PipelineObject, PipelineQuarterSlot

Acceptance (per issue #36)

  • Тестовый участок ЕКБ → реальный pipeline по ЖК с ready_dt в окне 24мес (если есть в БД)
  • Severity отличает мало конкурентов (low) от много (high)
  • By-class breakdown — pressure same-class vs other-class

Test plan

  • ruff + tsc --noEmit + lint — clean ( локально)
  • После deploy:
    curl -X POST https://gendsgn.ru/api/v1/parcels/66:41:0204016:10/analyze | jq '.pipeline_24mo | {objects_count, flats_total, severity, by_class}'
    
  • Visual: открыть UI → Market tab → блок «Конкуренты на 24 мес»

Risks

  • planned_commissioning от Росреестра часто оптимистичен (девелоперы переносят). Это видно в note под блоком; следующий sprint может добавить risk-adjusted = ready_dt + 6мес.
## Summary Closes #36 (priority/high, Demand D4 — будущая конкуренция за покупателя 24mo). Closes #19 (Конкурентный 360). **Backend** — `_aggregate_pipeline()` + новый запрос к `domrf_kn_objects`: - Радиус 5км, фильтр `ready_dt BETWEEN CURRENT_DATE AND CURRENT_DATE + 24 months` - В response `analyze.pipeline_24mo`: - `objects_count`, `flats_total` - `by_class: {economy: N, comfort: M, business: K, ...}` (эконом / комфорт / бизнес / премиум / элит / стандарт) - `by_quarter: [{quarter: "2026-Q1", objects, flats}, ...]` — хронологически - `severity`: low (<500 квартир) / medium (<3000) / high (≥3000) per acceptance #36 - `top_objects[10]` (по flat_count desc) - `note` про оптимистичность planned_commissioning **Frontend** — `Pipeline24moBlock.tsx`: - Severity badge (color-coded) с label + flats total - 3 summary numbers: объектов / квартир суммарно / горизонт+радиус - By-class chips (Эконом: 1200, Комфорт: 850, ...) - Гистограмма bar по кварталам (нормирована на max, tooltip per-quarter) - Toggle «Топ-N ЖК pipeline» → таблица comm_name / класс / квартир / месяц сдачи (sticky thead) - Добавлен в MarketTab выше «Market trend» - TS типы: Pipeline24mo, PipelineObject, PipelineQuarterSlot ## Acceptance (per issue #36) - [x] Тестовый участок ЕКБ → реальный pipeline по ЖК с ready_dt в окне 24мес (если есть в БД) - [x] Severity отличает мало конкурентов (low) от много (high) - [x] By-class breakdown — pressure same-class vs other-class ## Test plan - [ ] `ruff` + `tsc --noEmit` + `lint` — clean (✅ локально) - [ ] После deploy: ``` curl -X POST https://gendsgn.ru/api/v1/parcels/66:41:0204016:10/analyze | jq '.pipeline_24mo | {objects_count, flats_total, severity, by_class}' ``` - [ ] Visual: открыть UI → Market tab → блок «Конкуренты на 24 мес» ## Risks - planned_commissioning от Росреестра часто оптимистичен (девелоперы переносят). Это видно в note под блоком; следующий sprint может добавить risk-adjusted = ready_dt + 6мес.
lekss361 commented 2026-05-11 22:09:09 +00:00 (Migrated from github.com)

⚠️ Auto-review: minor (non-blocking) — 0 блокеров, 3 must-fix-before-push + 5 для cleanup.

Должно быть починено до merge:

  1. distance_m falsy guard (parcels.py:343-344). if obj.get("distance_m") пропускает round() для distance_m == 0.0 (falsy float). Centroid на здании → raw Decimal падает в JSON. Fix: if obj.get("distance_m") is not None.

  2. Нет GIST/spatial индекса на domrf_kn_objects(latitude, longitude). ST_DWithin фильтр в CTE = full seq scan для каждой строки. Текущий размер (~3032 объекта) — OK, но это паттерн который уже ломал производительность. Создать issue для database-expert: добавить (latitude, longitude) или stored geography column до роста объёмов.

  3. obj_class всегда NULL в production (известный bug Bug_Kn_API_Obj_Class_Always_Null_OPEN, ~1670 NULL'ов). by_class chips в UI всегда покажут только "Не определён" — dead UI segment. Код defensively не падает ((r["obj_class"] or "unknown")), но дофиг unused UI. Отметить в vault fixes/Bug_Kn_API_Obj_Class_Always_Null_OPEN, что D4 теперь тоже затронут.

Для cleanup после merge:

  1. CLASS_LABEL.null dead key (Pipeline24moBlock.tsx). JS object key всегда string, null из JSON приходит as absent key, не "null". Уберать запись null: "Не указан".

  2. 3 sync HTTP calls в endpoint (weather + air quality + seasonal). Каждый с timeout=5 → в худшем случае 15s блокировки. Pre-existing pattern, но этот PR делает их все три. Issue: запускать в ThreadPoolExecutor или background task с кэшем.

  3. 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.

  4. ST_GeomFromText(:wkt, 4326) calculated twice в одном query. CSE должна оптимизировать, но cleaner с named CTE для centroid.

  5. obj.ready_dt.substring(0, 7) fragile для display. Работает для ISO date, но если backend переключится на datetime — silently truncate. Использовать new Date(...).toLocaleDateString(...) с options.

Что хорошо ():

  • DISTINCT ON (obj_id) ORDER BY snapshot_date DESC — правильный dedup pattern (fix известный по Bug_Comparables_Duplicates_Fixed).
  • Two-layer null defenses: int(... or 0), (... or "unknown").lower().strip() or "unknown".
  • severity: "none" корректно протянут через все слои: Python early-return + TS literal union + Record include + FE empty-state guard.
  • TS strict, literal union, no any, optional pipeline_24mo? → backward compat.
  • aria-expanded, title tooltips — accessibility basics.
  • hasAny guard в MarketTab синхронизирован.
⚠️ **Auto-review: minor (non-blocking)** — 0 блокеров, 3 must-fix-before-push + 5 для cleanup. **Должно быть починено до merge:** 1. **`distance_m` falsy guard** (parcels.py:343-344). `if obj.get("distance_m")` пропускает `round()` для `distance_m == 0.0` (falsy float). Centroid на здании → raw `Decimal` падает в JSON. Fix: `if obj.get("distance_m") is not None`. 2. **Нет GIST/spatial индекса на `domrf_kn_objects(latitude, longitude)`**. `ST_DWithin` фильтр в CTE = full seq scan для каждой строки. Текущий размер (~3032 объекта) — OK, но это паттерн который уже ломал производительность. Создать issue для `database-expert`: добавить `(latitude, longitude)` или stored geography column до роста объёмов. 3. **`obj_class` всегда NULL в production** (известный bug `Bug_Kn_API_Obj_Class_Always_Null_OPEN`, ~1670 NULL'ов). `by_class` chips в UI всегда покажут только "Не определён" — dead UI segment. Код defensively не падает (`(r["obj_class"] or "unknown")`), но дофиг unused UI. Отметить в vault `fixes/Bug_Kn_API_Obj_Class_Always_Null_OPEN`, что D4 теперь тоже затронут. **Для cleanup после merge:** 4. **`CLASS_LABEL.null` dead key** (Pipeline24moBlock.tsx). JS object key всегда string, `null` из JSON приходит as absent key, не `"null"`. Уберать запись `null: "Не указан"`. 5. **3 sync HTTP calls в endpoint** (weather + air quality + seasonal). Каждый с `timeout=5` → в худшем случае 15s блокировки. Pre-existing pattern, но этот PR делает их все три. Issue: запускать в `ThreadPoolExecutor` или background task с кэшем. 6. **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`. 7. **`ST_GeomFromText(:wkt, 4326)` calculated twice** в одном query. CSE должна оптимизировать, но cleaner с named CTE для centroid. 8. **`obj.ready_dt.substring(0, 7)` fragile** для display. Работает для ISO date, но если backend переключится на datetime — silently truncate. Использовать `new Date(...).toLocaleDateString(...)` с options. **Что хорошо (✅):** - DISTINCT ON `(obj_id) ORDER BY snapshot_date DESC` — правильный dedup pattern (fix известный по `Bug_Comparables_Duplicates_Fixed`). - Two-layer null defenses: `int(... or 0)`, `(... or "unknown").lower().strip() or "unknown"`. - `severity: "none"` корректно протянут через все слои: Python early-return + TS literal union + Record include + FE empty-state guard. - TS strict, literal union, no `any`, optional `pipeline_24mo?` → backward compat. - `aria-expanded`, `title` tooltips — accessibility basics. - `hasAny` guard в MarketTab синхронизирован. <!-- gendesign-review-bot: sha=ade511b verdict=minor -->
lekss361 commented 2026-05-11 22:19:00 +00:00 (Migrated from github.com)

Применено в `e452a1d` — все 3 must-fix + 3 из 5 cleanup:

Must-fix:

  1. `distance_m` guard → `is not None` (centroid-on-building даёт 0.0 falsy).
  2. Docstring добавил note про seq scan ~3K OK + recommend GIST на (latitude, longitude) для роста — отдельный issue для database-expert.
  3. Docstring _aggregate_pipeline помечает obj_class NULL bug + ref на `Bug_Kn_API_Obj_Class_Always_Null_OPEN` (закрывается D6/#38).

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.

Применено в \`e452a1d\` — все 3 must-fix + 3 из 5 cleanup: **Must-fix:** 1. ✅ \`distance_m\` guard → \`is not None\` (centroid-on-building даёт 0.0 falsy). 2. ✅ Docstring добавил note про seq scan ~3K OK + recommend GIST на (latitude, longitude) для роста — отдельный issue для database-expert. 3. ✅ Docstring _aggregate_pipeline помечает obj_class NULL bug + ref на \`Bug_Kn_API_Obj_Class_Always_Null_OPEN\` (закрывается D6/#38). **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.
lekss361 commented 2026-05-11 22:19:12 +00:00 (Migrated from github.com)

🛑 Auto-merge halted: merge conflict — PR не может быть смержен чисто (конфликт с уже-merged изменениями в backend/app/api/v1/parcels.py, скорее всего после merge #88).

Что нужно сделать:

git fetch origin main
git checkout feat/site-finder-pipeline-24mo-v2
git rebase origin/main   # или git merge origin/main
# resolve conflicts in parcels.py
git push --force-with-lease

Bot re-review автоматически после fixup-push.

🛑 **Auto-merge halted: merge conflict** — PR не может быть смержен чисто (конфликт с уже-merged изменениями в `backend/app/api/v1/parcels.py`, скорее всего после merge #88). **Что нужно сделать:** ```bash git fetch origin main git checkout feat/site-finder-pipeline-24mo-v2 git rebase origin/main # или git merge origin/main # resolve conflicts in parcels.py git push --force-with-lease ``` Bot re-review автоматически после fixup-push. <!-- gendesign-review-bot: sha=ade511b verdict=merge-conflict -->
lekss361 commented 2026-05-11 22:24:28 +00:00 (Migrated from github.com)

Auto-review passed (LGTM) — все 8 прошлых рекомендаций обработаны корректно.

Resolved ():

  1. distance_m falsy guard: if obj.get("distance_m") is not None: + комментарий про 0.0/raw Decimal.
  2. Missing GIST index → inline note + tracked для database-expert (acceptable non-fix в scope этого PR).
  3. obj_class NULL production bug — docstring ссылается на fixes/Bug_Kn_API_Obj_Class_Always_Null_OPEN + D6/#38.
  4. CLASS_LABEL.null dead key удалён + объясняющий комментарий.
  5. 3 sync HTTP calls — deferred (pre-existing, separate issue).
  6. Magic numbers → 5 module-level int констант: PIPELINE_RADIUS_M, PIPELINE_HORIZON_MONTHS, PIPELINE_SEVERITY_MEDIUM_THRESHOLD, PIPELINE_SEVERITY_HIGH_THRESHOLD, PIPELINE_TOP_OBJECTS_LIMIT.
  7. ST_GeomFromText 2x — deferred (CSE для 3K rows OK).
  8. substring(0,7)fmtMonth(iso) helper с Date + ru-RU locale + NaN fallback.

Safety checks:

  • F-string interpolация в text(f"""...""") — оба значения int константы, нет user-input path → no SQL injection.
  • Rebase clean, никаких merge conflict artifacts.
  • TS types backward-compat (pipeline_24mo? optional).

Merge conflict resolved → auto-merge через ~3 мин (idle threshold для approve + commits ≤ 6).

✅ **Auto-review passed (LGTM)** — все 8 прошлых рекомендаций обработаны корректно. **Resolved (✅):** 1. `distance_m` falsy guard: `if obj.get("distance_m") is not None:` + комментарий про 0.0/raw Decimal. 2. Missing GIST index → inline note + tracked для database-expert (acceptable non-fix в scope этого PR). 3. `obj_class` NULL production bug — docstring ссылается на `fixes/Bug_Kn_API_Obj_Class_Always_Null_OPEN` + D6/#38. 4. `CLASS_LABEL.null` dead key удалён + объясняющий комментарий. 5. 3 sync HTTP calls — deferred (pre-existing, separate issue). 6. Magic numbers → 5 module-level `int` констант: `PIPELINE_RADIUS_M`, `PIPELINE_HORIZON_MONTHS`, `PIPELINE_SEVERITY_MEDIUM_THRESHOLD`, `PIPELINE_SEVERITY_HIGH_THRESHOLD`, `PIPELINE_TOP_OBJECTS_LIMIT`. 7. `ST_GeomFromText` 2x — deferred (CSE для 3K rows OK). 8. `substring(0,7)` → `fmtMonth(iso)` helper с Date + ru-RU locale + NaN fallback. **Safety checks:** - F-string interpolация в `text(f"""...""")` — оба значения `int` константы, нет user-input path → no SQL injection. - Rebase clean, никаких merge conflict artifacts. - TS types backward-compat (pipeline_24mo? optional). Merge conflict resolved → auto-merge через ~3 мин (idle threshold для approve + commits ≤ 6). <!-- gendesign-review-bot: sha=e452a1d verdict=approve -->
lekss361 commented 2026-05-11 22:26:12 +00:00 (Migrated from github.com)

🛑 Auto-merge halted: merge conflict (повторно) — PR снова конфликтует с main после твоего rebase, видимо в этом промежутке ещё один PR смержился в main (либо первая попытка rebase не завершилась clean).

Что нужно сделать:

git fetch origin main
git checkout feat/site-finder-pipeline-24mo-v2
git rebase origin/main
# resolve conflicts in parcels.py
git push --force-with-lease

Bot re-review автоматически.

🛑 **Auto-merge halted: merge conflict (повторно)** — PR снова конфликтует с main после твоего rebase, видимо в этом промежутке ещё один PR смержился в main (либо первая попытка rebase не завершилась clean). **Что нужно сделать:** ```bash git fetch origin main git checkout feat/site-finder-pipeline-24mo-v2 git rebase origin/main # resolve conflicts in parcels.py git push --force-with-lease ``` Bot re-review автоматически. <!-- gendesign-review-bot: sha=e452a1d verdict=merge-conflict -->
lekss361 commented 2026-05-12 05:22:25 +00:00 (Migrated from github.com)

Merge conflict resolved в `4e431bf` (merge main into branch — main теперь содержит X1 #92, X2 #88, P1 #89, P2 #91).

Что слилось:

  • 2 конфликта в `parcels.py` (между D4 helpers и P1/P2 helpers; между D4 aggregation block и X1/X2 computation blocks) + 1 в `site-finder.ts`.
  • Все resolved keep-both: D4 (`pipeline_24mo` + `aggregate_pipeline`) сосуществует с P1 (`geometry_suitability`), P2 (`neighbors_summary`), X1 (`score_breakdown_detailed`/`score_top_3`/`score_by_group`), X2 (`confidence_`). Response dict содержит все 5 секций.
  • TS: все 4 optional поля в `ParcelAnalysis` (pipeline_24mo + geometry_suitability + neighbors_summary + confidence_*).

`ruff check + format` + `tsc --noEmit` + `next lint` — clean.

PR diff после merge — D4-only (4 файла):

  • `backend/app/api/v1/parcels.py` (D4 changes)
  • `frontend/src/components/site-finder/MarketTab.tsx` (Pipeline24moBlock integration)
  • `frontend/src/components/site-finder/Pipeline24moBlock.tsx` (new D4 component)
  • `frontend/src/types/site-finder.ts` (D4 types)

`mergeable: MERGEABLE`. Ждём bot re-review.

Merge conflict resolved в \`4e431bf\` (merge main into branch — main теперь содержит X1 #92, X2 #88, P1 #89, P2 #91). **Что слилось:** - 2 конфликта в \`parcels.py\` (между D4 helpers и P1/P2 helpers; между D4 aggregation block и X1/X2 computation blocks) + 1 в \`site-finder.ts\`. - Все resolved **keep-both**: D4 (\`pipeline_24mo\` + \`_aggregate_pipeline\`) сосуществует с P1 (\`geometry_suitability\`), P2 (\`neighbors_summary\`), X1 (\`score_breakdown_detailed\`/\`score_top_3_*\`/\`score_by_group\`), X2 (\`confidence_*\`). Response dict содержит все 5 секций. - TS: все 4 optional поля в \`ParcelAnalysis\` (pipeline_24mo + geometry_suitability + neighbors_summary + confidence_*). \`ruff check + format\` + \`tsc --noEmit\` + \`next lint\` — clean. **PR diff после merge — D4-only (4 файла):** - \`backend/app/api/v1/parcels.py\` (D4 changes) - \`frontend/src/components/site-finder/MarketTab.tsx\` (Pipeline24moBlock integration) - \`frontend/src/components/site-finder/Pipeline24moBlock.tsx\` (new D4 component) - \`frontend/src/types/site-finder.ts\` (D4 types) \`mergeable: MERGEABLE\`. Ждём bot re-review.
lekss361 commented 2026-05-12 05:27:12 +00:00 (Migrated from github.com)

⚠️ 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:

  • Correct null handling везде (`flat_count` coercion, `obj_class` fallback, `ready_dt` guard).
  • TS — no `any`, no fetch in useEffect, exhaustive Record`<Pipeline24mo["severity"], ...>`.
  • `fmtMonth` robust против invalid ISO (NaN guard).
  • Empty-state branch (`severity="none"`) рендерит clean message без crash.
  • SQL pattern (f-string с int constants) идентичен existing approved code.
  • No hardcoded secrets, no auth bypass, no print().

Fix and push — 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:** - Correct null handling везде (\`flat_count\` coercion, \`obj_class\` fallback, \`ready_dt\` guard). - TS — no \`any\`, no fetch in useEffect, exhaustive Record\`<Pipeline24mo[\"severity\"], ...>\`. - \`fmtMonth\` robust против invalid ISO (NaN guard). - Empty-state branch (\`severity=\"none\"\`) рендерит clean message без crash. - SQL pattern (f-string с int constants) идентичен existing approved code. - No hardcoded secrets, no auth bypass, no print(). Fix and push — bot re-review автоматически. <!-- gendesign-review-bot: sha=4e431bf verdict=minor -->
lekss361 commented 2026-05-12 05:32:00 +00:00 (Migrated from github.com)

Все 3 minor findings применены в `8f804d1`:

  1. `PipelineObject.distance_m: number | null` — TS теперь матчит defensive Python guard. Inline comment объясняет почему (centroid-on-building 0.0 falsy, raw Decimal crash в JSON).

  2. Pipeline SQL: `text(f"...")` → `text("...")` + parameters:

    • `:radius_m` — `PIPELINE_RADIUS_M` int
    • `cast(:horizon_months || ' months' AS interval)` — string → interval cast
      Consistency с остальными SQL в файле + защита от future injection.
  3. `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.

Все 3 minor findings применены в \`8f804d1\`: 1. ✅ \`PipelineObject.distance_m: number | null\` — TS теперь матчит defensive Python guard. Inline comment объясняет почему (centroid-on-building 0.0 falsy, raw Decimal crash в JSON). 2. ✅ Pipeline SQL: \`text(f"...")\` → \`text("...")\` + parameters: - \`:radius_m\` — \`PIPELINE_RADIUS_M\` int - \`cast(:horizon_months || ' months' AS interval)\` — string → interval cast Consistency с остальными SQL в файле + защита от future injection. 3. ✅ \`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.
lekss361 commented 2026-05-12 05:34:00 +00:00 (Migrated from github.com)

Auto-review passed (LGTM) — все 3 minor findings починены чисто.

Verification fixup delta (4e431bf8f804d1):

  • TS `distance_m: number | null` — matches Python defensive guard, inline comment объясняет why.
  • Pipeline SQL: `text("""...""")` plain string + `:radius_m` / `:horizon_months` через `cast(:horizon_months || ' months' AS interval)`. f-string убран, no injection vector.
  • `top_objects`: explicit field selection (obj_id, comm_name, dev_name, obj_class, flat_count, ready_dt, distance_m). `dict(r)` убран, schema clean.

No new issues. Мержим (политика v5: approve = instant merge).

✅ **Auto-review passed (LGTM)** — все 3 minor findings починены чисто. **Verification fixup delta (4e431bf → 8f804d1):** - ✅ TS \`distance_m: number | null\` — matches Python defensive guard, inline comment объясняет why. - ✅ Pipeline SQL: \`text(\"\"\"...\"\"\")\` plain string + \`:radius_m\` / \`:horizon_months\` через \`cast(:horizon_months || ' months' AS interval)\`. f-string убран, no injection vector. - ✅ \`top_objects\`: explicit field selection (obj_id, comm_name, dev_name, obj_class, flat_count, ready_dt, distance_m). \`dict(r)\` убран, schema clean. No new issues. Мержим (политика v5: approve = instant merge). <!-- gendesign-review-bot: sha=8f804d1 verdict=approve -->
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#90
No description provided.