- Add window_months > 0 guard to vel_by_room dict comprehension (mirrors _monthly_rate)
- Correct outer comment in recommendation.py: honest-zero for known-zero buckets,
fallback only when velocity_by_room=None or bucket absent from _FORECAST_TO_METRIC_BUCKETS
- Add comment in as_dict() noting velocity_by_room is intentionally not serialized
(internal pipeline attr consumed directly by recommendation.py)
Add `velocity_by_room: dict[str, float] | None` to `MarketMetrics` — per-bucket
unit velocity (ед./мес) derived from the existing `sold_by_room` ROLLUP data that
`_query_sales_window` already returns. No new SQL required.
Thread per-bucket velocity through `_demand_only_overlay` via the new
`_FORECAST_TO_METRIC_BUCKETS` constant that maps each forecast bucket to its
market_metrics room-bucket keys. "80+ м²" sums "4" + "5+" keys. Fallback to
aggregate `unit_velocity` when `velocity_by_room` is None (thin-data path).
Previously `base_pace` was identical for all 5 room-buckets, so §9.4 norm and §9.2
base_pace cancelled out in pace/max_pace and ranking was driven purely by §9.5
macro_coef (segment steepness proxy). Now §9.2 reflects real per-bucket observed
demand from objective_lots.contract_date data.
Callers of `compute_market_metrics` that don't use `velocity_by_room` are unaffected
(the new field is additive to the frozen dataclass). All existing callers verified —
none construct `MarketMetrics` directly except the one production site.
Внутренний recommendation→product_scoring контракт-ключ был мислейблом: величина —
темп распродажи нежилого (sell-through, прокси ликвидности/спроса), а НЕ доля нежилого
в объёме застройки. Переименован ключ + исправлены reason/docstring/комментарии у
потребителя _score_commercial. Числовая логика не изменена. Ключ внутренний (нет
frontend/schema/openapi-потребителей) → rename контракт-безопасен. pytest 171 passed.
Closes#1635
Found by read-only services audit.
- recommendation._usp_from_deficits: skip di<=0 so «стройте его» is never emitted
for OVERSUPPLIED formats; all-surplus top-K → [] (no white-space niches).
Aligns with product_scoring._count_positive_usp (di>0). Was: «Дефицит формата
X — стройте его» for a surplus format, reaching PDF/Excel USP-ниши.
- report_assembler._domrf_coverage: drop ambiguous >1.0 percent-guess; normalize
per-branch (analyze pct /100, supply_layers fraction as-is). Sub-1% coverage
(0.8%) no longer read as 80% → no inflated confidence in the near-zero-coverage
case §15 flags. tests for both + end-to-end no-inflation. 241+148 pass.