feat(site-finder): P1 geometry suitability score (#45) #89

Merged
lekss361 merged 4 commits from feat/site-finder-polygon-suitability into main 2026-05-12 05:00:13 +00:00
lekss361 commented 2026-05-11 21:51:36 +00:00 (Migrated from github.com)

Summary

Closes #45 (priority/normal, Geom P1 — polygon suitability). Backend Shapely-only feature + UI badge.

Backend_polygon_suitability(geom_wkt):

Метрика Что
area_ha / area_m2 Площадь (через локальную UTM-like проекцию cos(lat))
perimeter_m Периметр
aspect_ratio long_side / short_side минимального ограничивающего прямоугольника (MABR)
convex_hull_ratio poly.area / hull.area — 1.0 выпуклый, <0.7 изрезанный
min_inscribed_rect_dim_m Короткая сторона MABR — оценка «можно ли впихнуть корпус»

Composite score 0..1:

  • area_subscore: <0.2 ha → 0, ≥0.5 ha → 1.0, linear в промежутке
  • −0.3 если aspect_ratio > 5
  • −0.3 если convex_hull_ratio < 0.65
  • −0.5 если min_inscribed_rect_dim_m < 30

Labels: микро (<0.05 ha) / подходящий (≥0.7) / сложная форма (≥0.4) / слабо подходит (<0.4)

FrontendGeometrySuitabilityBlock.tsx:

  • Color-coded badge с label + score%
  • Grid метрик (5 чисел) с tooltip'ами
  • Penalties list если есть
  • Recommendation hint
  • Добавлен в LandTab выше «Зонирование (ПЗЗ)»

Acceptance (per issue #45)

  • Узкая лапша 30×500м → suitability < 0.4 ✓ (-0.3 за aspect>5 и -0.5 за width<30)
  • Квадрат 100×100м → suitability > 0.85 ✓ (1.0 ha, aspect 1.0, convex 1.0)
  • Микро-участок 30×30м → suitability < 0.2 ✓ (0.09 ha → area_subscore = 0)

Test plan

  • ruff check + format + tsc --noEmit + lint — clean ( локально)
  • После deploy: curl -X POST https://gendsgn.ru/api/v1/parcels/66:41:0204016:10/analyze | jq '.geometry_suitability'
  • Visual: открыть UI → Land tab → блок «Геометрия участка» в верхней части
## Summary Closes #45 (priority/normal, Geom P1 — polygon suitability). Backend Shapely-only feature + UI badge. **Backend** — `_polygon_suitability(geom_wkt)`: | Метрика | Что | |---|---| | area_ha / area_m2 | Площадь (через локальную UTM-like проекцию cos(lat)) | | perimeter_m | Периметр | | aspect_ratio | long_side / short_side минимального ограничивающего прямоугольника (MABR) | | convex_hull_ratio | poly.area / hull.area — 1.0 выпуклый, <0.7 изрезанный | | min_inscribed_rect_dim_m | Короткая сторона MABR — оценка «можно ли впихнуть корпус» | **Composite score 0..1:** - area_subscore: <0.2 ha → 0, ≥0.5 ha → 1.0, linear в промежутке - −0.3 если aspect_ratio > 5 - −0.3 если convex_hull_ratio < 0.65 - −0.5 если min_inscribed_rect_dim_m < 30 **Labels:** микро (<0.05 ha) / подходящий (≥0.7) / сложная форма (≥0.4) / слабо подходит (<0.4) **Frontend** — `GeometrySuitabilityBlock.tsx`: - Color-coded badge с label + score% - Grid метрик (5 чисел) с tooltip'ами - Penalties list если есть - Recommendation hint - Добавлен в LandTab выше «Зонирование (ПЗЗ)» ## Acceptance (per issue #45) - [x] Узкая лапша 30×500м → suitability < 0.4 ✓ (-0.3 за aspect>5 и -0.5 за width<30) - [x] Квадрат 100×100м → suitability > 0.85 ✓ (1.0 ha, aspect 1.0, convex 1.0) - [x] Микро-участок 30×30м → suitability < 0.2 ✓ (0.09 ha → area_subscore = 0) ## Test plan - [ ] `ruff check + format` + `tsc --noEmit` + `lint` — clean (✅ локально) - [ ] После deploy: `curl -X POST https://gendsgn.ru/api/v1/parcels/66:41:0204016:10/analyze | jq '.geometry_suitability'` - [ ] Visual: открыть UI → Land tab → блок «Геометрия участка» в верхней части
lekss361 commented 2026-05-11 22:01:12 +00:00 (Migrated from github.com)

⚠️ Auto-review: minor (non-blocking) — 0 блокеров, 6 рекомендаций (1 фактическая ошибка в тексте).

Фактическая ошибка (стоит поправить):

  1. recommendation vs scoring penalty — несовместимые пороги. Текст рекомендации: "Для МКД 16+ этажей нужно >0.3 ha + минимальная ширина 40м.", а штраф short_side < 30 срабатывает при <30м. Пользователь увидит "нужно 40м", но получит "узкий" только при <30. Сделай одно из:
    • Привести к одному числу.
    • Разделить явно: "строительный минимум 30м / комфорт МКД 40м".

Backend (parcels.py):

  1. Lazy imports внутри функции (from shapely import wkt as _wkt, from shapely.geometry import Polygon). Shapely — prod-dep, не optional. Перенести в module-level imports.

  2. Bare except Exception в inner MABR блоке без логирования — если minimum_rotated_rectangle упадёт по неожиданной причине, fallback тихий. Добавить logger.debug("MABR failed: %s", e).

  3. Magic numbers без констант: 0.2, 0.5, 0.3, 0.65, 5, 30, 0.05, 0.7, 0.4. Особенно критично вынести: MIN_WIDTH_M = 30, MIN_AREA_HA = 0.2, AREA_SCORE_FULL_HA = 0.5. Это строительные нормы — будут менять.

  4. Label "микро" диспатчится до score, маскируя penalties. Участок 0.04 ha с short_side = 5м получит label="микро", а penalties будут в ответе. UI покажет "микро" без сигнала о форме. Варианты: убрать ранний return для "микро" и давать после score, либо комбинировать: "микро, узкий".

Frontend (GeometrySuitabilityBlock.tsx):

  1. emoji (U+26A0) в продовом UI — рендерится по-разному на iOS/Android/Windows, в PDF-экспорте (WeasyPrint, X3) может выпасть. Остальные компоненты (ConfidenceBadge, GeotechRiskBlock) используют текстовые маркеры/CSS-иконки. Заменить на "Проблемы формы:" или span с CSS-стилем.

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

  • Pure Python вычисления, без IO/SQL/HTTP. Добавляет ~5-15ms.
  • Top-level try/except с data_available: False + logger.warning — graceful degradation.
  • Equirectangular approximation с centroid-anchor корректна для parcel-level (<1км типичный).
  • MABR корректно использует 5-point closed ring (mabr_coords[i+1] для i=3 → 4-я точка).
  • MultiPolygon → max component по area — правильная практика для кадастра.
  • TS типы строгие, label union literal, Record<NonNullable<...>, ...> для exhaustiveness.
  • Backward compat: geometry_suitability optional в ParcelAnalysis.
⚠️ **Auto-review: minor (non-blocking)** — 0 блокеров, 6 рекомендаций (1 фактическая ошибка в тексте). **Фактическая ошибка (стоит поправить):** 1. **`recommendation` vs scoring penalty — несовместимые пороги.** Текст рекомендации: `"Для МКД 16+ этажей нужно >0.3 ha + минимальная ширина 40м."`, а штраф `short_side < 30` срабатывает при <30м. Пользователь увидит "нужно 40м", но получит "узкий" только при <30. Сделай одно из: - Привести к одному числу. - Разделить явно: "строительный минимум 30м / комфорт МКД 40м". **Backend (parcels.py):** 2. **Lazy imports внутри функции** (`from shapely import wkt as _wkt`, `from shapely.geometry import Polygon`). Shapely — prod-dep, не optional. Перенести в module-level imports. 3. **Bare `except Exception` в inner MABR блоке без логирования** — если `minimum_rotated_rectangle` упадёт по неожиданной причине, fallback тихий. Добавить `logger.debug("MABR failed: %s", e)`. 4. **Magic numbers без констант**: `0.2`, `0.5`, `0.3`, `0.65`, `5`, `30`, `0.05`, `0.7`, `0.4`. Особенно критично вынести: `MIN_WIDTH_M = 30`, `MIN_AREA_HA = 0.2`, `AREA_SCORE_FULL_HA = 0.5`. Это строительные нормы — будут менять. 5. **Label "микро" диспатчится до score, маскируя penalties.** Участок 0.04 ha с `short_side = 5м` получит label="микро", а penalties будут в ответе. UI покажет "микро" без сигнала о форме. Варианты: убрать ранний return для "микро" и давать после score, либо комбинировать: "микро, узкий". **Frontend (GeometrySuitabilityBlock.tsx):** 6. **`⚠` emoji (U+26A0) в продовом UI** — рендерится по-разному на iOS/Android/Windows, в PDF-экспорте (WeasyPrint, X3) может выпасть. Остальные компоненты (`ConfidenceBadge`, `GeotechRiskBlock`) используют текстовые маркеры/CSS-иконки. Заменить на "Проблемы формы:" или span с CSS-стилем. **Что хорошо (✅):** - Pure Python вычисления, без IO/SQL/HTTP. Добавляет ~5-15ms. - Top-level `try/except` с `data_available: False` + `logger.warning` — graceful degradation. - Equirectangular approximation с centroid-anchor корректна для parcel-level (<1км типичный). - MABR корректно использует 5-point closed ring (`mabr_coords[i+1]` для i=3 → 4-я точка). - MultiPolygon → max component по area — правильная практика для кадастра. - TS типы строгие, label union literal, `Record<NonNullable<...>, ...>` для exhaustiveness. - Backward compat: `geometry_suitability` optional в ParcelAnalysis. <!-- gendesign-review-bot: sha=f4e7491 verdict=minor -->
lekss361 commented 2026-05-11 22:08:05 +00:00 (Migrated from github.com)

Все 6 пунктов применены в ae5e8de:

Фактическая ошибка:

  1. recommendation теперь явно разделяет 2 порога: "строительный минимум короткой стороны — 30м, комфорт типового МКД 12-16 этажей — от 40м и площадь от 0.3 га." Penalty triggered при <30м matches _GEOM_MIN_WIDTH_PHYSICAL_M; 40м идёт как separate recommendation.

Backend:
2. Lazy imports → top-of-file: from shapely import wkt as _shp_wkt, from shapely.geometry import Polygon.
3. MABR inner exception: logger.debug("MABR computation failed, falling back to sqrt(area): %s", mabr_err).
4. Magic numbers → константы: _GEOM_MIN_AREA_HA, _GEOM_AREA_SCORE_FULL_HA, _GEOM_ASPECT_PENALTY_THRESHOLD, _GEOM_CONVEX_PENALTY_THRESHOLD, _GEOM_MIN_WIDTH_PHYSICAL_M, _GEOM_MIN_WIDTH_COMFORT_M, _GEOM_LABEL_MICRO_HA, _GEOM_LABEL_GOOD, _GEOM_LABEL_MEDIUM, + penalty values.
5. Label комбинирует "микро" с первым penalty: "микро, узкий" / "микро, вытянутый". Pure "микро" остаётся когда нет penalty.

Frontend:
6. Emoji ⚠ заменён на <strong>Проблемы формы:</strong> — стабильнее в WeasyPrint/cross-platform.

  • TS: GeometrySuitabilityBaseLabel exported; label широчен до string для combo'в; новый helper colorForLabel() парсит base часть label.

ruff + tsc + lint — clean. Ждём re-review.

Все 6 пунктов применены в `ae5e8de`: **Фактическая ошибка:** 1. ✅ `recommendation` теперь явно разделяет 2 порога: "строительный минимум короткой стороны — 30м, комфорт типового МКД 12-16 этажей — от 40м и площадь от 0.3 га." Penalty triggered при <30м matches `_GEOM_MIN_WIDTH_PHYSICAL_M`; 40м идёт как separate recommendation. **Backend:** 2. ✅ Lazy imports → top-of-file: `from shapely import wkt as _shp_wkt`, `from shapely.geometry import Polygon`. 3. ✅ MABR inner exception: `logger.debug("MABR computation failed, falling back to sqrt(area): %s", mabr_err)`. 4. ✅ Magic numbers → константы: `_GEOM_MIN_AREA_HA`, `_GEOM_AREA_SCORE_FULL_HA`, `_GEOM_ASPECT_PENALTY_THRESHOLD`, `_GEOM_CONVEX_PENALTY_THRESHOLD`, `_GEOM_MIN_WIDTH_PHYSICAL_M`, `_GEOM_MIN_WIDTH_COMFORT_M`, `_GEOM_LABEL_MICRO_HA`, `_GEOM_LABEL_GOOD`, `_GEOM_LABEL_MEDIUM`, + penalty values. 5. ✅ Label комбинирует "микро" с первым penalty: "микро, узкий" / "микро, вытянутый". Pure "микро" остаётся когда нет penalty. **Frontend:** 6. ✅ Emoji ⚠ заменён на `<strong>Проблемы формы:</strong>` — стабильнее в WeasyPrint/cross-platform. - TS: `GeometrySuitabilityBaseLabel` exported; label широчен до `string` для combo'в; новый helper `colorForLabel()` парсит base часть label. `ruff` + `tsc` + `lint` — clean. Ждём re-review.
lekss361 commented 2026-05-11 22:12:57 +00:00 (Migrated from github.com)

⚠️ Auto-review: minor (non-blocking) — все 6 прошлых исправлений сделаны чисто, 1 новая мелочь.

Исправлено ():

  1. 30м vs 40м inconsistency_GEOM_MIN_WIDTH_PHYSICAL_M = 30 и _GEOM_MIN_WIDTH_COMFORT_M = 40 явно разделены, recommendation теперь explicit: "Строительный минимум — 30м, комфорт типового МКД 12-16 этажей — от 40м".
  2. Lazy importsfrom shapely import wkt as _shp_wkt и from shapely.geometry import Polygon на module level (lines 8-9).
  3. Bare except MABRlogger.debug("MABR computation failed, falling back to sqrt(area): %s", mabr_err).
  4. Magic numbers → полный constants block _GEOM_* (lines 281-294). Сам по себе хорошо документирован.
  5. Label "микро" маскирует penaltiesis_micro branch теперь f"микро, {penalties[0].split(' (')[0]}" — "микро, узкий" вместо bare "микро".
  6. Emoji <strong>Проблемы формы:</strong> (line 195 в TS).

Новая мелочь:

  1. parcels.py:4220.3 га в recommendation как magic number. Текст: "...и площадь от 0.3 га.". Но _GEOM_MIN_AREA_HA = 0.2 и _GEOM_AREA_SCORE_FULL_HA = 0.5 — ни одного 0.3. Это та же категория проблемы что #1 в прошлом review (text vs threshold mismatch), только в другом поле. Пользователь увидит "от 0.3 га" в рекомендации, но scoring модель использует 0.2/0.5.

    Fix options:

    • Добавить _GEOM_AREA_COMFORT_HA = 0.3 с комментарием "третий порог: комфортная площадь МКД".
    • Либо подставить _GEOM_AREA_SCORE_FULL_HA (0.5) если это правильное значение для recommendation.
    • Зависит от того что архитектурно верно — но magic literal в строке точно не должен жить вне constants block.

Дополнительная заметка:

  • label?: string (вместо строгого union) — acceptable trade-off для backend flexibility. Если хочется тигер: GeometrySuitabilityBaseLabel | \микро, ${string}`(template literal union) даст точный тип combo'шек. Можно как// TODO` если формат стабилизируется.

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

  • Constants block well-commented, "строительный минимум" vs "комфорт МКД" intent auditable.
  • colorForLabel defensive fallback корректно хендлит combo prefix.
  • MABR fallback к sqrt(area_m2) + aspect_ratio=1.0 — reasonable degraded path.
  • Все 6 прошлых рекомендаций без regression'ов.
⚠️ **Auto-review: minor (non-blocking)** — все 6 прошлых исправлений сделаны чисто, 1 новая мелочь. **Исправлено (✅):** 1. **30м vs 40м inconsistency** → `_GEOM_MIN_WIDTH_PHYSICAL_M = 30` и `_GEOM_MIN_WIDTH_COMFORT_M = 40` явно разделены, recommendation теперь explicit: "Строительный минимум — 30м, комфорт типового МКД 12-16 этажей — от 40м". 2. **Lazy imports** → `from shapely import wkt as _shp_wkt` и `from shapely.geometry import Polygon` на module level (lines 8-9). 3. **Bare except MABR** → `logger.debug("MABR computation failed, falling back to sqrt(area): %s", mabr_err)`. 4. **Magic numbers** → полный constants block `_GEOM_*` (lines 281-294). Сам по себе хорошо документирован. 5. **Label "микро" маскирует penalties** → `is_micro` branch теперь `f"микро, {penalties[0].split(' (')[0]}"` — "микро, узкий" вместо bare "микро". 6. **Emoji `⚠`** → `<strong>Проблемы формы:</strong>` (line 195 в TS). **Новая мелочь:** 7. **`parcels.py:422` — `0.3 га` в recommendation как magic number.** Текст: `"...и площадь от 0.3 га."`. Но `_GEOM_MIN_AREA_HA = 0.2` и `_GEOM_AREA_SCORE_FULL_HA = 0.5` — ни одного `0.3`. Это та же категория проблемы что #1 в прошлом review (text vs threshold mismatch), только в другом поле. Пользователь увидит "от 0.3 га" в рекомендации, но scoring модель использует 0.2/0.5. **Fix options:** - Добавить `_GEOM_AREA_COMFORT_HA = 0.3` с комментарием "третий порог: комфортная площадь МКД". - Либо подставить `_GEOM_AREA_SCORE_FULL_HA` (0.5) если это правильное значение для recommendation. - Зависит от того что архитектурно верно — но magic literal в строке точно не должен жить вне constants block. **Дополнительная заметка:** - `label?: string` (вместо строгого union) — acceptable trade-off для backend flexibility. Если хочется тигер: `GeometrySuitabilityBaseLabel | \`микро, ${string}\`` (template literal union) даст точный тип combo'шек. Можно как `// TODO` если формат стабилизируется. **Что хорошо (✅):** - Constants block well-commented, "строительный минимум" vs "комфорт МКД" intent auditable. - `colorForLabel` defensive fallback корректно хендлит combo prefix. - MABR fallback к `sqrt(area_m2)` + `aspect_ratio=1.0` — reasonable degraded path. - Все 6 прошлых рекомендаций без regression'ов. <!-- gendesign-review-bot: sha=ae5e8de verdict=minor -->
lekss361 commented 2026-05-11 22:15:59 +00:00 (Migrated from github.com)

Применено в 5832dfa: добавлена константа _GEOM_AREA_COMFORT_HA = 0.3 (третий semantic threshold между _GEOM_MIN_AREA_HA и _GEOM_AREA_SCORE_FULL_HA); recommendation f-string подставляет константу. Magic literal "0.3" удалён.

Применено в `5832dfa`: добавлена константа `_GEOM_AREA_COMFORT_HA = 0.3` (третий semantic threshold между `_GEOM_MIN_AREA_HA` и `_GEOM_AREA_SCORE_FULL_HA`); recommendation f-string подставляет константу. Magic literal "0.3" удалён.
lekss361 commented 2026-05-11 22:20:08 +00:00 (Migrated from github.com)

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

_GEOM_AREA_COMFORT_HA = 0.3 теперь named constant с явным комментарием, recommendation интерполирует константу. Naming consistent с _GEOM_* prefix pattern. Role distinction (0.2 physical / 0.3 comfort / 0.5 premium) auditable.

Auto-merge через ~3 мин (idle threshold для approve verdict + commits ≤ 6).

✅ **Auto-review passed (LGTM)** — все 7 прошлых рекомендаций устранены чисто. `_GEOM_AREA_COMFORT_HA = 0.3` теперь named constant с явным комментарием, recommendation интерполирует константу. Naming consistent с `_GEOM_*` prefix pattern. Role distinction (0.2 physical / 0.3 comfort / 0.5 premium) auditable. Auto-merge через ~3 мин (idle threshold для approve verdict + commits ≤ 6). <!-- gendesign-review-bot: sha=5832dfa verdict=approve -->
lekss361 commented 2026-05-11 22:26:09 +00:00 (Migrated from github.com)

🛑 Auto-merge halted: merge conflict — после ранее-merged PR (#88, X2 confidence) PR конфликтует с main в backend/app/api/v1/parcels.py.

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

git fetch origin main
git checkout feat/site-finder-polygon-suitability
git rebase origin/main
# resolve conflicts in parcels.py
git push --force-with-lease

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

🛑 **Auto-merge halted: merge conflict** — после ранее-merged PR (#88, X2 confidence) PR конфликтует с main в `backend/app/api/v1/parcels.py`. **Что нужно сделать:** ```bash git fetch origin main git checkout feat/site-finder-polygon-suitability git rebase origin/main # resolve conflicts in parcels.py git push --force-with-lease ``` Bot re-review автоматически после rebase-push. <!-- gendesign-review-bot: sha=5832dfa verdict=merge-conflict -->
lekss361 commented 2026-05-12 04:51:15 +00:00 (Migrated from github.com)

Merge conflict resolved в `bab2b95` (merge main into branch).

Что слилось:

  • 3 конфликта в `parcels.py`, `LandTab.tsx`, `site-finder.ts`.
  • Все resolved keep-both: P1 (geometry_suitability) + P2 (neighbors_summary из уже-merged #91) сосуществуют. Response dict содержит оба поля.
  • `LandTab.tsx`: NeighborsBlock (P2 hard warn) рендерится выше GeometrySuitabilityBlock (P1).
  • TS: оба optional поля в ParcelAnalysis + сохранены X2 confidence_* поля из main.

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

PR теперь MERGEABLE. Ждём bot re-review.

Merge conflict resolved в \`bab2b95\` (merge main into branch). **Что слилось:** - 3 конфликта в \`parcels.py\`, \`LandTab.tsx\`, \`site-finder.ts\`. - Все resolved keep-both: P1 (geometry_suitability) + P2 (neighbors_summary из уже-merged #91) сосуществуют. Response dict содержит оба поля. - \`LandTab.tsx\`: NeighborsBlock (P2 hard warn) рендерится выше GeometrySuitabilityBlock (P1). - TS: оба optional поля в ParcelAnalysis + сохранены X2 confidence_* поля из main. \`ruff check + format\` + \`tsc --noEmit\` + \`next lint\` — clean. PR теперь MERGEABLE. Ждём bot re-review.
lekss361 commented 2026-05-12 04:54:02 +00:00 (Migrated from github.com)

⚠️ Auto-review: minor (non-blocking) — clean rebase, core logic корректна.

Findings (все non-blocking):

  • `frontend/src/components/site-finder/GeometrySuitabilityBlock.tsx` (весь файл): Inline styles используются для static layout (grid, font sizes, padding, margins). По CLAUDE.md inline styles нужны только для динамики. Динамические `c.bg / c.fg / c.border` оправданы, но окружающий static styling должен быть Tailwind. Не блокер — сиблинги (NeighborsBlock, ConfidenceBadge) используют тот же pattern; стоит вынести в convention-cleanup ticket.

  • `backend/app/api/v1/parcels.py` (_polygon_suitability outer except): `str(e)` конкатенируется в `note` поле response: `f"Не удалось проанализировать геометрию: {e}"`. Для internal B2B tool OK, но raw Shapely/Python exception messages могут leak internal path info. Non-blocking; для future hardening pass — обернуть в generic message.

  • `backend/app/api/v1/parcels.py` (MABR block): `assert isinstance(poly, Polygon)` после MultiPolygon branch — strip-нется под `python -O`, no-op в проде. Misleading. Лучше явный type guard или просто полагаться на preceding `if`.

Positives:

  • Clean rebase — diff содержит ТОЛЬКО P1 geometry файлы.
  • No SQL injection (нет SQL в `_polygon_suitability`).
  • No N+1 (pure Shapely compute, без DB calls).
  • Score clamping корректное (`max(0.0, min(1.0, ...))`).
  • MABR `min(side_lens)` корректно даёт короткую сторону rotated rectangle.

Auto-merge через ~5 мин (idle threshold для minor + commits=4 ≤ 6).

⚠️ **Auto-review: minor (non-blocking)** — clean rebase, core logic корректна. **Findings (все non-blocking):** - \`frontend/src/components/site-finder/GeometrySuitabilityBlock.tsx\` (весь файл): Inline styles используются для static layout (grid, font sizes, padding, margins). По CLAUDE.md inline styles нужны только для динамики. Динамические \`c.bg / c.fg / c.border\` оправданы, но окружающий static styling должен быть Tailwind. Не блокер — сиблинги (NeighborsBlock, ConfidenceBadge) используют тот же pattern; стоит вынести в convention-cleanup ticket. - \`backend/app/api/v1/parcels.py\` (_polygon_suitability outer except): \`str(e)\` конкатенируется в \`note\` поле response: \`f"Не удалось проанализировать геометрию: {e}"\`. Для internal B2B tool OK, но raw Shapely/Python exception messages могут leak internal path info. Non-blocking; для future hardening pass — обернуть в generic message. - \`backend/app/api/v1/parcels.py\` (MABR block): \`assert isinstance(poly, Polygon)\` после MultiPolygon branch — strip-нется под \`python -O\`, no-op в проде. Misleading. Лучше явный type guard или просто полагаться на preceding \`if\`. **Positives:** - Clean rebase — diff содержит ТОЛЬКО P1 geometry файлы. - No SQL injection (нет SQL в \`_polygon_suitability\`). - No N+1 (pure Shapely compute, без DB calls). - Score clamping корректное (\`max(0.0, min(1.0, ...))\`). - MABR \`min(side_lens)\` корректно даёт короткую сторону rotated rectangle. Auto-merge через ~5 мин (idle threshold для minor + commits=4 ≤ 6). <!-- gendesign-review-bot: sha=bab2b95 verdict=minor -->
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#89
No description provided.