feat(site-finder): X1 score breakdown + verbal explain (#47) #87

Closed
lekss361 wants to merge 8 commits from feat/site-finder-score-breakdown into main
lekss361 commented 2026-05-11 21:44:02 +00:00 (Migrated from github.com)

Summary

Closes #47 (priority/critical, UX X1 — score explanation).

BackendPOST /parcels/{cad}/analyze теперь возвращает per-factor breakdown:

  • score_breakdown_detailed: FactorContribution[] — каждый POI как отдельный фактор с verbal explain
  • score_top_3_positives / score_top_3_negatives — для краткой сводки в UI
  • score_by_group: ScoreGroupTotal[] — суммарные вклады по группам (Социалка / Торговля / Парки / Транспорт / Шум/трамвай / Локация / Прочее) для stacked-bar
  • Каждая запись: factor, category, category_ru, group, value, weight, contribution, contribution_pct, verbal, lat, lon
  • center_bonus добавлен как synthetic factor в группе «Локация» — теперь его вклад тоже виден в breakdown
  • Skip факторов с |contribution| < 0.01 (POI > 1км) — UI шуму не нужен; sum(contribution) сходится со scored value (sanity check pass)

Frontend — новый ScoreBreakdownPanel.tsx:

  • Stacked-bar по positive groups с легендой и tooltip per-bar
  • Топ-3 плюса / топ-3 минуса с verbal под цветовыми маркерами ▲/▼
  • Кнопка «Показать все факторы (N)» → разворачивает таблицу с factor / contribution / % (scrollable, sticky thead)
  • Интегрирован в OverviewTab сразу под секцией «Балл»
  • TS типы расширены (FactorContribution, ScoreGroupTotal); strict pass

Acceptance (per issue #47)

  • Каждый POI в score_breakdown_detailed имеет verbal explanation
  • Sum of contributions ≈ final score (sanity check passes — minor delta из-за округлений)
  • UI прячет полную таблицу за toggle (mobile-friendly)

Test plan

  • npx tsc --noEmit — clean ( локально)
  • npm run lint --max-warnings 0 — clean ( локально)
  • ruff check + format — clean ( локально)
  • После deploy: curl -X POST https://gendsgn.ru/api/v1/parcels/66:41:0204016:10/analyze | jq '.score_breakdown_detailed | length' → должно быть >0
  • Visual check: открыть https://gendsgn.ru/site-finder, ввести 66:41:0204016:10 → секция «Почему такой балл» под «Балл», stacked-bar + topN + раскрываемая таблица
## Summary Closes #47 (priority/critical, UX X1 — score explanation). **Backend** — `POST /parcels/{cad}/analyze` теперь возвращает per-factor breakdown: - `score_breakdown_detailed: FactorContribution[]` — каждый POI как отдельный фактор с verbal explain - `score_top_3_positives` / `score_top_3_negatives` — для краткой сводки в UI - `score_by_group: ScoreGroupTotal[]` — суммарные вклады по группам (Социалка / Торговля / Парки / Транспорт / Шум/трамвай / Локация / Прочее) для stacked-bar - Каждая запись: `factor`, `category`, `category_ru`, `group`, `value`, `weight`, `contribution`, `contribution_pct`, `verbal`, `lat`, `lon` - center_bonus добавлен как synthetic factor в группе «Локация» — теперь его вклад тоже виден в breakdown - Skip факторов с |contribution| < 0.01 (POI > 1км) — UI шуму не нужен; sum(contribution) сходится со scored value (sanity check pass) **Frontend** — новый `ScoreBreakdownPanel.tsx`: - Stacked-bar по positive groups с легендой и tooltip per-bar - Топ-3 плюса / топ-3 минуса с verbal под цветовыми маркерами ▲/▼ - Кнопка «Показать все факторы (N)» → разворачивает таблицу с factor / contribution / % (scrollable, sticky thead) - Интегрирован в `OverviewTab` сразу под секцией «Балл» - TS типы расширены (`FactorContribution`, `ScoreGroupTotal`); strict pass ## Acceptance (per issue #47) - [x] Каждый POI в `score_breakdown_detailed` имеет verbal explanation - [x] Sum of contributions ≈ final score (sanity check passes — minor delta из-за округлений) - [x] UI прячет полную таблицу за toggle (mobile-friendly) ## Test plan - [ ] `npx tsc --noEmit` — clean (✅ локально) - [ ] `npm run lint --max-warnings 0` — clean (✅ локально) - [ ] `ruff check + format` — clean (✅ локально) - [ ] После deploy: `curl -X POST https://gendsgn.ru/api/v1/parcels/66:41:0204016:10/analyze | jq '.score_breakdown_detailed | length'` → должно быть >0 - [ ] Visual check: открыть https://gendsgn.ru/site-finder, ввести `66:41:0204016:10` → секция «Почему такой балл» под «Балл», stacked-bar + topN + раскрываемая таблица
lekss361 commented 2026-05-11 21:51:38 +00:00 (Migrated from github.com)

⚠️ Auto-review: minor (non-blocking) — 0 блокеров, 5 рекомендаций + 1 организационная.

Backend (parcels.py):

  1. Semantic inconsistency weight field в centrality factor (~line 620). Для POI-факторов: contribution = weight * decay. Для centrality: weight == contribution (decay не применяется, bonus IS the weight). Если будущий UI/PDF-export покажет weight отдельно от contribution — centrality выглядит wrong. Fix: "weight": 1.0 либо "weight": None чтобы сигналить "no separate decay". Trap для X3 PDF report (следующий sprint item).

  2. group_totals: dict[str, dict[str, float]] — неверная аннотация (~line 996). count это int, не float. Файл не под mypy strict (CLAUDE.md scope: app.services.generative.*), CI не упадёт, но hint вводит в заблуждение. Чище: TypedDict для group row, либо dict[str, float | int].

  3. score_top_3_negatives ordering asymmetry. Для positives [:3] от descending sort → most-positive first. Для negatives [-3:][::-1] → most-negative first (тоже "dominant first"). Convention консистентна, но не очевидна — одна строка-комментарий предотвратит misread.

Frontend (ScoreBreakdownPanel.tsx):

  1. Stacked bar legend orphan для negative groups. positiveGroups = byGroup.filter(g => g.contribution > 0) — "Шум/трамвай" не попадает в bar, но byGroup.map рисует все legend swatches. Пользователь увидит красный квадратик "Шум/трамвай" без bar-сегмента. Варианты: (a) скрыть legend entry если нет bar segment, (b) показать negatives как separate "drag" section. Сейчас → cosmetic gap.

  2. Stacked bar — relative mix, не absolute magnitude. totalPositive нормализует до 100%, bar всегда заполнен. Parcel со score 0.4 (одна школа близко) и parcel со score 3.2 выглядят одинаково в баре. Если рядом крупно отображается итоговый score — норм. Если нет — visual misleading.

Organizational:

  1. Vault entry не обновлён — X1 = issue #47 в Sprint4_SiteFinder_v4. Per CLAUDE.md "Обновляй vault для нетривиальных изменений" — добавь observation в sprints/Sprint4_SiteFinder_v4.md или MOC что #47 shipped.

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

  • 4 новых response field'а добавлены как optional (?) → backward-compatible.
  • abs_total || 1.0 и abs(contribution) < 0.01 skip — clean defensive patterns.
  • _verbal_for_poi корректно хендлит empty/whitespace/dash names, использует (Unicode minus).
  • TS strict, типы FactorContribution/ScoreGroupTotal — fully typed, no any.
  • Sort once, reuse 3x — нет лишних passes.
⚠️ **Auto-review: minor (non-blocking)** — 0 блокеров, 5 рекомендаций + 1 организационная. **Backend (parcels.py):** 1. **Semantic inconsistency `weight` field в centrality factor** (~line 620). Для POI-факторов: `contribution = weight * decay`. Для centrality: `weight == contribution` (decay не применяется, bonus IS the weight). Если будущий UI/PDF-export покажет `weight` отдельно от `contribution` — centrality выглядит wrong. Fix: `"weight": 1.0` либо `"weight": None` чтобы сигналить "no separate decay". Trap для X3 PDF report (следующий sprint item). 2. **`group_totals: dict[str, dict[str, float]]` — неверная аннотация** (~line 996). `count` это `int`, не `float`. Файл не под mypy strict (CLAUDE.md scope: `app.services.generative.*`), CI не упадёт, но hint вводит в заблуждение. Чище: `TypedDict` для group row, либо `dict[str, float | int]`. 3. **`score_top_3_negatives` ordering asymmetry**. Для positives `[:3]` от descending sort → most-positive first. Для negatives `[-3:][::-1]` → most-negative first (тоже "dominant first"). Convention консистентна, но не очевидна — одна строка-комментарий предотвратит misread. **Frontend (ScoreBreakdownPanel.tsx):** 4. **Stacked bar legend orphan для negative groups**. `positiveGroups = byGroup.filter(g => g.contribution > 0)` — "Шум/трамвай" не попадает в bar, но `byGroup.map` рисует все legend swatches. Пользователь увидит красный квадратик "Шум/трамвай" без bar-сегмента. Варианты: (a) скрыть legend entry если нет bar segment, (b) показать negatives как separate "drag" section. Сейчас → cosmetic gap. 5. **Stacked bar — relative mix, не absolute magnitude**. `totalPositive` нормализует до 100%, bar всегда заполнен. Parcel со score 0.4 (одна школа близко) и parcel со score 3.2 выглядят одинаково в баре. Если рядом крупно отображается итоговый score — норм. Если нет — visual misleading. **Organizational:** 6. **Vault entry не обновлён** — X1 = issue #47 в `Sprint4_SiteFinder_v4`. Per CLAUDE.md "Обновляй vault для нетривиальных изменений" — добавь observation в `sprints/Sprint4_SiteFinder_v4.md` или MOC что #47 shipped. **Что хорошо (✅):** - 4 новых response field'а добавлены как optional (`?`) → backward-compatible. - `abs_total || 1.0` и `abs(contribution) < 0.01` skip — clean defensive patterns. - `_verbal_for_poi` корректно хендлит empty/whitespace/dash names, использует `−` (Unicode minus). - TS strict, типы `FactorContribution`/`ScoreGroupTotal` — fully typed, no `any`. - Sort once, reuse 3x — нет лишних passes. <!-- gendesign-review-bot: sha=1d1c169 verdict=minor -->
lekss361 commented 2026-05-11 21:55:00 +00:00 (Migrated from github.com)

Все 5 рекомендаций (+ 1 организационная) применены в `97dc4ba`:

Backend:

  1. weight=1.0 для centrality factor (semantic — decay не применяется, bonus IS the value). Защищает future PDF/UI которые могут показывать weight отдельно.
  2. group_totals: dict[str, dict[str, float | int]]count это int, остальное float.
  3. Inline-комментарий перед top-3 sort: "positives [:3] от descending → most-positive first; negatives [-3:][::-1] → most-negative first; оба dominant first".

Frontend:
4. Legend orphan fix: positive groups → legend под баром; negative groups → отдельная строка "Снижают балл — {group}: {value}" с красным цветом значения. Никаких swatch'ей без bar-сегмента.
5. ℹ️ Magnitude vs mix — оставлено как есть. Score ({N} / 40 · label) крупно отображается в KPI-карточке выше; bar показывает состав, не абсолют — это явный design choice, не misleading в текущем layout.

Organizational:
6. Vault entry обновлён: sprints/Sprint4_SiteFinder_v4.md → "Update 2026-05-12 — Shipped batch" с #47/#48/#45 переведёнными в shipped.

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

PR scope: frontend/src/components/site-finder/** UI + backend/app/api/v1/** response extension — частично blocked (backend API). Жду явный approval от @lekss361 для merge.

Все 5 рекомендаций (+ 1 организационная) применены в \`97dc4ba\`: **Backend:** 1. ✅ `weight=1.0` для centrality factor (semantic — decay не применяется, bonus IS the value). Защищает future PDF/UI которые могут показывать weight отдельно. 2. ✅ `group_totals: dict[str, dict[str, float | int]]` — `count` это int, остальное float. 3. ✅ Inline-комментарий перед top-3 sort: "positives [:3] от descending → most-positive first; negatives [-3:][::-1] → most-negative first; оба dominant first". **Frontend:** 4. ✅ Legend orphan fix: positive groups → legend под баром; negative groups → отдельная строка "Снижают балл — {group}: {value}" с красным цветом значения. Никаких swatch'ей без bar-сегмента. 5. ℹ️ Magnitude vs mix — оставлено как есть. Score (`{N} / 40 · label`) крупно отображается в KPI-карточке выше; bar показывает состав, не абсолют — это явный design choice, не misleading в текущем layout. **Organizational:** 6. ✅ Vault entry обновлён: `sprints/Sprint4_SiteFinder_v4.md` → "Update 2026-05-12 — Shipped batch" с #47/#48/#45 переведёнными в shipped. `ruff check + format` + `tsc --noEmit` + `lint` — clean. PR scope: `frontend/src/components/site-finder/**` UI + `backend/app/api/v1/**` response extension — частично blocked (backend API). Жду явный approval от @lekss361 для merge.
lekss361 commented 2026-05-11 22:08:55 +00:00 (Migrated from github.com)

⚠️ Auto-review: minor (non-blocking) — 4 из 6 прошлых находок исправлены, 1 новая.

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

  1. Centrality weight=1.0 + комментарий "no decay multiplier" → правильная semantic separation.
  2. group_totals: dict[str, dict[str, float | int]] + inline комментарий про count=int.
  3. score_top_3_negatives ordering comment — explicit "dominant first" convention документирована.
  4. Negative groups legend — отдельные "Снижают балл — " строки под баром, нет orphan swatches.

Прошлые non-blocking, не закрыты:
5. Stacked bar normalized to 100% (acceptable — числовые значения видны рядом).
6. Vault entry для X1/#47 — нашёл в sprints/Sprint4_SiteFinder_v4.md, всё OK.

Новая находка:

  1. React key collision risk (parcels.py:539, frontend ScoreBreakdownPanel.tsx:348). Factor key f"{cat}_{round(distance_m)}m" — два POI одной категории на одинаковом округлённом расстоянии (например, две школы по 450м) дадут duplicate keys school_450m, что приведёт к silent React reconciliation errors. Низкая вероятность в общем случае, но реалистично для плотных районов (центр ЕКБ, несколько аптек рядом).

    Fix (1 строка): добавить enumerate-индекс или osm_id в factor key:

    for idx, p in enumerate(poi_rows):
        ...
        "factor": f"{cat}_{round(distance_m)}m_{idx}",
    

Не блокирует, но лучше поправить.

⚠️ **Auto-review: minor (non-blocking)** — 4 из 6 прошлых находок исправлены, 1 новая. **Исправлено (✅):** 1. **Centrality weight=1.0** + комментарий "no decay multiplier" → правильная semantic separation. 2. **`group_totals: dict[str, dict[str, float | int]]`** + inline комментарий про count=int. 3. **`score_top_3_negatives` ordering comment** — explicit "dominant first" convention документирована. 4. **Negative groups legend** — отдельные "Снижают балл — <group>" строки под баром, нет orphan swatches. **Прошлые non-blocking, не закрыты:** 5. Stacked bar normalized to 100% (acceptable — числовые значения видны рядом). 6. Vault entry для X1/#47 — нашёл в `sprints/Sprint4_SiteFinder_v4.md`, всё OK. **Новая находка:** 7. **React key collision risk** (parcels.py:539, frontend ScoreBreakdownPanel.tsx:348). Factor key `f"{cat}_{round(distance_m)}m"` — два POI одной категории на одинаковом округлённом расстоянии (например, две школы по 450м) дадут duplicate keys `school_450m`, что приведёт к silent React reconciliation errors. Низкая вероятность в общем случае, но реалистично для плотных районов (центр ЕКБ, несколько аптек рядом). **Fix (1 строка)**: добавить enumerate-индекс или osm_id в factor key: ```python for idx, p in enumerate(poi_rows): ... "factor": f"{cat}_{round(distance_m)}m_{idx}", ``` Не блокирует, но лучше поправить. <!-- gendesign-review-bot: sha=97dc4ba verdict=minor -->
lekss361 commented 2026-05-11 22:14:47 +00:00 (Migrated from github.com)

Применено в c46637a: factor slug теперь включает enumerate-индекс ({cat}_{distance}m_{idx}), prevents React key collision когда два POI одной категории совпадают по округлённому расстоянию. Backward compat — factor opaque slug.

Применено в `c46637a`: factor slug теперь включает enumerate-индекс (`{cat}_{distance}m_{idx}`), prevents React key collision когда два POI одной категории совпадают по округлённому расстоянию. Backward compat — factor opaque slug.
lekss361 commented 2026-05-11 22:20:06 +00:00 (Migrated from github.com)

Auto-review passed (LGTM) — React key collision fix принят.

f"{cat}_{round(distance_m)}m_{idx}" гарантирует уникальный key в пределах poi_rows. Комментарий объясняет rationale ("two аптеки 450м в плотном районе"). Frontend key={f.factor} продолжает работать — factor по-прежнему string.

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

✅ **Auto-review passed (LGTM)** — React key collision fix принят. `f"{cat}_{round(distance_m)}m_{idx}"` гарантирует уникальный key в пределах `poi_rows`. Комментарий объясняет rationale ("two аптеки 450м в плотном районе"). Frontend `key={f.factor}` продолжает работать — factor по-прежнему string. Auto-merge через ~3 мин (idle threshold для approve verdict + commits ≤ 6). <!-- gendesign-review-bot: sha=c46637a verdict=approve -->
lekss361 commented 2026-05-11 22:26:07 +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-score-breakdown
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-score-breakdown git rebase origin/main # resolve conflicts in parcels.py git push --force-with-lease ``` Bot re-review автоматически после rebase-push. <!-- gendesign-review-bot: sha=c46637a verdict=merge-conflict -->
lekss361 commented 2026-05-12 04:45:04 +00:00 (Migrated from github.com)

Resolved merge conflict в `d0fa8a3`.

Что сделано:

  1. Локально rebase X1 commits на свежий `origin/main` (содержит X2 #88 + P2 #91).
  2. Конфликт в `backend/app/api/v1/parcels.py` разрешён вручную — keep both: X1 breakdown computation (factors_sorted / score_top_3 / score_by_group) и X2 confidence_info блок идут друг за другом перед `return`.
  3. `git merge -s ours origin/feat/site-finder-score-breakdown` чтобы инкорпорировать old remote branch как ancestor — non-force push прошёл fast-forward.
  4. `ruff check + format` + `tsc --noEmit` + `next lint --max-warnings 0` — clean.

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

Resolved merge conflict в \`d0fa8a3\`. **Что сделано:** 1. Локально rebase X1 commits на свежий \`origin/main\` (содержит X2 #88 + P2 #91). 2. Конфликт в \`backend/app/api/v1/parcels.py\` разрешён вручную — keep both: X1 breakdown computation (factors_sorted / score_top_3 / score_by_group) **и** X2 confidence_info блок идут друг за другом перед \`return\`. 3. \`git merge -s ours origin/feat/site-finder-score-breakdown\` чтобы инкорпорировать old remote branch как ancestor — non-force push прошёл fast-forward. 4. \`ruff check + format\` + \`tsc --noEmit\` + \`next lint --max-warnings 0\` — clean. PR теперь MERGEABLE. Ждём bot re-review.
lekss361 commented 2026-05-12 04:48:53 +00:00 (Migrated from github.com)

Changes requested — rebase re-introduced files уже на main.

Showstopper (rebase mishandled):

  • `frontend/src/components/site-finder/ConfidenceBadge.tsx` — уже на main (landed via PR #88).
  • `frontend/src/components/site-finder/NeighborsBlock.tsx` — уже на main (landed via PR #91).

Diff показывает оба файла как `new file` — squash-merge упрётся в duplicate-add conflict. Rebase replay-нул commits, чьё содержимое уже было в main после squash-merge тех PR, и git не detect-нул их как noop.

Что нужно сделать:
```bash
git checkout feat/site-finder-score-breakdown
git fetch origin main

Reset до точки расхождения и cherry-pick только X1 commits:

git rebase -i origin/main

В редакторе УДАЛИ строки с commits "X2 confidence" и "P2 neighbors"

(3777a77, 843314c, 1002214, b5a2181, ec68144 — duplicates после rebase)

Оставь только X1-related: 1d1c169 / 97dc4ba / c46637a

git push --force-with-lease
```

Logic bugs (надо починить в том же fixup):

  • `backend/app/api/v1/parcels.py` (score_top_3_negatives): `[-3:][::-1]` на descending-sorted даёт "least-negative first", не "most-negative first" как в комментарии. Убери `[::-1]`.
  • `backend/app/api/v1/parcels.py` (_compute_confidence): `int(market_trend["recent_deals_count"])` без guard — крашнет 500 на non-numeric string. Замени на `int(market_trend.get("recent_deals_count") or 0)`.

Style minor (на твой выбор):

  • `import datetime as _dt` function-scoped — лучше module-level.

Fix and push — bot re-review автоматически.

❌ **Changes requested** — rebase re-introduced files уже на main. **Showstopper (rebase mishandled):** - \`frontend/src/components/site-finder/ConfidenceBadge.tsx\` — уже на main (landed via PR #88). - \`frontend/src/components/site-finder/NeighborsBlock.tsx\` — уже на main (landed via PR #91). Diff показывает оба файла как \`new file\` — squash-merge упрётся в duplicate-add conflict. Rebase replay-нул commits, чьё содержимое уже было в main после squash-merge тех PR, и git не detect-нул их как noop. **Что нужно сделать:** \`\`\`bash git checkout feat/site-finder-score-breakdown git fetch origin main # Reset до точки расхождения и cherry-pick только X1 commits: git rebase -i origin/main # В редакторе УДАЛИ строки с commits "X2 confidence" и "P2 neighbors" # (3777a77, 843314c, 1002214, b5a2181, ec68144 — duplicates после rebase) # Оставь только X1-related: 1d1c169 / 97dc4ba / c46637a git push --force-with-lease \`\`\` **Logic bugs (надо починить в том же fixup):** - \`backend/app/api/v1/parcels.py\` (score_top_3_negatives): \`[-3:][::-1]\` на descending-sorted даёт "least-negative first", не "most-negative first" как в комментарии. Убери \`[::-1]\`. - \`backend/app/api/v1/parcels.py\` (_compute_confidence): \`int(market_trend["recent_deals_count"])\` без guard — крашнет 500 на non-numeric string. Замени на \`int(market_trend.get("recent_deals_count") or 0)\`. **Style minor (на твой выбор):** - \`import datetime as _dt\` function-scoped — лучше module-level. Fix and push — bot re-review автоматически. <!-- gendesign-review-bot: sha=d0fa8a3 verdict=changes -->
lekss361 commented 2026-05-12 04:57:53 +00:00 (Migrated from github.com)

Changes requested (re-review #2) — logic bugs fixed, но rebase showstopper остался.

Подтверждено:
`a6e95b6` починил все 3 logic bug-а: `score_top_3_negatives` ordering, `int()` guard в `_compute_confidence`, module-level `datetime` import. Otlichno.

Что НЕ исправлено — rebase всё ещё re-adds файлы с main:

  • `frontend/src/components/site-finder/ConfidenceBadge.tsx` — на main SHA `195e18a` (landed via PR #88).
  • `frontend/src/components/site-finder/NeighborsBlock.tsx` — на main SHA `f83ec87` (landed via PR #91).

Diff PR-а всё ещё показывает их как `new file` — squash-merge упадёт.

Проблема в commit history:
```
3777a77 feat(site-finder): X2 confidence indicator + caveats (#48) (#88) ← из merged PR #88, нельзя в PR #87
843314c feat(site-finder): P2 cad_buildings соседи + overlap check (#46) (#91) ← из merged PR #91, нельзя в PR #87
1002214 / b5a2181 / ec68144 ← duplicates X1 commits после второго rebase
d0fa8a3 merge: incorporate remote feat/site-finder-score-breakdown ← merge commit смешал всё
```

Как починить (один из 2 вариантов):

Вариант A — interactive rebase (рекомендую):
```bash
git fetch origin main
git rebase -i origin/main

В редакторе УДАЛИ строки (drop) для:

3777a77 feat(site-finder): X2 confidence... ← из #88

843314c feat(site-finder): P2 cad_buildings... ← из #91

1002214, b5a2181, ec68144 ← duplicates

Оставь ТОЛЬКО оригинальные X1 commits:

1d1c169 feat(site-finder): X1 score breakdown + verbal explain (#47)

97dc4ba fix(site-finder): address PR #87 auto-review minor feedback

c46637a fix(site-finder): unique factor key (X1 #47)

a6e95b6 fix(site-finder): X1 logic bugs from PR #87 re-review

git push --force-with-lease
```

Вариант B — fresh branch (если interactive rebase запутает):
```bash
git fetch origin main
git checkout -b feat/site-finder-score-breakdown-clean origin/main
git cherry-pick 1d1c169 97dc4ba c46637a a6e95b6
git push -u origin feat/site-finder-score-breakdown-clean
gh pr create --title "feat(site-finder): X1 score breakdown + verbal explain (#47)" --body "replaces #87 — clean branch without P2/X2 contamination"
gh pr close 87
```

После cleanup diff должен показать ТОЛЬКО:

  • `backend/app/api/v1/parcels.py` (X1 changes)
  • `frontend/src/components/site-finder/ScoreBreakdownPanel.tsx` (NEW)
  • `frontend/src/components/site-finder/OverviewTab.tsx` (X1 integration)
  • `frontend/src/types/site-finder.ts` (X1 types only)

Fix and push — bot re-review автоматически.

❌ **Changes requested (re-review #2)** — logic bugs fixed, но rebase showstopper остался. **Подтверждено:** ✅ \`a6e95b6\` починил все 3 logic bug-а: \`score_top_3_negatives\` ordering, \`int()\` guard в \`_compute_confidence\`, module-level \`datetime\` import. Otlichno. **Что НЕ исправлено — rebase всё ещё re-adds файлы с main:** - \`frontend/src/components/site-finder/ConfidenceBadge.tsx\` — на main SHA \`195e18a\` (landed via PR #88). - \`frontend/src/components/site-finder/NeighborsBlock.tsx\` — на main SHA \`f83ec87\` (landed via PR #91). Diff PR-а всё ещё показывает их как \`new file\` — squash-merge упадёт. **Проблема в commit history:** \`\`\` 3777a77 feat(site-finder): X2 confidence indicator + caveats (#48) (#88) ← из merged PR #88, нельзя в PR #87 843314c feat(site-finder): P2 cad_buildings соседи + overlap check (#46) (#91) ← из merged PR #91, нельзя в PR #87 1002214 / b5a2181 / ec68144 ← duplicates X1 commits после второго rebase d0fa8a3 merge: incorporate remote feat/site-finder-score-breakdown ← merge commit смешал всё \`\`\` **Как починить (один из 2 вариантов):** **Вариант A — interactive rebase (рекомендую):** \`\`\`bash git fetch origin main git rebase -i origin/main # В редакторе УДАЛИ строки (drop) для: # 3777a77 feat(site-finder): X2 confidence... ← из #88 # 843314c feat(site-finder): P2 cad_buildings... ← из #91 # 1002214, b5a2181, ec68144 ← duplicates # Оставь ТОЛЬКО оригинальные X1 commits: # 1d1c169 feat(site-finder): X1 score breakdown + verbal explain (#47) # 97dc4ba fix(site-finder): address PR #87 auto-review minor feedback # c46637a fix(site-finder): unique factor key (X1 #47) # a6e95b6 fix(site-finder): X1 logic bugs from PR #87 re-review git push --force-with-lease \`\`\` **Вариант B — fresh branch (если interactive rebase запутает):** \`\`\`bash git fetch origin main git checkout -b feat/site-finder-score-breakdown-clean origin/main git cherry-pick 1d1c169 97dc4ba c46637a a6e95b6 git push -u origin feat/site-finder-score-breakdown-clean gh pr create --title \"feat(site-finder): X1 score breakdown + verbal explain (#47)\" --body \"replaces #87 — clean branch without P2/X2 contamination\" gh pr close 87 \`\`\` После cleanup diff должен показать ТОЛЬКО: - \`backend/app/api/v1/parcels.py\` (X1 changes) - \`frontend/src/components/site-finder/ScoreBreakdownPanel.tsx\` (NEW) - \`frontend/src/components/site-finder/OverviewTab.tsx\` (X1 integration) - \`frontend/src/types/site-finder.ts\` (X1 types only) Fix and push — bot re-review автоматически. <!-- gendesign-review-bot: sha=a6e95b6 verdict=changes -->
lekss361 commented 2026-05-12 04:59:41 +00:00 (Migrated from github.com)

Logic bugs fixed в `a6e95b6`:

  1. `score_top_3_negatives` — переписал unambiguously через explicit ascending sort:
    ```python
    negatives_only = [f for f in factors_sorted if f["contribution"] < 0]
    score_top_3_negatives = sorted(negatives_only, key=lambda x: x["contribution"])[:3]
    ```
    Теперь "most-negative first" очевидно из кода, без хитростей slicing.

  2. `_compute_confidence(recent_deals_count)` — guard через try/except (ValueError, TypeError) + `or 0` fallback. Защищён от non-numeric строк.

  3. `import datetime as _dt` — перенесён в module-level imports (top of file).

Про "rebase mishandled" / "duplicate-add conflict":

Проверил вручную — это false alarm:

```bash
$ git ls-tree HEAD ConfidenceBadge.tsx
100644 blob 195e18a...

$ git ls-tree origin/main ConfidenceBadge.tsx
100644 blob 195e18a...
```

Содержимое `ConfidenceBadge.tsx` и `NeighborsBlock.tsx` на branch tip byte-identical с current `origin/main` (одинаковый blob SHA). `git diff origin/main..HEAD` (2-dot) показывает только 4 измененных файла (parcels.py, OverviewTab, ScoreBreakdownPanel, site-finder.ts) — X1-only.

PR diff display показывает их как "new file" потому что GitHub'овский `baseRefOid` фиксирован на `485c221` (main снэпшот когда PR создавался, до merge #88 и #91), а 3-dot `merge-base..head` всё ещё проходит через ту точку. Это косметика отображения, а squash-merge использует current main tip → произведёт чистый коммит с X1-only изменениями.

Подтверждаю:

  • `gh pr view 87 --json mergeable,mergeStateStatus` → `{mergeable: MERGEABLE, mergeStateStatus: CLEAN}` — конфликтов нет
  • Tree branch tip = main tree + X1 changes only (verified via ls-tree blob hashes)

Ждём bot re-review на `a6e95b6`.

**Logic bugs fixed в \`a6e95b6\`:** 1. ✅ \`score_top_3_negatives\` — переписал unambiguously через explicit ascending sort: \`\`\`python negatives_only = [f for f in factors_sorted if f["contribution"] < 0] score_top_3_negatives = sorted(negatives_only, key=lambda x: x["contribution"])[:3] \`\`\` Теперь "most-negative first" очевидно из кода, без хитростей slicing. 2. ✅ \`_compute_confidence(recent_deals_count)\` — guard через try/except (ValueError, TypeError) + \`or 0\` fallback. Защищён от non-numeric строк. 3. ✅ \`import datetime as _dt\` — перенесён в module-level imports (top of file). **Про "rebase mishandled" / "duplicate-add conflict":** Проверил вручную — это **false alarm**: \`\`\`bash $ git ls-tree HEAD ConfidenceBadge.tsx 100644 blob 195e18a... $ git ls-tree origin/main ConfidenceBadge.tsx 100644 blob 195e18a... \`\`\` Содержимое \`ConfidenceBadge.tsx\` и \`NeighborsBlock.tsx\` на branch tip **byte-identical** с current \`origin/main\` (одинаковый blob SHA). \`git diff origin/main..HEAD\` (2-dot) показывает только 4 измененных файла (parcels.py, OverviewTab, ScoreBreakdownPanel, site-finder.ts) — X1-only. PR diff display показывает их как "new file" потому что GitHub'овский \`baseRefOid\` фиксирован на \`485c221\` (main снэпшот когда PR создавался, до merge #88 и #91), а 3-dot \`merge-base..head\` всё ещё проходит через ту точку. Это **косметика отображения**, а squash-merge использует current main tip → произведёт чистый коммит с X1-only изменениями. Подтверждаю: - \`gh pr view 87 --json mergeable,mergeStateStatus\` → \`{mergeable: MERGEABLE, mergeStateStatus: CLEAN}\` — конфликтов нет - Tree branch tip = main tree + X1 changes only (verified via ls-tree blob hashes) Ждём bot re-review на \`a6e95b6\`.
lekss361 commented 2026-05-12 05:11:19 +00:00 (Migrated from github.com)

Закрываю в пользу #92 — clean branch off current main без rebase mess. Все 3 logic bug-фикса и весь feedback из re-review #2 уже включены в свежий commit. См. #92 для финальной версии X1 score breakdown.

Закрываю в пользу #92 — clean branch off current main без rebase mess. Все 3 logic bug-фикса и весь feedback из re-review #2 уже включены в свежий commit. См. #92 для финальной версии X1 score breakdown.

Pull request closed

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#87
No description provided.