[SF-v2 #950] 950-A: 3-слойный склад §9.3 (open/hidden/future) #970

Closed
opened 2026-06-02 19:20:41 +00:00 by lekss361 · 4 comments
Owner

Часть EPIC #950 (Site Finder v2 / GG-форсайт). Декомпозиция критпути — ground-truth (прод-БД + код) + web-research, 2026-06-02.

Scope: Layer1 open (offer_count/offer_area_m2); Layer2 under-construction (domrf_kn_objects не-проданные); Layer3 pipeline (lots_pd_count/lots_pd_area + plan_completion_date). Заменить/дополнить fixed _aggregate_pipeline.

Файлы: backend/app/services/site_finder/market_metrics.py (или new supply.py); parcels.py _aggregate_pipeline

DoD:

  • response supply_layers:{existing, under_construction, planned} каждый в units+m²
  • bucket по кварталам completion date

Зависит от: 949-B
Effort: M

**Часть EPIC #950 (Site Finder v2 / GG-форсайт).** Декомпозиция критпути — ground-truth (прод-БД + код) + web-research, 2026-06-02. **Scope:** Layer1 open (offer_count/offer_area_m2); Layer2 under-construction (domrf_kn_objects не-проданные); Layer3 pipeline (lots_pd_count/lots_pd_area + plan_completion_date). Заменить/дополнить fixed _aggregate_pipeline. **Файлы:** backend/app/services/site_finder/market_metrics.py (или new supply.py); parcels.py _aggregate_pipeline **DoD:** - response supply_layers:{existing, under_construction, planned} каждый в units+m² - bucket по кварталам completion date **Зависит от:** 949-B **Effort:** M
lekss361 added this to the Site Finder v2 — Foresight (GG-форсайт) milestone 2026-06-02 19:20:45 +00:00
lekss361 added the
analytics
GG-форсайт
scope/backend
site-finder
labels 2026-06-02 19:20:45 +00:00
Author
Owner

🔴 GG-critical (стратегсессия 2026-06-01)

ГГ выделил это как одно из ядровых требований — дословно (00:13:35):

«Система должна видеть, что в данной локации предлагается сегодня, что находится на складе, что находится в предложении — потому что это не тождественная история. Мы получили РС, часть открыли в продаже, часть не открыли, но система должна понимать, что остальное это просто склад, он когда-то поступит в продажу. Это же не скоропортящийся продукт

→ Это и есть Layer-1 (open) vs Layer-2 (hidden/склад) этой задачи. Без разделения «открытое предложение vs склад» прогноз дефицита/затоварки (#981) и распаковки (#980) не построить. Поднято до priority/critical, на критпути демо «Железнодорожники».

## 🔴 GG-critical (стратегсессия 2026-06-01) ГГ выделил это как одно из ядровых требований — дословно (00:13:35): > «Система должна видеть, что в данной локации **предлагается сегодня, что находится на складе, что находится в предложении** — потому что это не тождественная история. Мы получили РС, часть открыли в продаже, часть не открыли, но система должна понимать, что **остальное это просто склад, он когда-то поступит в продажу. Это же не скоропортящийся продукт**.» → Это и есть Layer-1 (open) vs Layer-2 (hidden/склад) этой задачи. Без разделения «открытое предложение vs склад» прогноз дефицита/затоварки (#981) и распаковки (#980) не построить. Поднято до **priority/critical**, на критпути демо «Железнодорожники».
Collaborator

Сделано (merged+deployed на проде). 3-слойный склад §9.3:

  • #1002domrf_kn_objects.free_flats (data unlock для L2) + миграция 124
  • #1003 — таблица supply_layers + view v_supply_layers_latest + миграция 125 (dry-run PG16)
  • #1004 — сервис supply_layers.py (L1 open / L2 hidden mapping-free / L3 future)
  • #1005 — Celery-воркер + beat Mon 06:00 МСК (UPSERT слоёв)

Каждый PR: opus code-reviewer . Прод-данные появятся после первого прогона воркера (Пн 06:00) или ручного триггера.

✅ **Сделано** (merged+deployed на проде). 3-слойный склад §9.3: - #1002 — `domrf_kn_objects.free_flats` (data unlock для L2) + миграция 124 - #1003 — таблица `supply_layers` + view `v_supply_layers_latest` + миграция 125 (dry-run PG16) - #1004 — сервис `supply_layers.py` (L1 open / L2 hidden mapping-free / L3 future) - #1005 — Celery-воркер + beat Mon 06:00 МСК (UPSERT слоёв) Каждый PR: opus code-reviewer ✅. Прод-данные появятся после первого прогона воркера (Пн 06:00) или ручного триггера.
lekss361 reopened this issue 2026-06-03 17:59:31 +00:00
Author
Owner

🔴 Reopen — CRITICAL: Layer3 future supply молча теряется ~77–94% при каждом прогоне. Валидация смерженной реализации (#1004 compute + #1005 persist task + #1003 store) против прод-БД.

Баг. backend/app/workers/tasks/supply_layers_refresh.py _UPSERT_SQL использует ON CONFLICT (layer, district_name, complex_id, obj_class, source, snapshot_date). Для Layer3 все строки одного района имеют идентичный ключ: layer=3, complex_id=NULL, obj_class=NULL, source='domrf_multiphase', snapshot_date=<один прогон> — различаются только по dev_group_name, которого нет ни в колонке, ни в ключе (uq_supply_layers_logical подтверждён: dev_group отсутствует). → Все 9–16 L3-строк района схлопываются на ОДИН конфликт-ключ и перезаписывают друг друга в рамках одного прогона. Остаётся только последний проитерированный застройщик; expected_online_date тоже затирается произвольной группой.

Замер на проде: Академический 16 строк / 13 808 будущих юнитов → выживает ~3 228; Чкаловский 19 980 → ~4 561.

Корень — в #1004 supply_layers.py::compute_layer3_future: dev_group_name попадает только в строку method=…, не как колонка-несущая ключ; o.obj_class селектится в CTE, но в outer-запрос не выводится (dead). Юнит-тесты на MagicMock не ловят (нет реального ON CONFLICT с несколькими dev_group); test_task_coherent_single_snapshot_date даже фиксирует единый snapshot_date — что и гарантирует схлопывание.

Fix: агрегировать L3 до district-грейна перед persist, ЛИБО дать L3 ключ-несущий грейн (dev_group → отдельная колонка или в obj_class). DoD (planned в units+m²) для L3 не выполнен.

Layer1/Layer2 и view v_supply_layers_latest (#1003) — корректны, без претензий.

🔴 **Reopen — CRITICAL: Layer3 future supply молча теряется ~77–94% при каждом прогоне.** Валидация смерженной реализации (#1004 compute + #1005 persist task + #1003 store) против прод-БД. **Баг.** `backend/app/workers/tasks/supply_layers_refresh.py` `_UPSERT_SQL` использует `ON CONFLICT (layer, district_name, complex_id, obj_class, source, snapshot_date)`. Для Layer3 все строки одного района имеют идентичный ключ: `layer=3, complex_id=NULL, obj_class=NULL, source='domrf_multiphase', snapshot_date=<один прогон>` — различаются только по `dev_group_name`, которого **нет ни в колонке, ни в ключе** (`uq_supply_layers_logical` подтверждён: dev_group отсутствует). → Все 9–16 L3-строк района схлопываются на ОДИН конфликт-ключ и перезаписывают друг друга в рамках одного прогона. Остаётся только последний проитерированный застройщик; `expected_online_date` тоже затирается произвольной группой. **Замер на проде:** Академический 16 строк / 13 808 будущих юнитов → выживает ~3 228; Чкаловский 19 980 → ~4 561. **Корень** — в #1004 `supply_layers.py::compute_layer3_future`: `dev_group_name` попадает только в строку `method=…`, не как колонка-несущая ключ; `o.obj_class` селектится в CTE, но в outer-запрос не выводится (dead). Юнит-тесты на MagicMock не ловят (нет реального ON CONFLICT с несколькими dev_group); `test_task_coherent_single_snapshot_date` даже фиксирует единый snapshot_date — что и гарантирует схлопывание. **Fix:** агрегировать L3 до district-грейна перед persist, ЛИБО дать L3 ключ-несущий грейн (dev_group → отдельная колонка или в obj_class). DoD (`planned` в units+m²) для L3 не выполнен. _Layer1/Layer2 и view `v_supply_layers_latest` (#1003) — корректны, без претензий._
Collaborator

FIXED + verified on prod (#1035, migration 128 + worker).

Root cause confirmed exactly as reported: L3 future-supply computed per (district_name, dev_group_name) but dev_group_name was never a key column -> all dev_groups collapsed into one upsert key.

Fix: added supply_layers.dev_group_name, rebuilt uq_supply_layers_logical to (layer,district_name,complex_id,obj_class,dev_group_name,source,snapshot_date) NULLS NOT DISTINCT; worker _UPSERT_SQL + ON CONFLICT threaded dev_group_name; L1/L2=NULL (unaffected).

PROD RE-MEASURE (Akademichesky L3, after supply_layers_refresh on the deployed fix):
BEFORE: 1 row / 607 units
AFTER: 16 rows / 13808 units / 15 dev_groups <-- == ground-truth should-be

Migration deploys before container restart; collapsed data self-healed on the refresh run (42 districts, 176 rows upserted). Closing on prod ground-truth — reopen if you see otherwise.

FIXED + verified on prod (#1035, migration 128 + worker). Root cause confirmed exactly as reported: L3 future-supply computed per (district_name, dev_group_name) but dev_group_name was never a key column -> all dev_groups collapsed into one upsert key. Fix: added supply_layers.dev_group_name, rebuilt uq_supply_layers_logical to (layer,district_name,complex_id,obj_class,dev_group_name,source,snapshot_date) NULLS NOT DISTINCT; worker _UPSERT_SQL + ON CONFLICT threaded dev_group_name; L1/L2=NULL (unaffected). PROD RE-MEASURE (Akademichesky L3, after supply_layers_refresh on the deployed fix): BEFORE: 1 row / 607 units AFTER: 16 rows / 13808 units / 15 dev_groups <-- == ground-truth should-be Migration deploys before container restart; collapsed data self-healed on the refresh run (42 districts, 176 rows upserted). Closing on prod ground-truth — reopen if you see otherwise.
Sign in to join this conversation.
No project
No assignees
2 participants
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#970
No description provided.