[Fable 5][P2] ROLLUP grand-total неотличим от NULL-группы rooms_int — velocity/absorption форсайта может схлопнуться #1214

Closed
opened 2026-06-13 04:49:44 +00:00 by bot-backend · 0 comments
Collaborator

Из армейского ревью Claude Fable 5 (2026-06-11)

Confirmed by 2/2 адверсариальных верификатора, severity P2 (correctness), confidence 0.65.

Файл

backend/app/services/site_finder/market_metrics.py:498

Evidence (цитата кода)

for r in rows:
    cnt = int(r["units_sold_window"] or 0)
    area = float(r["area_sold_window"] or 0.0)
    if r["rooms_int"] is None:
        # ROLLUP grand-total.
        units_total = cnt
        area_total = area

Why bug

_SALES_WINDOW_SQL делает GROUP BY ROLLUP (rooms_int), а rooms_int nullable (NULL реален: sales_series явно обрабатывает None). Проданный лот с rooms_int IS NULL даёт ДВЕ строки с NULL (NULL-группа и итог), неразличимые в Python; порядок без ORDER BY не определён. При MixedAggregate-плане итог идёт первым → NULL-группа затирает units_total/area_total частичным счётом → unit_velocity/absorption_rate/months_of_supply занижены и уходят в base_pace demand_supply_forecast (неверные числа отчёта).

Suggested fix

Добавить в SELECT GROUPING(rooms_int) AS is_total и ветвить по нему; NULL-rooms группу класть в by_room['unknown']; аккумулировать by_room[b] = by_room.get(b, 0) + cnt вместо присваивания.

Verify notes

  • [real/P2] Трасс.+живой тест PG16: дефолтный план ROLLUP(rooms_int) — MixedAggregate, grand-total (Group Key: ()) эмитится ПЕРВЫМ, NULL-группа после, среди hash-строк. rooms_int реально nullable: _normalize_rooms→None («неопределенный тип»), sales_series.py:399 обрабатывает NULL. Один проданный лот с NULL rooms_int → loop затирает units_total частичным счётом (тест: 2000→200) → unit_velocity/absorption занижены, MoS завышен → base_pace (recommendation.py:586). Детерминированно.
  • [real/P2] Защиты нет: rooms_int nullable, ETL явно пишет NULL ('неопределенный тип'→None), WHERE не исключает NULL rooms, GROUPING()/ORDER BY отсутствуют. Проданный лот с NULL rooms даёт 2 строки rooms_int IS NULL (NULL-группа + итог); last-write-wins в цикле зависит от plan-dependent порядка — при hashed grouping sets итог идёт раньше NULL-группы, units_total схлопывается до частичного счёта → unit_velocity→base_pace (demand_supply_forecast.py:596), absorption, MoS неверны.

Provenance

  • Finders: site-finder-A, site-finder-B
  • Confidence: 0.65
  • Originally batched at: main@d796027
## Из армейского ревью Claude Fable 5 (2026-06-11) Confirmed by 2/2 адверсариальных верификатора, severity **P2** (correctness), confidence 0.65. ## Файл [`backend/app/services/site_finder/market_metrics.py:498`](backend/app/services/site_finder/market_metrics.py:498) ## Evidence (цитата кода) ``` for r in rows: cnt = int(r["units_sold_window"] or 0) area = float(r["area_sold_window"] or 0.0) if r["rooms_int"] is None: # ROLLUP grand-total. units_total = cnt area_total = area ``` ## Why bug _SALES_WINDOW_SQL делает GROUP BY ROLLUP (rooms_int), а rooms_int nullable (NULL реален: sales_series явно обрабатывает None). Проданный лот с rooms_int IS NULL даёт ДВЕ строки с NULL (NULL-группа и итог), неразличимые в Python; порядок без ORDER BY не определён. При MixedAggregate-плане итог идёт первым → NULL-группа затирает units_total/area_total частичным счётом → unit_velocity/absorption_rate/months_of_supply занижены и уходят в base_pace demand_supply_forecast (неверные числа отчёта). ## Suggested fix Добавить в SELECT `GROUPING(rooms_int) AS is_total` и ветвить по нему; NULL-rooms группу класть в by_room['unknown']; аккумулировать by_room[b] = by_room.get(b, 0) + cnt вместо присваивания. ## Verify notes - [real/P2] Трасс.+живой тест PG16: дефолтный план ROLLUP(rooms_int) — MixedAggregate, grand-total (Group Key: ()) эмитится ПЕРВЫМ, NULL-группа после, среди hash-строк. rooms_int реально nullable: _normalize_rooms→None («неопределенный тип»), sales_series.py:399 обрабатывает NULL. Один проданный лот с NULL rooms_int → loop затирает units_total частичным счётом (тест: 2000→200) → unit_velocity/absorption занижены, MoS завышен → base_pace (recommendation.py:586). Детерминированно. - [real/P2] Защиты нет: rooms_int nullable, ETL явно пишет NULL ('неопределенный тип'→None), WHERE не исключает NULL rooms, GROUPING()/ORDER BY отсутствуют. Проданный лот с NULL rooms даёт 2 строки rooms_int IS NULL (NULL-группа + итог); last-write-wins в цикле зависит от plan-dependent порядка — при hashed grouping sets итог идёт раньше NULL-группы, units_total схлопывается до частичного счёта → unit_velocity→base_pace (demand_supply_forecast.py:596), absorption, MoS неверны. ## Provenance - Finders: `site-finder-A, site-finder-B` - Confidence: 0.65 - Originally batched at: main@d796027
bot-backend added the
Fable 5 ревью
label 2026-06-13 04:49:44 +00:00
Sign in to join this conversation.
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#1214
No description provided.