[Fable 5][P2] L3 future-supply: фильтр по volatile-колонкам внутри DISTINCT ON берёт «последний подходящий» снапшот, а не последний #1212

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.7.

Файл

backend/app/services/site_finder/supply_layers.py:269

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

           AND o.ready_dt IS NOT NULL
           AND o.ready_dt > CURRENT_DATE + CAST(:horizon AS interval)
           AND COALESCE(o.free_flats, 0) <= :max_free_flats
           AND (
                CAST(:district AS text) IS NULL
                OR o.district_name = CAST(:district AS text)
               )
         ORDER BY o.obj_id, o.snapshot_date DESC NULLS LAST

Why bug

В _L3_FUTURE_SQL условия по ready_dt и free_flats стоят в WHERE внутри CTE с DISTINCT ON (obj_id) ORDER BY snapshot_date DESC. domrf_kn_objects хранит историю снапшотов, поэтому берётся «последний снапшот, прошедший фильтр»: проект, когда-то бывший «объявлен, не продаётся», остаётся в layer3 и после открытия продаж (свежий снапшот с большим free_flats отфильтрован, взят старый). Итог: future-supply завышен, двойной счёт с L1/L2, старые flat_count/ready_dt. Докстринг обещает «свежий снапшот, затем фильтр».

Suggested fix

Во внутреннем CTE оставить только стабильные фильтры (region_cd, district_name) + DISTINCT ON; условия ready_dt/free_flats перенести во внешний запрос после выбора последнего снапшота.

Verify notes

  • [real/P2] domrf_kn_objects хранит историю: миграция 50 → UNIQUE(obj_id,snapshot_date), loader UPSERT ON CONFLICT (obj_id,snapshot_date), скрейп еженедельный. WHERE выполняется до DISTINCT ON → L3 берёт последний ПРОШЕДШИЙ фильтр снапшот: после открытия продаж свежая строка (free_flats=180) отсеяна, взята старая (free_flats=0/NULL→0). Объект остаётся в L3: двойной счёт с L1/L2, stale flat_count/ready_dt в supply_layers и форсайте. L2-CTE фильтрует лишь стабильные колонки — в L3 паттерн нарушен.
  • [real/P2] domrf_kn_objects версионируется: 50_schema — UNIQUE(obj_id, snapshot_date), scraper INSERT ON CONFLICT (obj_id, snapshot_date), пруна нет. В _L3_FUTURE_SQL volatile-фильтры (ready_dt, free_flats) в WHERE ДО DISTINCT ON → берётся последний ПРОШЕДШИЙ фильтр снапшот: открывший продажи проект остаётся в L3 по старой строке (pre-м.124 строки: free_flats NULL → COALESCE=0 проходит всегда). Соседний _L2_HIDDEN_SQL фильтрует volatile ПОСЛЕ DISTINCT ON. Тесты — substring-only.

Provenance

  • Finders: site-finder-B
  • Confidence: 0.7
  • Originally batched at: main@d796027
## Из армейского ревью Claude Fable 5 (2026-06-11) Confirmed by 2/2 адверсариальных верификатора, severity **P2** (correctness), confidence 0.7. ## Файл [`backend/app/services/site_finder/supply_layers.py:269`](backend/app/services/site_finder/supply_layers.py:269) ## Evidence (цитата кода) ``` AND o.ready_dt IS NOT NULL AND o.ready_dt > CURRENT_DATE + CAST(:horizon AS interval) AND COALESCE(o.free_flats, 0) <= :max_free_flats AND ( CAST(:district AS text) IS NULL OR o.district_name = CAST(:district AS text) ) ORDER BY o.obj_id, o.snapshot_date DESC NULLS LAST ``` ## Why bug В _L3_FUTURE_SQL условия по ready_dt и free_flats стоят в WHERE внутри CTE с DISTINCT ON (obj_id) ORDER BY snapshot_date DESC. domrf_kn_objects хранит историю снапшотов, поэтому берётся «последний снапшот, прошедший фильтр»: проект, когда-то бывший «объявлен, не продаётся», остаётся в layer3 и после открытия продаж (свежий снапшот с большим free_flats отфильтрован, взят старый). Итог: future-supply завышен, двойной счёт с L1/L2, старые flat_count/ready_dt. Докстринг обещает «свежий снапшот, затем фильтр». ## Suggested fix Во внутреннем CTE оставить только стабильные фильтры (region_cd, district_name) + DISTINCT ON; условия ready_dt/free_flats перенести во внешний запрос после выбора последнего снапшота. ## Verify notes - [real/P2] domrf_kn_objects хранит историю: миграция 50 → UNIQUE(obj_id,snapshot_date), loader UPSERT ON CONFLICT (obj_id,snapshot_date), скрейп еженедельный. WHERE выполняется до DISTINCT ON → L3 берёт последний ПРОШЕДШИЙ фильтр снапшот: после открытия продаж свежая строка (free_flats=180) отсеяна, взята старая (free_flats=0/NULL→0). Объект остаётся в L3: двойной счёт с L1/L2, stale flat_count/ready_dt в supply_layers и форсайте. L2-CTE фильтрует лишь стабильные колонки — в L3 паттерн нарушен. - [real/P2] domrf_kn_objects версионируется: 50_schema — UNIQUE(obj_id, snapshot_date), scraper INSERT ON CONFLICT (obj_id, snapshot_date), пруна нет. В _L3_FUTURE_SQL volatile-фильтры (ready_dt, free_flats) в WHERE ДО DISTINCT ON → берётся последний ПРОШЕДШИЙ фильтр снапшот: открывший продажи проект остаётся в L3 по старой строке (pre-м.124 строки: free_flats NULL → COALESCE=0 проходит всегда). Соседний _L2_HIDDEN_SQL фильтрует volatile ПОСЛЕ DISTINCT ON. Тесты — substring-only. ## Provenance - Finders: `site-finder-B` - Confidence: 0.7 - 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#1212
No description provided.