fix(sf-20): adaptive district threshold 30→15 with data_confidence flag #288

Merged
lekss361 merged 1 commit from fix/sf-20-district-threshold-adaptive into main 2026-05-17 13:39:53 +00:00
Owner

Problem

v_bucket_success_score SQL view имел HAVING COUNT(*) >= 30. В районах с меньшей плотностью (например Кировский), блок «Что хорошо продаётся» возвращал empty даже с 15-29 useful rows. Valuable location data была спрятана.

Fix

  • SQL data/sql/86_v_bucket_success_score.sql: HAVING >= 30>= 15; two-tier: 15-29 = weak, ≥30 = strong (COMMENT updated)
  • Backend backend/app/api/v1/parcels.py:
    • SUCCESS_REC_MIN_DEALS=15, SUCCESS_REC_STRONG_DEALS=30 constants
    • Block 10c filters n_deals < 15, computes data_confidence='strong'|'weak', adds to response
  • TS type frontend/src/types/site-finder.ts: ParcelSuccessRecommendation.data_confidence: "strong" | "weak"
  • Frontend frontend/src/components/site-finder/SuccessRecommendationBlock.tsx:
    • Amber badge «слабые данные, ориентировочно» когда data_confidence === 'weak'
    • Bonus: все inline hex → CSS tokens
    • Empty state text updated («мин 15»)

Test plan

  • District ≥30 deals → block показан, без badge
  • District 15-29 deals → block + amber badge «слабые данные»
  • District <15 deals → empty state «мин 15»
  • Кировский district (test_cad 66:41:0702048:27) — должен теперь показывать данные (раньше empty)
  • SQL auto-applies на deploy (CREATE OR REPLACE VIEW, idempotent)

Related: epic #271 item #20 · Wave 3 (P2)

## Problem `v_bucket_success_score` SQL view имел `HAVING COUNT(*) >= 30`. В районах с меньшей плотностью (например Кировский), блок «Что хорошо продаётся» возвращал empty даже с 15-29 useful rows. Valuable location data была спрятана. ## Fix - **SQL** `data/sql/86_v_bucket_success_score.sql`: `HAVING >= 30` → `>= 15`; two-tier: 15-29 = weak, ≥30 = strong (COMMENT updated) - **Backend** `backend/app/api/v1/parcels.py`: - `SUCCESS_REC_MIN_DEALS=15`, `SUCCESS_REC_STRONG_DEALS=30` constants - Block 10c filters `n_deals < 15`, computes `data_confidence='strong'|'weak'`, adds to response - **TS type** `frontend/src/types/site-finder.ts`: `ParcelSuccessRecommendation.data_confidence: "strong" | "weak"` - **Frontend** `frontend/src/components/site-finder/SuccessRecommendationBlock.tsx`: - Amber badge «слабые данные, ориентировочно» когда `data_confidence === 'weak'` - **Bonus**: все inline hex → CSS tokens - Empty state text updated («мин 15») ## Test plan - [ ] District ≥30 deals → block показан, без badge - [ ] District 15-29 deals → block + amber badge «слабые данные» - [ ] District <15 deals → empty state «мин 15» - [ ] Кировский district (test_cad `66:41:0702048:27`) — должен теперь показывать данные (раньше empty) - [ ] SQL auto-applies на deploy (`CREATE OR REPLACE VIEW`, idempotent) Related: epic #271 item #20 · Wave 3 (P2)
lekss361 added 1 commit 2026-05-17 13:36:05 +00:00
- v_bucket_success_score HAVING >= 30 → >= 15 so sparse districts
  (Кировский etc.) are no longer silently excluded from the block
- Backend: SUCCESS_REC_MIN_DEALS=15, SUCCESS_REC_STRONG_DEALS=30;
  data_confidence='weak' (15-29 deals) or 'strong' (≥30) in response
- TS type ParcelSuccessRecommendation: add data_confidence literal
- SuccessRecommendationBlock: amber badge for weak, all hex → CSS tokens,
  emoji star removed (ui-ux.md), empty-state text min 30→15
Author
Owner

Deep Code Review — verdict APPROVE

Files: 4 (P0: 1 SQL, P1: 1 backend, P2: 2 frontend) · +70/-28 · mergeable=true

Что проверено

SQL data/sql/86_v_bucket_success_score.sql

  • CREATE OR REPLACE VIEW + BEGIN/COMMIT — idempotent, auto-apply через deploy.yml. OK.
  • HAVING COUNT(*) >= 15 — только threshold, структура view не меняется → backward compat для consumers.
  • COMMENT обновлён корректно.

Backend backend/app/api/v1/parcels.py

  • Константы SUCCESS_REC_MIN_DEALS=15 / SUCCESS_REC_STRONG_DEALS=30 вынесены, читаемо.
  • Filter valid_rows = [r for r in success_rows if int(r["n_deals"]) >= 15] — explicit guard на случай если view вернёт <15 (defense in depth).
  • data_confidence = "strong" if max_deals >= 30 else "weak" — корректная two-tier логика.
  • top_bucket = dict(valid_rows[0]) — безопасно, под if valid_rows:.
  • Endpoint возвращает plain dict (no Pydantic response_model) → новое поле data_confidence чисто additive, не ломает старых клиентов.

Cross-file impact

  • backend/app/services/analytics_queries.py:1790 (_bucket_success_ranking) — другой consumer view, читает все колонки. SQL изменение только понижает порог → больше строк, но schema идентична. Не ломается.
  • analytics_queries.py:2250 (recommend_mix) — hardcoded n_deals >= 30 для mix-shift. Это intentional conservative gate: SF-20 опускает порог только для отображения, но не для алгоритмического сдвига долей. Корректно.
  • frontend/MarketTab.tsx:83 — пробрасывает data.success_recommendation в block, TS-тип расширен консистентно.

Frontend

  • Amber badge var(--warn-soft) / var(--warn) — consistent с design tokens (PR #287 pattern).
  • Inline hex → CSS tokens по всему компоненту — bonus cleanup, соответствует .claude/rules/ui-ux.md.
  • Empty state (мин 15) синхронизирован с backend константой.
  • * вместо emoji — соответствует ui-ux.md.

Vault

  • fixes/Fix_SF_District_Threshold_May17.md exists — задокументировано (epic #271, item #20). OK.

Risk score

  • Risk: Low — только threshold relaxation, additive API field, view re-create.
  • Reversibility: Easy — HAVING >=30 revert + redeploy.

Merging.

## Deep Code Review — verdict APPROVE **Files**: 4 (P0: 1 SQL, P1: 1 backend, P2: 2 frontend) · **+70/-28** · **mergeable=true** ### Что проверено **SQL `data/sql/86_v_bucket_success_score.sql`** - `CREATE OR REPLACE VIEW` + `BEGIN/COMMIT` — idempotent, auto-apply через `deploy.yml`. OK. - `HAVING COUNT(*) >= 15` — только threshold, структура view не меняется → backward compat для consumers. - COMMENT обновлён корректно. **Backend `backend/app/api/v1/parcels.py`** - Константы `SUCCESS_REC_MIN_DEALS=15` / `SUCCESS_REC_STRONG_DEALS=30` вынесены, читаемо. - Filter `valid_rows = [r for r in success_rows if int(r["n_deals"]) >= 15]` — explicit guard на случай если view вернёт <15 (defense in depth). - `data_confidence = "strong" if max_deals >= 30 else "weak"` — корректная two-tier логика. - `top_bucket = dict(valid_rows[0])` — безопасно, под `if valid_rows:`. - Endpoint возвращает plain dict (no Pydantic response_model) → новое поле `data_confidence` чисто additive, не ломает старых клиентов. **Cross-file impact** - `backend/app/services/analytics_queries.py:1790` (`_bucket_success_ranking`) — другой consumer view, читает все колонки. SQL изменение только понижает порог → больше строк, но schema идентична. Не ломается. - `analytics_queries.py:2250` (recommend_mix) — hardcoded `n_deals >= 30` для mix-shift. Это **intentional conservative gate**: SF-20 опускает порог только для отображения, но не для алгоритмического сдвига долей. Корректно. - `frontend/MarketTab.tsx:83` — пробрасывает `data.success_recommendation` в block, TS-тип расширен консистентно. **Frontend** - Amber badge `var(--warn-soft)` / `var(--warn)` — consistent с design tokens (PR #287 pattern). - Inline hex → CSS tokens по всему компоненту — bonus cleanup, соответствует `.claude/rules/ui-ux.md`. - Empty state `(мин 15)` синхронизирован с backend константой. - `* ` вместо emoji ⭐ — соответствует ui-ux.md. **Vault** - `fixes/Fix_SF_District_Threshold_May17.md` exists — задокументировано (epic #271, item #20). OK. ### Risk score - **Risk**: Low — только threshold relaxation, additive API field, view re-create. - **Reversibility**: Easy — `HAVING >=30` revert + redeploy. Merging.
lekss361 merged commit 9eed55cd30 into main 2026-05-17 13:39:53 +00:00
Author
Owner

Merged via deep-code-reviewer — verdict APPROVE. Merge commit 9eed55cd.

Post-deploy smoke checks

  1. SQL view applied (после deploy.yml):

    SELECT cad_district_id, district_name, COUNT(*) AS rows
    FROM v_bucket_success_score
    GROUP BY 1, 2
    ORDER BY rows DESC;
    

    Ожидаем — появятся районы, ранее отсутствовавшие (Кировский и др. low-density).

  2. API smoke66:41:0702048:27 (Кировский):

    curl -s "https://gendsgn.ru/api/v1/parcels/analyze?cad_num=66:41:0702048:27" \
      | jq '.success_recommendation'
    

    Ожидаем: non-null объект, data_confidence: "weak", ranking[].n_deals >= 15.

  3. Frontend — открыть участок с 15-29 сделок: amber badge «слабые данные, ориентировочно» рядом с заголовком блока. Участок с ≥30 сделок: badge нет.

Merged via deep-code-reviewer — verdict APPROVE. Merge commit `9eed55cd`. ### Post-deploy smoke checks 1. **SQL view applied** (после `deploy.yml`): ```sql SELECT cad_district_id, district_name, COUNT(*) AS rows FROM v_bucket_success_score GROUP BY 1, 2 ORDER BY rows DESC; ``` Ожидаем — появятся районы, ранее отсутствовавшие (Кировский и др. low-density). 2. **API smoke** — `66:41:0702048:27` (Кировский): ```bash curl -s "https://gendsgn.ru/api/v1/parcels/analyze?cad_num=66:41:0702048:27" \ | jq '.success_recommendation' ``` Ожидаем: non-null объект, `data_confidence: "weak"`, `ranking[].n_deals >= 15`. 3. **Frontend** — открыть участок с 15-29 сделок: amber badge «слабые данные, ориентировочно» рядом с заголовком блока. Участок с ≥30 сделок: badge нет.
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#288
No description provided.