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

Merged
lekss361 merged 1 commit from feat/site-finder-x1-clean into main 2026-05-12 05:17:00 +00:00
lekss361 commented 2026-05-12 05:11:08 +00:00 (Migrated from github.com)

Summary

Replaces #87 (rebase mess — clean branch off current main).

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 (most-negative first через explicit ascending sort)
  • score_by_group: ScoreGroupTotal[] — суммарные вклады по группам (Социалка / Торговля / Парки / Транспорт / Шум/трамвай / Локация / Прочее) для stacked-bar
  • center_bonus добавлен как synthetic factor в группе «Локация», weight=1.0 (decay не применяется — bonus IS the value)
  • factor slug включает enumerate idx — prevents React key collision

FrontendScoreBreakdownPanel.tsx:

  • Stacked-bar по positive groups с tooltip
  • Топ-3 плюса (▲ зелёный) / топ-3 минуса (▼ красный) с verbal
  • Отдельная строка "Снижают балл — Шум/трамвай: ..." для negative groups (visual cue без orphan swatches)
  • Toggle «Показать все факторы (N)» → таблица с factor / contribution / % (scrollable, sticky thead)
  • Интегрирован в OverviewTab под секцией «Балл»
  • TS типы: FactorContribution, ScoreGroupTotal

Acceptance (per issue #47)

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

History note

PR #87 был на сломанной ветке (rebase + -s ours merge привнесли X2 #88 и P2 #91 в branch ancestry через второй parent — GitHub PR diff показывал их как new files, хотя blob SHA byte-identical с main). Создан этот свежий branch off current main с одним clean commit. Логически эквивалентно последней версии PR #87 включая все ранее принятые fixup'ы (centrality weight=1.0, group_totals типа float|int, unique factor key с idx, top-3 negatives explicit ascending sort, etc).

PR #87 будет закрыт после merge этого.

Test plan

  • ruff check + format — clean
  • tsc --noEmit — clean
  • npm run lint --max-warnings 0 — clean
  • После deploy: curl -X POST https://gendsgn.ru/api/v1/parcels/66:41:0204016:10/analyze | jq '.score_breakdown_detailed | length, .score_top_3_positives, .score_by_group'
  • Visual: открыть https://gendsgn.ru/site-finder, ввести 66:41:0204016:10 → секция «Почему такой балл» в Overview
## Summary Replaces #87 (rebase mess — clean branch off current main). 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` (most-negative first через explicit ascending sort) - `score_by_group: ScoreGroupTotal[]` — суммарные вклады по группам (Социалка / Торговля / Парки / Транспорт / Шум/трамвай / Локация / Прочее) для stacked-bar - center_bonus добавлен как synthetic factor в группе «Локация», `weight=1.0` (decay не применяется — bonus IS the value) - `factor` slug включает enumerate idx — prevents React key collision **Frontend** — `ScoreBreakdownPanel.tsx`: - Stacked-bar по positive groups с tooltip - Топ-3 плюса (▲ зелёный) / топ-3 минуса (▼ красный) с verbal - Отдельная строка "Снижают балл — Шум/трамвай: ..." для negative groups (visual cue без orphan swatches) - Toggle «Показать все факторы (N)» → таблица с factor / contribution / % (scrollable, sticky thead) - Интегрирован в OverviewTab под секцией «Балл» - TS типы: FactorContribution, ScoreGroupTotal ## Acceptance (per issue #47) - [x] Каждый POI в `score_breakdown_detailed` имеет verbal explanation - [x] Sum of contributions ≈ final score (sanity check passes) - [x] UI прячет полную таблицу за toggle (mobile-friendly) ## History note PR #87 был на сломанной ветке (rebase + `-s ours` merge привнесли X2 #88 и P2 #91 в branch ancestry через второй parent — GitHub PR diff показывал их как new files, хотя blob SHA byte-identical с main). Создан этот свежий branch off current main с одним clean commit. Логически эквивалентно последней версии PR #87 включая все ранее принятые fixup'ы (centrality weight=1.0, group_totals типа float|int, unique factor key с idx, top-3 negatives explicit ascending sort, etc). PR #87 будет закрыт после merge этого. ## Test plan - [x] `ruff check + format` — clean - [x] `tsc --noEmit` — clean - [x] `npm run lint --max-warnings 0` — clean - [ ] После deploy: `curl -X POST https://gendsgn.ru/api/v1/parcels/66:41:0204016:10/analyze | jq '.score_breakdown_detailed | length, .score_top_3_positives, .score_by_group'` - [ ] Visual: открыть https://gendsgn.ru/site-finder, ввести `66:41:0204016:10` → секция «Почему такой балл» в Overview
lekss361 commented 2026-05-12 05:14:25 +00:00 (Migrated from github.com)

Auto-review passed (LGTM) — clean replacement PR, all prior X1 fixes в branch'е.

Verification:

  • Diff содержит ТОЛЬКО X1 файлы: `parcels.py` (X1 changes), `ScoreBreakdownPanel.tsx` (NEW), `OverviewTab.tsx` (X1 integration), `site-finder.ts` (X1 types).
  • НЕТ `ConfidenceBadge.tsx` / `NeighborsBlock.tsx` — rebase showstopper устранён.
  • `sorted(negatives_only, key=lambda x: x["contribution"])[:3]` — корректный ordering для top-3 negatives.
  • `int(n_recent_raw)` обёрнут в try/except — guard для non-numeric recent_deals_count.
  • `import datetime as _dt` на module level.
  • Division-by-zero guarded (backend: `or 1.0`; frontend: `|| 1`).
  • Empty `factors_detailed` case handled.
  • TS strict — no `any`, новые поля optional (backward-compat).
  • No SQL injection, no auth bypass, no N+1.

Чтобы замержить — оставь approval comment (`залей` / `merge it` / `approved` / `мердж` / `lgtm`). Bot не мерджит по idle threshold (политика v4).

✅ **Auto-review passed (LGTM)** — clean replacement PR, all prior X1 fixes в branch'е. **Verification:** - ✅ Diff содержит ТОЛЬКО X1 файлы: \`parcels.py\` (X1 changes), \`ScoreBreakdownPanel.tsx\` (NEW), \`OverviewTab.tsx\` (X1 integration), \`site-finder.ts\` (X1 types). - ✅ НЕТ \`ConfidenceBadge.tsx\` / \`NeighborsBlock.tsx\` — rebase showstopper устранён. - ✅ \`sorted(negatives_only, key=lambda x: x[\"contribution\"])[:3]\` — корректный ordering для top-3 negatives. - ✅ \`int(n_recent_raw)\` обёрнут в try/except — guard для non-numeric recent_deals_count. - ✅ \`import datetime as _dt\` на module level. - ✅ Division-by-zero guarded (backend: \`or 1.0\`; frontend: \`|| 1\`). - ✅ Empty \`factors_detailed\` case handled. - ✅ TS strict — no \`any\`, новые поля optional (backward-compat). - ✅ No SQL injection, no auth bypass, no N+1. **Чтобы замержить — оставь approval comment** (\`залей\` / \`merge it\` / \`approved\` / \`мердж\` / \`lgtm\`). Bot не мерджит по idle threshold (политика v4). <!-- gendesign-review-bot: sha=d4c9930 verdict=approve -->
lekss361 commented 2026-05-12 05:15:58 +00:00 (Migrated from github.com)

approved

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