fix(sf-09): iterative cap algorithm + cap_skipped flag + unit tests
Review verdict MINOR (PR #282) — single-pass cap_and_redistribute
не гарантировал invariant max ≤ MAX_BUCKET_SHARE_PCT. Surplus от clamped
поднимал free выше cap (например {1k:75, studio:15, 2k:10} → studio=39).
- backend/app/services/site_finder/best_layouts.py
- _cap_and_redistribute → iterative while-loop до сходимости
- Returns (result_map, cap_skipped) — флаг для pathological all-clamped
- Float work map, Hamilton финальный pass для invariant sum=100
- backend/app/schemas/parcel.py
- LayoutTzRecommendation.cap_skipped: bool = False
- backend/tests/services/site_finder/test_best_layouts.py
- 6 parametrized tests: invariants, 7 failing cases from review,
no-dominant unchanged, empty, cap_skipped propagation, normal-case
- frontend/src/types/best-layouts.ts — cap_skipped: boolean
- frontend/src/components/site-finder/BestLayoutsBlock.tsx
- banner условие сменилось с maxPct > 60 на rec.cap_skipped (honest signal)